8

聊聊 C++ 中的四种类型转换符

 2 years ago
source link: https://www.cnblogs.com/huangxincheng/p/16480503.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在玩 C 的时候,经常会用 void* 来指向一段内存地址开端,然后再将其强转成尺度更小的 char*int* 来丈量一段内存,参考如下代码:


int main()
{
	void* ptr = malloc(sizeof(int) * 10);

	int* int_ptr = (int*)ptr;
	char* char_ptr = (char*)ptr;
}

由于 C 的自由度比较大,想怎么玩就怎么玩,带来的弊端就是容易隐藏着一些不易发现的bug,归根到底还是程序员的功底不扎实,C++ 设计者觉得不能把程序员想的太厉害,应该要力所能及的帮助程序员避掉一些不必要的潜在 bug,并且还要尽最大努力的避免对性能有过多的伤害,所以就出现了 4 个强制类型转换运算符。

  1. const_cast
  2. reinterpret_cast
  3. dynamic_cast
  4. static_cast

既然 C++ 做了归类,必然就有其各自用途,接下来我们逐一和大家聊一下。

二:理解四大运算符

1. const_cast

这是四个运算符中最好理解的,玩过 C++ 的都知道,默认情况下是不能修改一个 const 变量,比如下面这样:


int main()
{
	const int i = 10;
	i = 12;
}

这段代码肯定是要报错的,那如果我一定要实现这个功能,如何做呢?这就需要用到 const_cast 去掉它的常量符号,然后对 i 进行操作即可,所以修改代码如下:


int main()
{
	const int i = 10;
	auto j = const_cast<int*>(&i);
	*(j) = 12;
}

d465671f00aa4f70b934eabac3358b26~tplv-k3u1fbpfcp-zoom-1.image

2. reinterpret_cast

从名字上看就是一个 重新解释转换,很显然这个非常底层,如果大家玩过 windbg ,应该知道用 dt 命令可以将指定的内存地址按照某一个结构体丈量出来,比如说 C# 的 CLR 在触发 GC 时,会有 gc_mechanisms 结构,参考代码如下:


0:000> dt WKS::gc_mechanisms 0x7ffb6ba96e60
coreclr!WKS::gc_mechanisms
   +0x000 gc_index         : 1
   +0x008 condemned_generation : 0n0
   +0x00c promotion        : 0n0
   +0x010 compaction       : 0n1
   +0x014 loh_compaction   : 0n0
   +0x018 heap_expansion   : 0n0
   +0x01c concurrent       : 0
   +0x020 demotion         : 0n0
   +0x024 card_bundles     : 0n1
   +0x028 gen0_reduction_count : 0n0
   +0x02c should_lock_elevation : 0n0
   +0x030 elevation_locked_count : 0n0
   +0x034 elevation_reduced : 0n0
   +0x038 minimal_gc       : 0n0
   +0x03c reason           : 0 ( reason_alloc_soh )
   +0x040 pause_mode       : 1 ( pause_interactive )
   +0x044 found_finalizers : 0n0
   +0x048 background_p     : 0n0
   +0x04c b_state          : 0 ( bgc_not_in_process )
   +0x050 allocations_allowed : 0n1
   +0x054 stress_induced   : 0n0
   +0x058 entry_memory_load : 0
   +0x05c exit_memory_load : 0

其实 reinterpret_cast 大概也是干这个事的,参考代码如下:


typedef struct _Point {
	int x;
	int y;
} Point;

int main()
{
	Point point = { 10,11 };

	//内存地址
	void* ptr = &point;

	//根据内存地址 丈量出  Point
	Point* ptr_point = reinterpret_cast<Point*>(ptr);

	printf("x=%d", ptr_point->x);
}

从代码看,我直接根据 ptr 地址丈量出了 Point 结构,说实话这个和 C 玩法就比较类似了。

3. dynamic_cast

在多态场景下,有时候会遇到这样的一个问题,一个父类有多个子类,我现在手拥一个父类,我不知道能不能将它转换为其中一个子类,要试探一下看看,那怎么去试探呢? 类似 C# 中的 as 运算符,在 C++ 中就需要用 dynamic_cast 来做这件事情,参考如下:


//点
class Point {
public:
	Point(int x, int y) :x(x), y(y) {}
	virtual void show() {}
public:
	int x;
	int y;
};

