本篇博客让我们来见识一下C++中新增的类型转换方法

[TOC]

1.C语言中类型转换

在C语言中,类型转换有下面两种形式

  • 隐式类型转换
  • 显示强制类型转换int a=(int)'c'

这两种方式想必各位都很熟悉了,但隐式类型转换在一些场景里面会出现问题

1
2
3
4
5
6
7
8
9
void insert(size_t pos,char c)
{
int end=10;
while(end>=pos)
{
end--;
}
//...
}

上面的代码中,end是int类型,当进行比较的时候,end会-1直到小于pos

如果pos=0,问题就来了!

隐式类型转换会让end在比较的时候被转换为size_t无符号整型,而在无符号整型中,-1是一个非常大的正数,从而导致这个函数进入死循环!


  • 隐式类型转换可能会丢失数据的精度
  • 显示类型转换的写法都一样,导致不能很好的区分情况

C++委员会也是认识到了这里的问题,当产生隐式类型转换的时候,难以跟踪错误的来源,于是开发了下面的新的类型转换方式

不过,因为C++兼容C语言,所以C中的转换方式依旧支持


2.C++中的强制类型转换

C++中新增了下面四种明明的强制类型转换操作符

1
static_cast、reinterpret_cast、const_cast、dynamic_cast

2.1 static_cast

这个关键字是用于相近类型之间的转换的,比如double和int,char和int之间

1
2
3
4
5
6
double d = 11.4;
int a1 = static_cast<int>(d);//相近类型的转换
char ch = 'a';
int a2 = static_cast<int>(ch);
cout << a1 << endl;
cout << a2 << endl;

image-20221020162052557

除了以上的类型转换,其还支持将内置类型转自定义类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct mytest
{
public:
// explicit mytest(int a = -1)
mytest(int a = -1)
{
_a = a;
cout << "mytest() " << a << endl;
}
mytest(const mytest &st)
{
_a = st._a;
cout << "mytest(copy) " << st._a << endl;
}
mytest &operator=(const mytest &st)
{
_a = st._a;
cout << "mytest operator= " << st._a << endl;
return *this;
}

private:
int _a;
};

使用如下代码进行测试,当mytest的构造函数没有加explicit关键字的时候,下方的两种构造方式都可以成功调用(隐式类型转换)

1
2
3
4
5
6
7
int main()
{
mytest t1 = static_cast<mytest>(3);
mytest t2 = 5;

return 0;
}
1
2
3
$ ./test
mytest() 3
mytest() 5

加了explicit之后,t2就没有办法成功构造了,因为隐式类型转换被禁用了

1
2
3
4
5
$ g++ test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:120:17: error: conversion from ‘int’ to non-scalar type ‘mytest’ requested
mytest t2 = 5;
^

static_cast在加了explicit之后,依旧可以帮我们调用构造函数来创建对象;

1
2
$ ./test
mytest() 3

2.2 reinterpret_cast

这个关键字用于不相近类型之间的转换,比如指针转成int

1
2
3
4
int* p = &a1;
//int x = static_cast<int>(p);//报错:类型转换无效
int x = reinterpret_cast<int>(p);//非相近类型中的转换
cout << x << endl;

打印的结果如下(每次运行都不一样)

1
2029408

2.3 const_cast

如同其名,这个关键字的作用是取消一个变量的const属性

1
2
3
4
5
const int c1 = 3;//这里定义的变量是在栈上的,可以间接修改
int* ptr1 = const_cast<int*>(&c1);//取消const权限
*ptr1 = 4;
cout << c1 << endl;//修改了地址之后没有变化
cout << *ptr1 << endl;

image-20221020162259828

欸,为什么我们取地址之后,修改为4了,变量c1本身不会变化呢?

这是因为编译器做了一些优化,把c1放到了某个地方,取的时候并没有直接去内存里面取

volatile关键字

这里我们可以使用volatile关键字修饰变量,要求每一次都必须要去内存中取

1
2
3
4
5
6
7
//volatile关键字,每次访问c都去内存中取,屏蔽编译器优化
volatile const int c2 = 3;
int* ptr2 = const_cast<int*>(&c2);//取消const权限
*ptr2 = 4;
cout << "volatitle: ";
cout << c2 << endl;
cout << *ptr2 << endl;

image-20221020162443380

注意事项

https://blog.csdn.net/qq_45853229/article/details/124669527

const_cast的目的并不是为了让你去修改一个本身被定义为const的值。

因为这样做的后果是无法预期的。const_cast的目的是修改一些指针/引用的权限,如果我们原本无法通过这些指针/引用修改某块内存的值,现在你可以了。

2.4 dynamic_cast

https://en.cppreference.com/w/cpp/language/dynamic_cast

该关键字是用于继承中,将一个父类的指针/引用转换为子类对象的指针/引用;注意是父类转子类!

