将表达式从一种类型转换为另一种类型被称为类型转换。这可以隐式地或显式地完成。
隐式转换
当表达式需要转换成它的兼容类型之一时,编译器会自动执行隐式转换。例如,原始数据类型之间的任何转换都可以隐式完成。
long a = 5; // int implicitly converted to long
double b = a; // long implicitly converted to double
这些隐式原语转换可以进一步分为两种:晋升和降级。当表达式隐式转换为较大的类型时,发生升级;当表达式转换为较小的类型时,发生降级。因为降级会导致信息丢失,所以这些转换会在大多数编译器上生成警告。如果潜在的信息丢失是有意的,可以通过使用显式的强制转换来抑制警告。
// Promotion
long a = 5; // int promoted to long
double b = a; // long promoted to double
// Demotion
int c = 10.5; // warning: possible loss of data
bool d = c; // warning: possible loss of data
显式转换
第一种显式强制转换是从 C 继承而来的,通常称为 C 风格强制转换 。所需的数据类型只需放在需要转换的表达式左侧的括号中。
int c = (int)10.5; // double demoted to int
char d = (char)c; // int demoted to char
C++ 强制转换
C 样式的强制转换适用于基本数据类型之间的大多数转换。然而,当涉及到类和指针之间的转换时,它可能太强大了。为了更好地控制不同类型的转换,可能的 C++ 引入了四种新的类型转换,称为命名转换或新型转换。这些转换是:静态、重新解释、常量和动态转换。
static_cast<new_type> (expression)
reinterpret_cast<new_type> (expression)
const_cast<new_type> (expression)
dynamic_cast<new_type> (expression)
如上所述,它们的格式是在转换的名称后面加上用尖括号括起来的新类型,然后是用括号括起来的要转换的表达式。这些类型转换可以更精确地控制转换的执行方式,从而使编译器更容易捕捉转换错误。相比之下,C 风格的强制转换在一个操作中包括静态、重新解释和常量强制转换。因此,如果使用不当,该类型转换更有可能执行细微的转换错误。
静态投
静态强制转换在兼容类型之间执行转换。它类似于 C 风格的类型转换,但更具限制性。例如,C 风格的强制转换允许一个整数指针指向一个字符。
char c = 10; // 1 byte
int *p = (int*)&c; // 4 bytes
由于这会导致一个 4 字节的指针指向 1 字节的已分配内存,因此写入该指针将会导致运行时错误,或者会覆盖一些相邻的内存。
*p = 5; // run-time error: stack corruption
与 C 风格的强制转换不同,静态强制转换将允许编译器检查指针和指针对象数据类型是否兼容,这允许程序员在编译期间捕捉这种不正确的指针赋值。
int *q = static_cast<int*>(&c); // compile-time error
重新解释 Cast
为了强制进行指针转换,就像 C 风格的强制转换在后台进行的一样,可以使用 reinterpret 强制转换。
int *r = reinterpret_cast<int*>(&c); // forced conversion
这种强制转换处理某些不相关类型之间的转换,例如从一种指针类型转换到另一种不兼容的指针类型。它将简单地执行数据的二进制复制,而不改变底层的位模式。请注意,这种低级操作的结果是特定于系统的,因此不可移植。如果无法完全避免,则应谨慎使用。
Const Cast
第三种 C++ 强制转换是 const 强制转换。这个主要用于添加或删除变量的const
修饰符。
const int myConst = 5;
int *nonConst = const_cast<int*>(&a); // removes const
尽管 const cast 允许更改常量的值,但这样做仍然是无效的代码,可能会导致运行时错误。例如,如果常量位于只读存储器的某个部分,就会出现这种情况。
*nonConst = 10; // potential run-time error
相反,Const cast 主要用于有一个采用非常量指针参数的函数时,即使它不修改指针对象。
void print(int *p) { std::cout << *p; }
然后,可以通过使用常量强制转换将常量变量传递给该函数。
print(&myConst); // error: cannot convert
// const int* to int*
print(nonConst); // allowed
c 风格和新型造型
请记住,C 风格的强制转换也可以删除const
修饰符,但是同样,因为它在幕后进行这种转换,所以 C++ 强制转换更可取。使用 C++ 类型转换的另一个原因是它们比 C 风格类型转换更容易在源代码中找到。这很重要,因为造型错误可能很难发现。使用 C++ 强制转换的第三个原因是它们写起来不舒服。由于在许多情况下可以避免显式转换,所以这是有意为之,以便程序员可以寻找不同的解决方案。
动态投
第四个也是最后一个 C++ 强制转换是动态强制转换。这个只用于将对象指针和对象引用转换成继承层次结构中的其他指针或引用类型。通过执行运行时检查,确保指针指向目标类型的完整对象,这是确保所指向的对象可以被转换的唯一强制转换。为了使这种运行时检查成为可能,对象必须是多态的。也就是说,该类必须定义或继承至少一个虚函数。这是因为编译器只会为这些对象生成所需的运行时类型信息。
动态转换示例
在下面的例子中,使用动态转换将一个MyChild
指针转换成一个MyBase
指针。这个从派生到基的转换成功了,因为Child
对象包含了一个完整的Base
对象。
class MyBase { public: virtual void test() {} };
class MyChild : public MyBase {};
int main()
{
MyChild *child = new MyChild();
MyBase *base = dynamic_cast<MyBase*>(child); // ok
}
下一个例子试图将一个MyBase
指针转换成一个MyChild
指针。由于Base
对象不包含一个完整的Child
对象,这个指针转换将会失败。为了表明这一点,动态强制转换返回一个空指针。这为在运行时检查转换是否成功提供了一种便捷的方法。
MyBase *base = new MyBase();
MyChild *child = dynamic_cast<MyChild*>(base);
if (child == 0) std::cout << "Null pointer returned";
如果转换的是引用而不是指针,那么动态转换将会失败,抛出一个bad_cast
异常。这需要使用 try-catch 语句来处理。
#include <exception>
// ...
try { MyChild &child = dynamic_cast<MyChild&>(*base); }
catch(std::bad_cast &e)
{
std::cout << e.what(); // bad dynamic_cast
}
动态或静态转换
使用动态强制转换的优点是,它允许程序员在运行时检查转换是否成功。缺点是进行这种检查会带来性能开销。由于这个原因,在第一个例子中使用静态转换会更好,因为从派生到基的转换永远不会失败。
MyBase *base = static_cast<MyBase*>(child); // ok
但是,在第二个示例中,转换可能成功,也可能失败。如果MyBase
对象包含一个MyBase
实例,它将失败;如果它包含一个MyChild
实例,它将成功。在某些情况下,这可能直到运行时才知道。在这种情况下,动态转换是比静态转换更好的选择。
// Succeeds for a MyChild object
MyChild *child = dynamic_cast<MyChild*>(base);
如果使用静态转换而不是动态转换来执行从基到派生的转换,转换就不会失败。它会返回一个指向不完整对象的指针。取消引用这样的指针会导致运行时错误。
// Allowed, but invalid
MyChild *child = static_cast<MyChild*>(base);
// Incomplete MyChild object dereferenced
(*child);