//矩形
class Rectangle :public Point {
public:
	Rectangle(int x, int y, int w, int h) : Point(x, y), w(w), h(h) {}
public:
	int w;
	int h;
};

//三角形
class Triangle :public Point {
public:
	Triangle(int x, int y, int z) :Point(x, y), z(z) {}
public:
	int z;
};

int main()
{
	Point* p1 = new Rectangle(10, 20, 100, 200);
	Point* p2 = new Triangle(4, 5, 6);

	//将  p1 转成 子类 Triangle 会报错的
	Triangle* t1 = dynamic_cast<Triangle*>(p1);

	if (t1 == nullptr) {
		printf("p1 不能转成 Triangle");
	}
}

c05e39d870f147ffac64548e6f851ce9~tplv-k3u1fbpfcp-zoom-1.image

对,场景就是这个,p1 其实是 Rectangle 转上去的, 这时候你肯定是不能将它向下转成 Triangle , 问题就在这里,很多时候你并不知道此时的 p1 是哪一个子类。

接下来的一个问题是,C++ 并不像C# 有元数据,那它是如何鉴别呢? 其实这用了 RTTI 技术,哪里能看出来呢?哈哈,看汇编啦。


	Triangle* t1 = dynamic_cast<Triangle*>(p1);
00831D57  push        0  
00831D59  push        offset Triangle `RTTI Type Descriptor' (083C150h)  
00831D5E  push        offset Point `RTTI Type Descriptor' (083C138h)  
00831D63  push        0  
00831D65  mov         eax,dword ptr [p1]  
00831D68  push        eax  
00831D69  call        ___RTDynamicCast (083104Bh)  
00831D6E  add         esp,14h  
00831D71  mov         dword ptr [t1],eax  

从汇编可以看到编译器这是带夹私货了,在底层偷偷的调用了一个 ___RTDynamicCast 函数在运行时帮忙检测的,根据 cdcel 调用协定,参数是从右到左,恢复成代码大概是这样。


___RTDynamicCast(&p1, 0, &Point, &Triangle,0)

3. static_cast

从名字上就能看出,这个强转具有 static 语义,也就是 编译阶段 就生成好了,具体安全不安全,它就不管了,就拿上面的例子,将 dynamic_cast 改成 static_cast 看看有什么微妙的变化。


int main()
{
	Point* p1 = new Rectangle(10, 20, 100, 200);
	Point* p2 = new Triangle(4, 5, 6);

	Triangle* t1 = static_cast<Triangle*>(p1);

	printf("x=%d, y=%d,z=%d", t1->x, t1->y, t1->z);
}

61903fb1fc584505927e4a22b981b430~tplv-k3u1fbpfcp-zoom-1.image

我们发现居然转成功了,而且 Triangle 的值也是莫名奇怪,直接取了 Rectangle 的前三个值,如果这是生产代码,肯定要挨批了。。。

接下来简单看下汇编代码:


	Triangle* t1 = static_cast<Triangle*>(p1);
00DF5B17  mov         eax,dword ptr [p1]  
00DF5B1A  mov         dword ptr [t1],eax  

	printf("x=%d, y=%d,z=%d", t1->x, t1->y, t1->z);
00DF5B1D  mov         eax,dword ptr [t1]  
00DF5B20  mov         ecx,dword ptr [eax+0Ch]  
00DF5B23  push        ecx  
00DF5B24  mov         edx,dword ptr [t1]  
00DF5B27  mov         eax,dword ptr [edx+8]  
00DF5B2A  push        eax  
00DF5B2B  mov         ecx,dword ptr [t1]  
00DF5B2E  mov         edx,dword ptr [ecx+4]  
00DF5B31  push        edx  
00DF5B32  push        offset string "x=%d, y=%d,z=%d" (0DF8C80h)  
00DF5B37  call        _printf (0DF145Bh)  
00DF5B3C  add         esp,10h

从代码中看,它其实就是将 p1 的首地址给了 t1,然后依次把copy偏移值 +4,+8,+0C, 除了转换这个,还可以做一些 int ,long ,double 之间的强转,当然也是一样,编译时汇编代码就已经生成好了。

好了,本篇就说这么多,希望对你有帮助。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK