0%

C++ 中的类型转换

今晚翻了一下 todo list,发现这个条目堆积在未完成列表的末尾,于是来学习一下 C++ 中的四种类型转化方式,而 C++ 的复习也告一段落。我知道我还差得很远,等某天 C++ 功底足够深厚,来写一下 C++ 的内存模型。

static_cast

static_cast 是一个 c++ 运算符,功能是把一个表达式转换为某种类型,依赖编译时得到的类型信息,没有运行时类型检查来保证转换的安全性,用于非多态类型的转换。所以为了安全起见,通常用于转换数值数据类型,如 intdouble

1
2
int a = 5, b = 2; 
double result = static_cast<double>(a) / b;

如果涉及到类层次的类型转换,父到子不安全(向下转换),子到父安全(向上转换,也是隐式转换)。由于不检查转换的安全性,因此向下转换为子类类型时,可能有基类不存在的成员变量和函数,会导致未定义行为,直接报错。

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 <iostream>

using namespace std;

class base{
public:
int a = 10;
};

class child : public base {
public:
void show() {
cout << this->a << endl;
}
};

int main() {

child a;
base b;

auto c = static_cast<base>(a); // fine
auto d = static_cast<child>(b); // fail

return 0;
}

const_cast

const_cast 能给变量添加 const 特性或移除变量的 const 特性,其他的任何转换不能移除 const 修饰。因为可以直接将非 const 变量赋值给 const 变量,因此 const_cast 增加 const 修饰很少用到。此外,如果修饰的引用或指针指向的是 const 修饰的变量,将指针和引用修改为 non-const 会导致未定义行为;相反,如果修饰的变量不是 const,那么转换是安全的。

至于未定义行为是否能编译通过,取决于使用的编译器,不能一概而论。是 UB 不过这样写的话编译器不会报错, 然后一般来说会产生指向一个局部变量的指针, 所以也不会 Segmentation fault

但是如果是字符串常量, 那实现上会存放在 rodata, 运行时加载到了一个只读的页, 如果 const_castconst 然后写入那么就会 Segmentation fault

1
2
3
4
5
6
7
8
9
10
int a = 5; // NOTE: non-const object 
const int* pA = &a; // non-const to const
*pA = 10; // compiler error, pA is a pointer to const int
int* pX = const_cast<int*>(pA); // cast away constness
*pX = 10 // fine and a is now 10

const int a = 5; // NOTE: const object
const int* pA = &a;
int* pX = const_cast<int*>(pA); // cast away constness
*pX = 10 // Free ticket to a long journey of UNDEFINED BEHAVIOR

此外,const_cast 不能修改到其他的数据类型:

1
2
3
4
5
int* a = nullptr; 
const char* ptr = "Hello";
a = const_cast<int*>(ptr); // Fail
a = reinterpret_cast<int*>(ptr); // Fail, reinterpret_cast can't cast away const qualifiers
a = reinterpret_cast<int*>(const_cast<char*>(ptr)); // Fine, as long as you know why you are doing it

reinterpret_cast

这个是最危险的转换,如果使用这个转换,相当于告诉了编译器,我知道我在做什么。假如从 A 类型转换为 B 类型,那么就是基于 bit 模式,用 B 类型重新解释 A 类型之前指向的地址,由于是重新解释地址,因此这个转换不会编译为任何 CPU 指令。滥用 reinterpret_cast 运算符可能很容易带来风险,除非所需转换本身是低级别的,比如 int 到 double,否则建议使用其他强制转换运算符之一。

这个转换允许将任何指针转换为任何其他指针类型(如 char*int*One_class*Unrelated_class* 之类的转换,但其本身并不安全),也允许将任何整数类型转换为任何指针类型以及反向转换。这些转换中唯一正确的是 reinterpret_cast 将变量转换为原类型, 会得到相同的值,但是如果中间变量所占内存小的话就不一定了。听着就危险,此外,reinterpret_cast 运算符不能丢掉 const 修饰。

1
2
3
4
5
6
7
class A { /* ... */ };  
class B { /* ... */ };
A *a = new A{};
B *b = reinterpret_cast<B*>(a); // Fine

const char* message = "hello";
int* data = reinterpret_cast<int*>(message); // can't remove const

dynamic_cast

这个和 RTTI 还有些关系,所以多说一些吧。这个转换应用于用于多态类型的转换,如果被转换的类型不具备多态性,那么向上转换可以在编译时期分析继承关系,确定类型,而且向下转换会直接报错:

1
2
3
4
5
6
7
8
class Base { }; 
class Derived : public Base { };

Base a, *ptr_a;
Derived b, *ptr_b;

ptr_a = dynamic_cast<Base *>(&b); // Fine
ptr_b = dynamic_cast<Derived *>(&a); // Fail

假设此时基类具备了多态性,那么这个转换能够去检验具有继承关系的父子类型的指针、引用的转换是否安全。那么可以将一个指针类型或引用转换为其他多态类型,也可以用来向下转换。由于只适用于指针或引用,那么只能进行运行时类型检查,dynamic_cast 将寻找所需的对象并在可能的情况下返回它,对不明确的指针的转换将失败(返回 nullptr),但不引发异常。来看个例子:

第二个转换失败,因为子类不具有多态性,

1
2
3
4
5
6
7
8
9
10
11
class Base { virtual void dummy() {} }; // polymorphic class 
class Derived : public Base { int a; }; // so is this

Base *ptr_a = new Derived{};
Base *ptr_b = new Base{};

Derived *ptr_c = nullptr;
Derived *ptr_d = nullptr;

ptr_c = dynamic_cast<Derived *>(ptr_a); // Fine, 指向的和转换的一致
ptr_d = dynamic_cast<Derived *>(ptr_b); // ptr_d will be NULL, 指向的和转换的不一致

dynamic_cast 在转换之前,就要确定基类究竟指向的是什么类型的对象。由于基类可能指向任何的自身类型对象、子类类型对象。所以,编译期基类指针是无法确定其指向的对象类型的,只能等到运行时。

typeid

dynamic_cast 外,也能通过 typeid 运算符使用运行时堕胎,获得变量的类型。此时,需要重点注意的是,typeid 可以在编译期将获得变量的类型,也可以在运行期获得变量的类型。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <typeinfo>

using namespace std;

int main() {

int a = 10;
cout << typeid(a).name() << endl;

return 0;
}

而对于无法在编译时期确认的类型,只能等到运行时期。如下所示的程序:

1
2
3
4
5
6
7
class base{
public:
virtual void show() {...}
};

class A: base {virtual void show() override}
class B: base {virtual void show() override}

但是重点是要把方法声明为虚方法:

  • 如果一个方法不是虚方法,那么将根据引用类型或指针类型选择执行的方法,静态联编
  • 如果一个方法是虚方法,将根据指针或引用指向对象的类型选择执行的方法,动态联编
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章