之前学习继承的时候,我们了解过

  • 向上转型:父类的指针、引用可以直接指向子类对象的指针/引用(这是一个赋值兼容的规则,不需要进行转换,或者使用static_cast正常转换)
  • 向下转型:反过来之后,可以直接赋值吗?不够安全

dynamic_cast的作用就是判断一个父类指针指向的是不是他的子类,并进行转换。

  • 如果是,能够成功转换。
  • 如果不是,传入的是指针则返回nullptr,传入的是引用抛异常(std::bad_cast,因为没有空引用)

这个关键字最大的作用,便是可以帮我们判断这个父类指针/引用指向的是否为一个子类对象

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
class A
{
public:
virtual void f() {}
};
class B : public A
{};

void fun(A* pa)
{
// dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
B* pb1 = static_cast<B*>(pa);
B* pb2 = dynamic_cast<B*>(pa);
cout << "pb1:" << pb1 << endl;
if (pb2)
{
cout << "转换成功!pb2:" << pb2 << endl;
}
else
{
cout << "转换失败!pb2:" << pb2 << endl;
}

}

void test2()
{
A a;
B b;
fun(&a);
fun(&b);
}

image-20221020163245904

这里有个要求,那便是父类中必须要有虚函数(多态),否则无法成功转换。

image-20221020163349806

怎么实现的?

以下说明来自GPT:

  1. RTTI(Run-Time Type Identification)的支持:dynamic_cast 依赖于 RTTI 信息,它允许在运行时获取对象的实际类型信息。RTTI 是通过在编译阶段为类生成额外的元数据来实现的,这些元数据通常包括对象的类型信息以及与之相关的虚函数表。
  2. 类型检查:在执行 dynamic_cast 时,首先进行类型检查。如果转换目标是指针类型,而实际对象的类型不匹配或不兼容,则 dynamic_cast 返回一个空指针(对于指针类型)或抛出一个 std::bad_cast 异常(对于引用类型)。如果类型匹配,则 dynamic_cast 将继续执行。
  3. 类型转换:如果类型检查通过,dynamic_cast 将执行类型转换。对于指针类型,如果转换是合法的,它将返回指向所请求类型的指针;对于引用类型,它将返回对所请求类型的引用。
  4. 虚函数表的使用:在执行 dynamic_cast 时,通常使用虚函数表来确定对象的实际类型。当 dynamic_cast 需要检查或转换指向基类的指针或引用时,它会查看对象的虚函数表来获取实际类型信息。
  5. 多态性的支持:dynamic_cast 在处理多态性时特别有用。它可以在类层次结构中安全地转换指针或引用,并在必要时进行类型检查,确保转换是有效的。这对于处理基类指针或引用指向派生类对象的情况非常有用。

其中我能理解的是第四点虚函数表的使用。对于每一个类而言,虚函数表是只有一张的,所以编译器能够通过这个虚函数表来确认父类指针指向的子类对象是谁。

[C++] - dynamic_cast介绍及工作原理、typeid、type_info 博客中有更详细的说明,引用如下。

《深度探索C++对象模型》中有个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point
{
public:
Point(float xval);
virtual ~Point();

float x() const;
static int PointCount();

protected:
virtual ostream& print(ostream& os) const;

float _x;
static int _point_count;
};

这个类的内存布局如下图,type_info是C++ Standard所定义的类型描述器,type_info中放置着每个类的类型信息。virtual table的第一个slot存有type_info的地址。

image.png

《The C++ Programming Language》中有个例子:

1
2
3
4
// polymor phic base (Ival_slider has virtual functions)
class My_slider: public Ival_slider {
// ...
};

这个类会有一个自己的vptr虚表指针,虚表指针指向的虚表中包含一个type_info标记自己类的信息。

image.png

当使用dynamic_cast的时候,编译器会先构造一个零时的My_slider对象,从它的虚表vptr指向的虚表中找到My_slider类的type_info,再和提供的指针pb所对应的type_info进行对比,如果一致则返回转换后My_slider类的指针,不一致则返回nullptr;

1
2
3
4
void g(Ival_box* pb, Date* pd)
{
My_slider* pd1 = dynamic_cast<My_slider*>(pb); // OK
}

简而言之,在继承多态模型中,每一个子类都会有一个虚函数表,这个虚函数表中就包含了类型的type_info。使用dynamic_cast进行类型转换的时候,就是通过判断type_info和模板传入的类的type_info是否一致来确定能否成功转换的。

3.C++强制类型转换的作用

C++希望我们规范强制类型转换的情景,针对性的调用不同的关键字

但是由于它没有强制,在实际情况中用的反而不多

不过需要注意的是,强制类型转换会关闭/挂起正常的类型检查,在强制类型转换之前,我们要仔细检查是否还有别的方法来达到目的。最好是避免使用强制类型转换!

4.RTTI

之前学习智能指针的时候,我们学过一种思路叫RAII

这里的RTTI全称为Run-time Type identification,即运行时类型识别

C++通过下面几种方式来支持RTTI:

  • typeid
  • decltype
  • dyanmic_cast

这个概念只需要了解即可!