重要的事情说三遍:
隐式转换
隐式转换在值被复制到兼容类型时会自动执行。例如:
short a = 2000;
int b;
b = a;
在这里,a
的值从 short
类型提升到 int
类型,不需要任何显式操作符。这被称为“标准转换”。标准转换影响基本数据类型,并允许在数值类型之间(例如 short
到 int
,int
到 float
,double
到 int
等),bool
类型之间以及某些指针转换之间进行转换。
从较小的整数类型转换为 int
或从 float
转换为 double
被称为“提升”,并且保证在目标类型中产生相同的值。其他算术类型之间的转换可能无法始终表示相同的值:
- 如果将负整数值转换为无符号类型,结果值将对应其二进制补码表示(例如,
-1
成为该类型的最大值,-2
成为第二大值,依此类推)。 - 从/到
bool
的转换将false
视为零(对于数值类型)或空指针(对于指针类型);true
等价于所有其他值,并转换为等价的1
。 - 如果从浮点类型转换为整数类型,值会被截断(小数部分被移除)。如果结果超出类型所能表示的范围,则转换会导致未定义行为。
- 否则,如果转换在相同种类的数值类型之间进行(整数到整数或浮点到浮点),则转换是有效的,但值是实现特定的(可能不具有可移植性)。
一些转换可能意味着精度的丢失,编译器可以通过警告信号提示。这种警告可以通过显式转换来避免。
对于非基本类型,数组和函数会隐式转换为指针,指针一般允许以下转换:
- 空指针可以转换为任何类型的指针。
- 任何类型的指针可以转换为
void
指针。 - 指针上转:指向派生类的指针可以转换为指向可访问和无歧义的基类的指针,而不会修改其
const
或volatile
限定。
类的隐式转换
在类的世界中,可以通过三种成员函数来控制隐式转换:
- 单参数构造函数:允许从特定类型进行隐式转换以初始化对象。
- 赋值运算符:允许在赋值时从特定类型进行隐式转换。
- 类型转换运算符:允许转换为特定类型。
例如:
// 类的隐式转换:
#include <iostream>
using namespace std;
class A {};
class B {
public:
// 从 A 转换(构造函数):
B (const A& x) {}
// 从 A 转换(赋值运算符):
B& operator= (const A& x) {return *this;}
// 转换为 A(类型转换运算符)
operator A() {return A();}
};
int main ()
{
A foo;
B bar = foo; // 调用构造函数
bar = foo; // 调用赋值运算符
foo = bar; // 调用类型转换运算符
return 0;
}
类型转换运算符使用特定的语法:它使用 operator
关键字,后跟目标类型和一个空括号。注意,返回类型是目标类型,因此在 operator
关键字之前不需要指定返回类型。
关键字 explicit
在函数调用中,C++ 允许对每个参数进行一次隐式转换。这对于类来说可能有些问题,因为这并不总是预期的行为。例如,如果我们在上一个例子中添加以下函数:
void fn (B arg) {}
该函数接受类型为 B
的参数,但它也可以接受类型为 A
的对象作为参数:
fn (foo);
这可能不是预期的情况,但无论如何,可以通过使用 explicit
关键字标记受影响的构造函数来防止这种情况:
// explicit:
#include <iostream>
using namespace std;
class A {};
class B {
public:
explicit B (const A& x) {}
B& operator= (const A& x) {return *this;}
operator A() {return A();}
};
void fn (B x) {}
int main ()
{
A foo;
B bar (foo);
bar = foo;
foo = bar;
// fn (foo); // 不允许用于显式构造函数。
fn (bar);
return 0;
}
此外,使用 explicit
标记的构造函数不能使用赋值语法调用;在上面的例子中,bar
不能用以下方式构造:
B bar = foo;
类型转换成员函数(在前一节中描述的那些)也可以指定为 explicit
。这可以防止隐式转换,就像 explicit
构造函数对目标类型所做的一样。
类型转换
C++ 是一种强类型语言。许多转换,特别是那些涉及值不同解释的转换,需要显式转换,在 C++ 中称为“类型转换”。存在两种主要的泛型类型转换语法:函数式和 C 风格:
double x = 10.3;
int y;
y = int (x); // 函数式语法
y = (int) x; // C 风格类型转换语法
这些通用类型转换形式的功能对于大多数基本数据类型的需求来说已经足够。然而,这些运算符可以不加区分地应用于类和类指针,这可能导致代码在语法上正确但在运行时出错。例如,以下代码可以无错误地编译:
// 类类型转换
#include <iostream>
using namespace std;
class Dummy {
double i, j;
};
class Addition {
int x, y;
public:
Addition (int a, int b) { x = a; y = b; }
int result() { return x + y; }
};
int main () {
Dummy d;
Addition* padd;
padd = (Addition*) &d;
cout << padd->result();
return 0;
}
程序声明了一个指向 Addition
的指针,但随后将其分配给一个指向另一个不相关类型对象的引用,使用显式类型转换:
padd = (Addition*) &d;
不受限制的显式类型转换允许将任何指针转换为任何其他指针类型,而不考虑它们指向的类型。随后的对成员 result
的调用会产生运行时错误或其他意外结果。
为了控制这些类之间的转换,我们有四个特定的类型转换运算符:dynamic_cast
、reinterpret_cast
、static_cast
和 const_cast
。它们的格式是新类型用尖括号 (<>
) 包围,并紧接其后的是需要转换的表达式用括号包围。
dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
static_cast <new_type> (expression)
const_cast <new_type> (expression)
这些表达式的传统类型转换等价形式是:
(new_type) expression
new_type (expression)
但每个都有自己的特殊特性:
dynamic_cast
- 用途:主要用于在运行时进行安全的向下转换(downcasting),即将基类指针或引用转换为派生类指针或引用。
- 特性:使用时需要运行时类型识别(RTTI),只能用于多态类型(即包含虚函数的类)。
- 转换失败时:
- 对指针类型,转换失败会返回
nullptr
。 - 对引用类型,转换失败会抛出
std::bad_cast
异常。
- 对指针类型,转换失败会返回
dynamic_cast
只能用于指向类的指针和引用(或 void*
)。其目的是确保类型转换的结果指向目标指针类型的有效完整对象。
这自然包括指针上转(从派生类指针转换为基类指针),与隐式转换相同。
但 dynamic_cast
还可以将多态类(那些具有虚成员的类)从基类指针转换为派生类指针(即“下转”),前提是指向的对象是目标类型的完整对象。例如:
// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;
class Base { virtual void dummy() {} };
class Derived: public Base { int a; };
int main () {
try {
Base* pba = new Derived;
Base* pbb = new Base;
Derived* pd;
pd = dynamic_cast<Derived*>(pba);
if (pd == 0) cout << "第一次类型转换的空指针。\n";
pd = dynamic_cast<Derived*>(pbb);
if (pd == 0) cout << "第二次类型转换的空指针。\n";
} catch (exception& e) { cout << "异常: " << e.what(); }
return 0;
}
兼容性说明:这种类型的
dynamic_cast
需要运行时类型信息 (RTTI) 来跟踪动态类型。一些编译器支持此功能作为一个默认禁用的选项。需要启用此功能才能使dynamic_cast
在运行时类型检查中正常工作。
上面的代码尝试从类型为 Base*
(pba
和 pbb
)的指针对象执行两次动态转换到类型为 Derived*
的指针对象,但只有第一次成功。注意它们各自的初始化:
Base* pba = new Derived;
Base* pbb = new Base;
尽管它们都是类型为 Base*
的指针,但 pba
实际上指向一个 Derived
类型的对象,而 pbb
指向一个 Base
类型的对象。因此,当使用 dynamic_cast
执行它们各自的类型转换时,pba
指向的是一个完整的 Derived
类对象,而 pbb
指向的是一个 Base
类对象,这是一个不完整的 Derived
类对象。
当 dynamic_cast
不能转换指针时(因为它不是所需类的完整对象),它会返回一个空指针以表示失败。如果 dynamic_cast
用于转换为引用类型且转换不可能,会抛出类型为 bad_cast
的异常。
dynamic_cast
还可以执行指针之间的其他隐式转换:在指针类型之间(即使在不相关的类之间)转换空指针,以及将任何类型的指针转换为 void*
指针。
static_cast
- 用途:用于在编译时进行类型安全的转换。可以进行基本类型之间的转换、向上转换(upcasting)和向下转换(需要确保安全)。
- 特性:不会进行运行时类型检查,只要在编译时类型兼容就可以转换。
static_cast
可以在相关类的指针之间执行转换,不仅可以上转(从派生类指针转换为基类指针),还可以下转(从基类指针转换为派生类指针)。在运行时不执行任何检查以保证被转换的对象实际上是目标类型的完整对象。因此,程序员需要确保转换是安全的。另一方面,它不承担 dynamic_cast
的类型安全检查的开销。
class Base {};
class Derived : public Base {};
Base* a = new Base;
Derived* b = static_cast<Derived*>(a);
这段代码是合法的,尽管 b
会指向类的一个不完整对象,若解引用它可能导致运行时错误。
因此,static_cast
能够在类指针上执行隐式允许的转换,以及它们的相反转换。
static_cast
还能够执行所有隐式允许的转换(不仅限于类指针),并且还能够执行这些转换的反向操作。它可以:
- 将
void*
转换为任何指针类型。在这种情况下,它保证如果void*
值是通过从同一指针类型转换而获得的,则结果指针值相同。 - 将整数、浮点值和枚举类型转换为枚举类型。
此外,static_cast
还可以执行以下操作: - 显式调用单参数构造函数或转换运算符。
- 转换为右值引用。
- 将枚举类值转换为整数或浮点值。
- 将任何类型转换为
void
,评估并丢弃值。
reinterpret_cast
- 用途:用于进行低级别的、几乎没有任何类型检查的强制转换。可以用来在指针和整数之间转换,或者在不同的指针类型之间转换。
- 特性:不安全,通常用于底层操作或需要与硬件接口的场合。
- 注意:转换结果依赖于实现,可能导致未定义行为。
reinterpret_cast
将任何指针类型转换为任何其他指针类型,即使是不相关类的指针类型。操作结果只是简单的二进制复制指针值。
它还可以将指针转换为整数类型或从整数类型转换为指针类型。这种整数值表示指针的格式是平台特定的。唯一的保证是将指针转换为足够大的整数类型(如 intptr_t
)来完全包含它,并且保证可以转换回有效的指针。
reinterpret_cast
能执行的转换是基于重新解释类型的二进制表示,通常会导致系统特定的代码,因此不具备可移植性。例如:
class A { /* ... */ };
class B { /* ... */ };
A* a = new A;
B* b = reinterpret_cast<B*>(a);
这段代码可以编译,尽管它没有多大意义,因为现在 b
指向一个完全不相关且可能不兼容的类对象。解引用 b
是不安全的。
const_cast
- 用途:用于添加或移除
const
或volatile
修饰符,但不能用于将const
对象转化为非const
对象并修改其值(这样做会导致未定义行为)。 - 特性:常用于需要对
const
对象进行某些特殊操作时,例如通过API传递常量对象,但API需要非const
参数。
这种类型转换操作符操纵指针所指对象的常量性,可以设置或移除。例如,为了将常量指针传递给期望非常量参数的函数:
// const_cast
#include <iostream>
using namespace std;
void print (char* str) {
cout << str << '\n';
}
int main () {
const char* c = "sample text";
print (const_cast<char*> (c));
return 0;
}
上面的例子是可以保证工作的,因为函数 print
不会写入指向的对象。请注意,移除指向对象的常量性然后实际写入它会导致未定义行为。
typeid
typeid
允许检查表达式的类型:
typeid (expression)
该运算符返回一个对常量对象的引用,类型为 type_info
,它在标准头文件 <typeinfo>
中定义。由 typeid
返回的值可以用 ==
和 !=
运算符与另一个 typeid
返回的值进行比较,或者可以通过其 name()
成员获取表示数据类型或类名的以空字符结尾的字符串。
// typeid
#include <iostream>
#include <typeinfo>
using namespace std;
int main () {
int* a, b;
a = 0; b = 0;
if (typeid(a) != typeid(b)) {
cout << "a 和 b 是不同类型:\n";
cout << "a 是: " << typeid(a).name() << '\n';
cout << "b 是: " << typeid(b).name() << '\n';
}
return 0;
}
当 typeid
应用于类时,typeid
使用 RTTI 来跟踪动态类型。当 typeid
应用于多态类的表达式时,结果是最派生的完整对象的类型:
// typeid, 多态类
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;
class Base { virtual void f(){} };
class Derived : public Base {};
int main () {
try {
Base* a = new Base;
Base* b = new Derived;
cout << "a 是: " << typeid(a).name() << '\n';
cout << "b 是: " << typeid(b).name() << '\n';
cout << "*a 是: " << typeid(*a).name() << '\n';
cout << "*b 是: " << typeid(*b).name() << '\n';
} catch (exception& e) { cout << "异常: " << e.what() << '\n'; }
return 0;
}
注意:type_info
成员 name
返回的字符串取决于编译器和库的具体实现。它不一定是一个简单的典型类型名称字符串,像本输出中的编译器。
请注意,typeid
对指针类型的考虑是指针类型本身(a
和 b
都是 class Base *
类型)。然而,当 typeid
应用于对象时(如 *a
和 *b
),typeid
会得到它们的动态类型(即最派生的完整对象的类型)。
如果 typeid
评估的类型是一个由解引用运算符(*
)前缀的指针,并且该指针的值为 null,typeid
会抛出 bad_typeid
异常。