关于C/C++的类型转换和类型别名的整理笔记。

C语言类型转换

从C语言的类型转换开始,包括最基本的显式类型转换(强制类型转换)和隐式类型转换(自动类型转换)。

数值类型转换语义

整数和浮点数之间的类型转换满足舍入原则:

  • 整数转换到浮点数的结果是距离最近的浮点数
  • 浮点数转换到整数的结果是向0舍入的整数(如果得到的整数值不在目标类型范围内,行为未定义)

两个方向的转换均可能损失信息。

整数类型到整数类型的转换满足如下规则:

  • 如果原始整数值可以在目标整数类型下精确表达,那么转换是无损的;
  • 在其它情形下则是有损的转换:
    • 如果目标是无符号整数类型,具体实现是取模意义下的。
    • 如果目标是有符号整数类型,具体实现是未知的。

指针类型转换语义

指向无限定类型对象的指针可以隐式转换成指向该类型有限定版本的指针,也就是说, 我们可以添上constvolatile等限定符,原指针与结果比较相等,例如

1
2
int n;
const int* p = &n; // &n 拥有类型 int*

任何指针类型都可以与void *类型进行双向的隐式转换,例如

1
2
3
int n = 10;
void* vp = &n;
int* ip = &vp;

并且保证:若指针被转换成void *再转换回来,则其值与原指针比较相等。

这部分内容在C语言和C++中是有显著差异的,C++对于指针类型转换,尤其是与void *的转换有更严格的要求,例如指向对象的指针类型可以隐式转换为void *,但是反过来却必须要显式类型转换,并且对于函数指针是不允许隐式转换为void *的。

隐式类型转换时机

隐式类型转换主要发生在下列情景:

  • 不同类型间赋值或初始化
  • 不同类型间算术运算
  • 函数调用时的实参传递,以及return语句

例如在使用不同基本类型的数据进行初始化或赋值时,会自动尝试隐式类型转换

1
float a = 100; // int -> float

赋值对应的类型转换可能会损失精度,例如

1
2
int a = 1.1; // double -> int
// a = 1

在使用不同类型的数据进行算术运算时,编译器也会将某些类型自动转换,例如

1
2
double a = 2.0/3; // 0.6666667
double b = 2/3; // 0

这里2.0/3会将分母自动转换为浮点数进行运算,而2/3只会执行整数除法。

除此之外,隐式类型转换还经常发生在下面两种情景(经常被称为类型退化)

  • 数组类型自动退化为指针(尤其在传参过程中)
  • 函数类型自动退化为函数指针

算术中的隐式类型转换

对于涉及浮点数的二元算术运算,满足如下规则:

  • 如果一个数为double,会试图将另一个数转换为double
  • 否则,如果一个数为float,会试图将另一个数转换为float

对于只涉及整数的二元算术运算,满足如下规则:

  • 首先,对于那些范围小于int的整数类型(例如charbool),将其自动提升为int类型,提升的过程保持值不变(包括符号)。
  • 然后比较两者的类型:
    • 如果两个类型一样,即为公共类型,无须继续转换
    • 否则,两个类型不同:
      • 如果两个类型均为有符号或无符号类型,则自动将小类型转换为大类型
      • 否则,一个为有符号类型T1,一个为无符号类型unsigned T2,继续判断:
        • 如果T1的等级比unsigned T2不低,则T1转换为unsigned T2
        • 否则,T1的等级比unsigned T2
          • 如果T1可以包括unsigned T2,则unsigned T2转换为T1
          • 否则,将T1unsigned T2均转换为unsigned T1

显式类型转换

显式类型转换的语法为(目标类型)表达式,产生的效果相当于用表达式向目标类型赋值时触发的隐式类型转换。 与隐式类型转换相比,它可以向指定的类型进行转换,同时也更容易损失信息。

例如,在整数类型之间的强制转换

1
2
int a = 5;
short b = (short)a; // 将 int 类型强制转换为 short 类型

在浮点数类型之间的强制转换

1
2
double x = 3.14;
float y = (float)x; // 将 double 类型强制转换为 float 类型

整数和浮点数之间的转换

1
2
3
4
5
int a = 42;
double b = (double)a; // 将 int 类型强制转换为 double 类型

float x = 3.14;
int y = (int)x; // 将 float 类型强制转换为 int 类型

除此之外,还有指针类型之间(以及函数指针之间)的转换

1
2
3
int arr[10];
void* ptr = (void*)arr; // 将 int* 指针强制转换为 void* 指针
int* intPtr = (int*)ptr; // 将 void* 指针强制转换为 int* 指针

以及指针类型和整数类型之间的转换,此时的语义是获取或指定指针指向的内存地址

1
2
3
int arr[10];
void* ptr = arr; // 指向数组的指针
long address = (long)ptr; // 获取arr的内存地址

凡是涉及到指针的强制类型转换都是比较危险的,需要谨慎使用,尤其是指针类型和整数类型之间的转换。

Cpp类型转换

C++基本继承了C语言的类型转换规则,此外,C++还需要考虑很多很多额外的内容,过于复杂, 因此我们主要关注面向对象的部分,即自定义类型之间的类型转换,以及新加入的四个显式的强制类型转换关键词的使用

  • const_cast
  • reinterpret_cast
  • static_cast
  • dynamic_cast

modern C++ 建议:

  • 不要使用C语言中常用的void *类型,C++提供了更好的工具去解决同样的问题;
  • 不要使用C语言风格的显式类型转换,使用C++提供的static_cast进行替代;
  • 谨慎使用dynamic_cast,少用const_cast,如果必须使用这两种转换,通常意味着程序在设计上存在问题;
  • 在一般情况下都不要使用reinterpret_cast,因为它非常危险。

自定义类型转换

我们在这里先考虑最简单的单个自定义类型与其它类型的转换,下一节再考虑基类与派生类的转换关系。

自定义类型需要提供特殊的类型转换函数来支持向其它类型进行单向转换,语法形式为

1
2
3
operator new_type(){
...
}

例如 operator int()operator double() 分别代表向intdouble进行的单向类型转换。

反过来我们也需要已知类型向自定义类型的转换,此时我们需要提供单参数的构造函数(又称为隐式转换构造函数)。 在下面的例子中,我们实现了自定义类型Colorstd::string的双向类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <format>
#include <iostream>
#include <string>

class Color {
private:
int red;
int green;
int blue;

public:
Color(int r, int g, int b) : red(r), green(g), blue(b) {}

// 隐式转换构造函数
Color(const std::string &hexCode) {
std::string hex = hexCode;
if (hex[0] == '#') { hex = hex.substr(1); }

if (hex.length() == 6) {
red = std::stoi(hex.substr(0, 2), nullptr, 16);
green = std::stoi(hex.substr(2, 2), nullptr, 16);
blue = std::stoi(hex.substr(4, 2), nullptr, 16);
}
else {
red = green = blue = 0; // 如果格式不正确,设置为 0
}
}

// 类型转换方法
// 将 Color 对象转换为十六进制颜色字符串
operator std::string() const {
return std::format("#{:02X}{:02X}{:02X}", red, green, blue);
}

// 打印 RGB 值
void printRGB() const {
std::cout << std::format("RGB: ({}, {}, {})\n", red, green, blue);
}
};

int main() {
Color temp1(255, 87, 51);
temp1.printRGB();

// 将颜色隐式转换为十六进制颜色代码
std::string hexCode1 = temp1;
std::cout << "Hex: " << hexCode1 << "\n";

// 从十六进制颜色代码字符串转换为颜色
Color temp2 = std::string{"#FF5733"};
temp2.printRGB();

return 0;
}

这里在形式上通过不同类型的赋值来触发隐式类型转换:hexCode1的赋值语句触发了类型转换函数,temp2的赋值语句触发了单参数的构造函数。

我们可以加入explicit修饰词来禁止这两个方向的隐式类型转换,这可以避免很多意外出现的类型转换导致的程序错误(尤其是涉及到整数的自动转换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <format>
#include <iostream>
#include <string>

class Color {
private:
int red;
int green;
int blue;

public:
Color(int r, int g, int b) : red(r), green(g), blue(b) {}

// 隐式转换构造函数
explicit Color(const std::string &hexCode) {
std::string hex = hexCode;
if (hex[0] == '#') { hex = hex.substr(1); }

if (hex.length() == 6) {
red = std::stoi(hex.substr(0, 2), nullptr, 16);
green = std::stoi(hex.substr(2, 2), nullptr, 16);
blue = std::stoi(hex.substr(4, 2), nullptr, 16);
}
else {
red = green = blue = 0; // 如果格式不正确,设置为 0
}
}

// 类型转换方法
// 将 Color 对象转换为十六进制颜色字符串
explicit operator std::string() const {
return std::format("#{:02X}{:02X}{:02X}", red, green, blue);
}

// 打印 RGB 值
void printRGB() const {
std::cout << std::format("RGB: ({}, {}, {})\n", red, green, blue);
}
};

int main() {
// 初始化颜色(从 RGB 值)
Color temp1(255, 87, 51);
temp1.printRGB();

// 将颜色隐式转换为十六进制颜色代码
std::string hexCode1 = static_cast<typename std::string>(temp1); // 显式类型转换
std::cout << "Hex: " << hexCode1 << "\n";

// 从十六进制颜色代码初始化颜色
Color temp2{"#FF5733"}; // 单参数构造
temp2.printRGB();

return 0;
}

注意此时使用方法也需要进行变化:包含隐式转换的赋值不再可用,显式的类型转换以及显式的单参数构造仍然可用,

基类与派生类的类型转换

考虑最简单的自定义类型继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
void hello() const { std::cout << "hello, from Base class\n"; }

virtual void show() const { std::cout << "show, from Base class\n"; }
};

class Derived : public Base {
public:
void hello() const { std::cout << "hello, from Derived class\n"; }

void show() const override { std::cout << "show, from Derived class\n"; }
};

这里为了演示效果,基类和派生类均实现了虚函数show()和非虚函数hello()

由于C++支持多继承,以及为解决菱形继承所引入的虚继承,在复杂的继承关系下的讨论也更加复杂,这里我们主要考虑最简单的单继承情形。

值类型转换

由于派生类包含了基类的数据,派生类向基类的值转换通常是安全的,C++允许派生类向基类的类型转换(包括隐式类型转换), 但是显然派生类可能含有基类没有的数据,因此C++禁止了基类向派生类的类型转换(编译报错)。

值的转换例如

1
2
3
4
5
6
7
8
9
10
int main() {
Base baseobj;
Derived derivedobj;

Base baseobj2 = derivedobj; // 派生类对象隐式转换为基类对象
Base baseobj3 = static_cast<Base>(derivedobj); // 显式转换
// derivedobj = baseobj; // 编译报错

return 0;
}

指针/引用类型转换

除了值的转换,更重要的是基类与派生类的指针/引用之间的转换,C++的动态多态机制需要借助这部分语法实现,这里主要以指针举例,引用的使用没什么区别。 由于我们转换的是指针(或引用),转换过程不会修改派生类的任何数据。

通常将基类向派生类指针/引用的转换称为向下转换,将派生类向基类的指针/引用转换称为向上转换。

向上转换是安全的,C++允许直接进行隐式转换

1
2
3
4
5
6
7
int main() {
Derived derivedObj;
Base *basePtr = &derivedObj; // 派生类指针隐式转换为基类指针
basePtr->hello(); // hello, from Base class
basePtr->show(); // show, from Derived class
return 0;
}

关于这两个方法的调用效果:

  • 对于非虚函数hello(),调用完全由指针的类型决定,即调用的是基类实现的版本。
  • 对于虚函数show(),虽然指针类型是基类,但是调用根据实际对象的虚函数指针所指向的虚函数表,获取并调用派生类实现的版本。

向下的转换通常是不确定且不安全的,C++不支持隐式的向下转换。 C++提供了两种显式的类型转换语法,既可以用于向下转换,也可以用于向上转换

  • static_cast,仅在编译期转换,返回目标类型对象的指针或引用,转换不够安全;(static_cast的用处很广,这只是一个应用)
  • dynamic_cast,包括运行时的安全检查,如果检查通过,返回目标类型对象的指针或引用;如果检查失败,则返回 nullptr(对于指针类型)或抛出异常(对于引用类型)。(这是dynamic_cast的主要使用场景)

对于当前的简单继承关系,显式或隐式的向上转换都可以成功进行,结果都没有区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
Derived derivedObj;

Base *basePtr = &derivedObj;
basePtr->hello(); // hello, from Base class
basePtr->show(); // show, from Derived class

Base *basePtr2 = static_cast<Base *>(&derivedObj);
basePtr2->hello(); // hello, from Base class
basePtr2->show(); // show, from Derived class

Base *basePtr3 = static_cast<Base *>(&derivedObj);
basePtr3->hello(); // hello, from Base class
basePtr3->show(); // show, from Derived class

return 0;
}

两种向下转换的实验如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
Derived *derivedObj = static_cast<Derived *>(new Base{});
derivedObj->hello(); // hello, from Base class
derivedObj->show(); // show, from Derived class

Derived *derivedObj2 = dynamic_cast<Derived *>(new Base{});
if (derivedObj2 != nullptr) { // 转换失败
derivedObj2->hello();
derivedObj2->show();
}

Base *tmp = new Derived{};
Derived *derivedObj3 = dynamic_cast<Derived *>(tmp);
if (derivedObj3 != nullptr) {
derivedObj3->hello(); // hello, from Base class
derivedObj3->show(); // show, from Derived class
}

return 0;
}

其中dynamic_cast必须要求指针指向的实际对象类型是目标类型(或其派生类型),因此从Base对象进行的转换是失败的,从Derived对象进行的转换是成功的,即使它们都通过Base *指针传递。

关于这两种类型转换的具体讨论见下文。

const_cast

这种强制转换比较简单,只能用于指针或引用的constvolatile属性的添加或移除,由于这些属性只在编译期有意义,这个转换也只对编译器的指令生成有意义。

1
const_cast<new_type>(expression)

其中new_type必须是一个指针、引用或者指向对象类型成员的指针类型。例如

1
2
3
4
5
6
7
8
int main()
{
int i = 3; // i 不是 const
const int& rci = i;
const_cast<int&>(rci) = 4; // OK:移除const,实际在修改 i
std::cout << "i = " << i << '\n';
return 0;
}

这里如果对i的定义改为const int,那么会出现一个经典的问题:编译器认为i不可变,因此在输出语句中直接将其替换为了常量3,输出结果仍然为i = 3。 虽然我们使用类型转换强行修改了对应的内存值,但是这种行为与i的不变语义以及编译器的优化处理产生了矛盾,并没有达到修改输出的预期效果。

reinterpret_cast

reinterpret_cast 代表的强制转换与其它的类型转换都很不相同:它严格保持在内存中的二进制数据不变,只是重新按照新的类型对内存中的二进制数据进行解释。const_cast类似,这个转换只对编译器的语法解析有意义,没有任何对应CPU指令的语义效果。

它允许任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换。 它的缺点是转换始终是不安全的:无法保证内存中的数据解释为新的类型是有意义的。

例如

1
2
3
4
5
6
int a = 42;
int* ptr = reinterpret_cast<int*>(a); // 将整数转换为指针类型

int arr[10];
int* ptr = arr;
uintptr_t intPtr = reinterpret_cast<uintptr_t>(ptr); // 将指针转换为整数类型

如果必须使用这种转换,但是代码审查规范中明确禁止使用reinterpret_cast,可以使用两次static_cast的做法进行替换,第一次转换到void *,第二次转换为目标类型。

dynamic_cast

dynamic_cast提供的是沿继承层级向上、向下及侧向,安全地进行的指针或引用的类型转换。 它的特点是在转换之前执行运行时的安全检查,如果检查通过,返回目标类型对象的指针或引用;如果检查失败,则返回 nullptr(对于指针类型)或抛出异常(对于引用类型)。

1
2
dynamic_cast<new_type*>(expression)
dynamic_cast<new_type&>(expression)

安全检查的具体内容如下:

  • 如果输入的是空指针,则检查必然失败,返回空指针。
  • 如果转换的两个类型不含有虚函数(包括虚析构函数),则检查必然失败,因为运行时检查需要依赖运行时类型信息,后者要求类含有虚函数(包括虚析构函数)。
  • 如果输入的指针或引用指向的实际对象的类型和new_type不相容(即实际对象类型不是new_typenew_type的派生类型),则检查失败。

dynamic_cast类型转换的检查只与指针指向的实际对象类型和new_type有关,并不关心表面上的指针类型。(引用同理)

例如考虑下面的分叉继承关系(A->B代表B继承自A

1
2
A -> B
A -> C

进行如下的实验(注释行代表转换失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cassert>

class A {
public:
virtual ~A() = default; // 必要的
};

class B : public A {};

class C : public A {};

int main() {
A *tmp1 = new A{};
assert(dynamic_cast<A *>(tmp1) != nullptr);
// assert(dynamic_cast<B *>(tmp1) != nullptr);
// assert(dynamic_cast<C *>(tmp1) != nullptr);

A *tmp2 = new B{};
assert(dynamic_cast<A *>(tmp2) != nullptr);
assert(dynamic_cast<B *>(tmp2) != nullptr);
// assert(dynamic_cast<C *>(tmp2) != nullptr);

B *tmp3 = new B{};
assert(dynamic_cast<A *>(tmp3) != nullptr);
assert(dynamic_cast<B *>(tmp3) != nullptr);
// assert(dynamic_cast<C *>(tmp3) != nullptr);

return 0;
}

实验结果说明:实际类型为A的对象不允许转换为BC,实际类型为B的对象不允许转换为C

再考虑一个更复杂的典型菱形继承关系

1
2
3
A -> B
A -> C
B,C -> D

进行如下的实验(注释行代表转换失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <cassert>

class A {
public:
virtual ~A() = default;
};

class B : public virtual A {};

class C : public virtual A {};

class D : public B, public C {};

int main() {
A *tmp1 = new A{};
assert(dynamic_cast<A *>(tmp1) != nullptr);
// assert(dynamic_cast<B *>(tmp1) != nullptr);
// assert(dynamic_cast<C *>(tmp1) != nullptr);
// assert(dynamic_cast<D *>(tmp1) != nullptr);

A *tmp2 = new B{};
assert(dynamic_cast<A *>(tmp2) != nullptr);
assert(dynamic_cast<B *>(tmp2) != nullptr);
// assert(dynamic_cast<C *>(tmp2) != nullptr);
// assert(dynamic_cast<D *>(tmp2) != nullptr);

A *tmp3 = new D{};
assert(dynamic_cast<A *>(tmp3) != nullptr);
assert(dynamic_cast<B *>(tmp3) != nullptr);
assert(dynamic_cast<C *>(tmp3) != nullptr);
assert(dynamic_cast<D *>(tmp3) != nullptr);

B *tmp4 = new B{};
assert(dynamic_cast<A *>(tmp4) != nullptr);
assert(dynamic_cast<B *>(tmp4) != nullptr);
// assert(dynamic_cast<C *>(tmp4) != nullptr);
// assert(dynamic_cast<D *>(tmp4) != nullptr);

B *tmp5 = new D{};
assert(dynamic_cast<A *>(tmp5) != nullptr);
assert(dynamic_cast<B *>(tmp5) != nullptr);
assert(dynamic_cast<C *>(tmp5) != nullptr);
assert(dynamic_cast<D *>(tmp5) != nullptr);

D *tmp6 = new D{};
assert(dynamic_cast<A *>(tmp6) != nullptr);
assert(dynamic_cast<B *>(tmp6) != nullptr);
assert(dynamic_cast<C *>(tmp6) != nullptr);
assert(dynamic_cast<D *>(tmp6) != nullptr);

return 0;
}

实验结果说明:实际类型为A的对象不允许转换为BCD,实际类型为B的对象不允许转换为CD,实际类型为D的对象则可以任意转换为ABC类型。

static_cast

static_cast是C++提供的用于完全替代C语言显式类型转换语法的工具,主要进行那些比较自然的低风险的类型转换, 使用例如

1
2
int i = 42;
float f = static_cast<float>(i); // 将 int 显式转换为 float

这个做法的代码比原本的(float)i可读性更高,在代码中更加突出,容易检查。

除此之外,static_cast也可以用于存在继承关系的自定义类型之间的转换, 局限在这个情境下,static_cast成功转换的要求和dynamic_cast并不一样, 转换失败既可能直接编译失败,也可能触发未定义行为。 与dynamic_cast不同的是,static_cast执行的只是编译期转换,完全不涉及运行时检查,因此无法获取指针/引用的实际对象类型。

static_cast类型转换的检查只与指针类型和目标类型有关,与指针在运行时指向的实际类型无关。(引用同理)

例如考虑下面的分叉继承关系(A->B代表B继承自A

1
2
A -> B
A -> C

进行如下的实验(注释行代表转换失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cassert>

class A {};

class B : public A {};

class C : public A {};

int main() {
A *tmp1 = new A{};
assert(static_cast<A *>(tmp1) != nullptr);
assert(static_cast<B *>(tmp1) != nullptr);
assert(static_cast<C *>(tmp1) != nullptr);

A *tmp2 = new B{};
assert(static_cast<A *>(tmp2) != nullptr);
assert(static_cast<B *>(tmp2) != nullptr);
assert(static_cast<C *>(tmp2) != nullptr);

B *tmp3 = new B{};
assert(static_cast<A *>(tmp3) != nullptr);
assert(static_cast<B *>(tmp3) != nullptr);
// assert(static_cast<C *>(tmp3) != nullptr);

return 0;
}

实验结果的解释:BC没有继承或被继承的关系,因此B*C*的转换失败,其它的均存在向上或向下的继承关系,因此转换成功。

再考虑一个更复杂的典型菱形继承关系

1
2
3
A -> B
A -> C
B,C -> D

进行如下的实验(注释行代表转换失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <cassert>

class A {
public:
virtual ~A() = default;
};

class B : public virtual A {};

class C : public virtual A {};

class D : public B, public C {};

int main() {
A *tmp1 = new A{};
assert(static_cast<A *>(tmp1) != nullptr);
// assert(static_cast<B *>(tmp1) != nullptr);
// assert(static_cast<C *>(tmp1) != nullptr);
// assert(static_cast<D *>(tmp1) != nullptr);

A *tmp2 = new B{};
assert(static_cast<A *>(tmp2) != nullptr);
// assert(static_cast<B *>(tmp2) != nullptr);
// assert(static_cast<C *>(tmp2) != nullptr);
// assert(static_cast<D *>(tmp2) != nullptr);

A *tmp3 = new D{};
assert(static_cast<A *>(tmp3) != nullptr);
// assert(static_cast<B *>(tmp3) != nullptr);
// assert(static_cast<C *>(tmp3) != nullptr);
// assert(static_cast<D *>(tmp3) != nullptr);

B *tmp4 = new B{};
assert(static_cast<A *>(tmp4) != nullptr);
assert(static_cast<B *>(tmp4) != nullptr);
// assert(static_cast<C *>(tmp4) != nullptr);
assert(static_cast<D *>(tmp4) != nullptr);

B *tmp5 = new D{};
assert(static_cast<A *>(tmp5) != nullptr);
assert(static_cast<B *>(tmp5) != nullptr);
// assert(static_cast<C *>(tmp5) != nullptr);
assert(static_cast<D *>(tmp5) != nullptr);

D *tmp6 = new D{};
assert(static_cast<A *>(tmp6) != nullptr);
assert(static_cast<B *>(tmp6) != nullptr);
assert(static_cast<C *>(tmp6) != nullptr);
assert(static_cast<D *>(tmp6) != nullptr);

return 0;
}

实验结果的解释:由于菱形继承关系的二义性导致指针类型A*B*C*D*的转换全部失败了,BC完全没有继承或被继承的关系,因此B*C*的转换失败。

dynamic_cast vs static_cast

在处理继承关系下的类型转换时,通常会采用dynamic_caststatic_cast进行,这里结合上面的实验,简要对比并总结两者的区别:

  • 转换时间和效率不同:
    • static_cast的转换发生在编译期,效率高
    • dynamic_cast的转换发生在运行时,转换之前需要引入运行时检查,效率较低
  • 转换检查不同:
    • static_cast检查指针类型目标类型是否存在继承或被继承关系
    • dynamic_cast检查指针或引用的实际对象类型是不是目标类型或其派生类型(要求类型必须具有虚函数或虚析构函数)
  • 安全性不同:
    • static_cast只能利用编译期信息进行检查,安全性较低,转换失败会导致编译报错(也可能触发未定义行为)
    • dynamic_cast利用了运行期信息进行检查,安全性更高,检查失败会返回空指针或抛出异常

除此之外,实际上reinterpret_cast也可以用于自定义类型的转换,但是比这两个主要的强制类型转换更危险。

C/Cpp类型别名

typedef

typedef是用于为类型定义别名的关键字,在C和C++中都可以使用。

最简单的用法是对基本类型起别名,例如

1
typedef int Length;

下面的是MSVC的stdint.h中的部分源码,对基本数据类型起了意义更明确的别名

1
2
3
4
5
6
7
8
typedef signed char        int8_t;
typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

使用typedef可以为结构体定义一个别名,以简化代码的书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// before
struct Point{
int x;
int y;
};

struct Point p;

// after
typedef struct Point{
int x;
int y;
} Point;

Point p;

对函数指针类型本身也过于复杂了,经常会使用typedef为其定义一个别名,以增强可读性

1
2
3
4
5
6
7
typedef int (*FuncType)(int, int);

int add(int a, int b) {
return a + b;
}

FuncType f = add;

using

using在C++中可以用于定义类型别名,相比于C语言提供的typedefusing具有更高的可读性和灵活性。

最基本的使用如下

1
using Length = unsigned int;

在C++中没有必要为结构体重命名,因为结构体的使用本身就很简洁了。

可以使用using为函数指针类型起一个别名

1
2
3
4
5
6
7
using FuncType = int(*)(int, int);

int add(int a, int b) {
return a + b;
}

FuncType f = add;

作为与typedef的对比,我们考虑一个比较复杂的函数指针类型:输入一个int和一个形如bool(*)(double)的函数指针,返回一个std::pair<int,double>数据,显然using的可读性更高

1
2
3
typedef std::pair<int, double> (*ComplexFuncType)(int, bool (*)(double));

using ComplexFuncType = std::pair<int, double> (*)(int, bool (*)(double));

using可以在模板类型中使用时,用于定义模板类型的别名(typedef不支持涉及模板类型的别名)

1
2
3
4
template<typename T>
using Vec = std::vector<T>;

Vec<int> v; // std::vector<int> v;

现代C++建议使用using完全替代typedef,以提高代码的可读性。