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

打印 上一主题 下一主题

主题 923|帖子 923|积分 2769

一:背景

在玩 C 的时候,经常会用 void* 来指向一段内存地址开端,然后再将其强转成尺度更小的 char* 或 int* 来丈量一段内存,参考如下代码:
  1. int main()
  2. {
  3.         void* ptr = malloc(sizeof(int) * 10);
  4.         int* int_ptr = (int*)ptr;
  5.         char* char_ptr = (char*)ptr;
  6. }
复制代码
由于 C 的自由度比较大,想怎么玩就怎么玩,带来的弊端就是容易隐藏着一些不易发现的bug,归根到底还是程序员的功底不扎实,C++ 设计者觉得不能把程序员想的太厉害,应该要力所能及的帮助程序员避掉一些不必要的潜在 bug,并且还要尽最大努力的避免对性能有过多的伤害,所以就出现了 4 个强制类型转换运算符。

  • const_cast
  • reinterpret_cast
  • dynamic_cast
  • static_cast
既然 C++ 做了归类,必然就有其各自用途,接下来我们逐一和大家聊一下。
二:理解四大运算符

1. const_cast

这是四个运算符中最好理解的,玩过 C++ 的都知道,默认情况下是不能修改一个 const 变量,比如下面这样:
  1. int main()
  2. {
  3.         const int i = 10;
  4.         i = 12;
  5. }
复制代码
这段代码肯定是要报错的,那如果我一定要实现这个功能,如何做呢?这就需要用到 const_cast 去掉它的常量符号,然后对 i 进行操作即可,所以修改代码如下:
  1. int main()
  2. {
  3.         const int i = 10;
  4.         auto j = const_cast<int*>(&i);
  5.         *(j) = 12;
  6. }
复制代码

2. reinterpret_cast

从名字上看就是一个 重新解释转换,很显然这个非常底层,如果大家玩过 windbg ,应该知道用 dt 命令可以将指定的内存地址按照某一个结构体丈量出来,比如说 C# 的 CLR 在触发 GC 时,会有 gc_mechanisms 结构,参考代码如下:
  1. 0:000> dt WKS::gc_mechanisms 0x7ffb6ba96e60
  2. coreclr!WKS::gc_mechanisms
  3.    +0x000 gc_index         : 1
  4.    +0x008 condemned_generation : 0n0
  5.    +0x00c promotion        : 0n0
  6.    +0x010 compaction       : 0n1
  7.    +0x014 loh_compaction   : 0n0
  8.    +0x018 heap_expansion   : 0n0
  9.    +0x01c concurrent       : 0
  10.    +0x020 demotion         : 0n0
  11.    +0x024 card_bundles     : 0n1
  12.    +0x028 gen0_reduction_count : 0n0
  13.    +0x02c should_lock_elevation : 0n0
  14.    +0x030 elevation_locked_count : 0n0
  15.    +0x034 elevation_reduced : 0n0
  16.    +0x038 minimal_gc       : 0n0
  17.    +0x03c reason           : 0 ( reason_alloc_soh )
  18.    +0x040 pause_mode       : 1 ( pause_interactive )
  19.    +0x044 found_finalizers : 0n0
  20.    +0x048 background_p     : 0n0
  21.    +0x04c b_state          : 0 ( bgc_not_in_process )
  22.    +0x050 allocations_allowed : 0n1
  23.    +0x054 stress_induced   : 0n0
  24.    +0x058 entry_memory_load : 0
  25.    +0x05c exit_memory_load : 0
复制代码
其实 reinterpret_cast 大概也是干这个事的,参考代码如下:
  1. typedef struct _Point {
  2.         int x;
  3.         int y;
  4. } Point;
  5. int main()
  6. {
  7.         Point point = { 10,11 };
  8.         //内存地址
  9.         void* ptr = &point;
  10.         //根据内存地址 丈量出  Point
  11.         Point* ptr_point = reinterpret_cast<Point*>(ptr);
  12.         printf("x=%d", ptr_point->x);
  13. }
复制代码
从代码看,我直接根据 ptr 地址丈量出了 Point 结构,说实话这个和 C 玩法就比较类似了。
3. dynamic_cast

在多态场景下,有时候会遇到这样的一个问题,一个父类有多个子类,我现在手拥一个父类,我不知道能不能将它转换为其中一个子类,要试探一下看看,那怎么去试探呢? 类似 C# 中的 as 运算符,在 C++ 中就需要用 dynamic_cast  来做这件事情,参考如下:
  1. //点
  2. class Point {
  3. public:
  4.         Point(int x, int y) :x(x), y(y) {}
  5.         virtual void show() {}
  6. public:
  7.         int x;
  8.         int y;
  9. };
  10. //矩形
  11. class Rectangle :public Point {
  12. public:
  13.         Rectangle(int x, int y, int w, int h) : Point(x, y), w(w), h(h) {}
  14. public:
  15.         int w;
  16.         int h;
  17. };
  18. //三角形
  19. class Triangle :public Point {
  20. public:
  21.         Triangle(int x, int y, int z) :Point(x, y), z(z) {}
  22. public:
  23.         int z;
  24. };
  25. int main()
  26. {
  27.         Point* p1 = new Rectangle(10, 20, 100, 200);
  28.         Point* p2 = new Triangle(4, 5, 6);
  29.         //将  p1 转成 子类 Triangle 会报错的
  30.         Triangle* t1 = dynamic_cast<Triangle*>(p1);
  31.         if (t1 == nullptr) {
  32.                 printf("p1 不能转成 Triangle");
  33.         }
  34. }
复制代码

对,场景就是这个,p1 其实是 Rectangle 转上去的, 这时候你肯定是不能将它向下转成 Triangle , 问题就在这里,很多时候你并不知道此时的 p1 是哪一个子类。
接下来的一个问题是,C++ 并不像C# 有元数据,那它是如何鉴别呢? 其实这用了 RTTI 技术,哪里能看出来呢?哈哈,看汇编啦。
  1.         Triangle* t1 = dynamic_cast<Triangle*>(p1);
  2. 00831D57  push        0  
  3. 00831D59  push        offset Triangle `RTTI Type Descriptor' (083C150h)  
  4. 00831D5E  push        offset Point `RTTI Type Descriptor' (083C138h)  
  5. 00831D63  push        0  
  6. 00831D65  mov         eax,dword ptr [p1]  
  7. 00831D68  push        eax  
  8. 00831D69  call        ___RTDynamicCast (083104Bh)  
  9. 00831D6E  add         esp,14h  
  10. 00831D71  mov         dword ptr [t1],eax  
复制代码
从汇编可以看到编译器这是带夹私货了,在底层偷偷的调用了一个 ___RTDynamicCast 函数在运行时帮忙检测的,根据 cdcel 调用协定,参数是从右到左,恢复成代码大概是这样。
  1. ___RTDynamicCast(&p1, 0, &Point, &Triangle,0)
复制代码
3. static_cast

从名字上就能看出,这个强转具有 static 语义,也就是 编译阶段 就生成好了,具体安全不安全,它就不管了,就拿上面的例子,将 dynamic_cast 改成 static_cast 看看有什么微妙的变化。
  1. int main()
  2. {
  3.         Point* p1 = new Rectangle(10, 20, 100, 200);
  4.         Point* p2 = new Triangle(4, 5, 6);
  5.         Triangle* t1 = static_cast<Triangle*>(p1);
  6.         printf("x=%d, y=%d,z=%d", t1->x, t1->y, t1->z);
  7. }
复制代码

我们发现居然转成功了,而且 Triangle 的值也是莫名奇怪,直接取了 Rectangle 的前三个值,如果这是生产代码,肯定要挨批了。。。
接下来简单看下汇编代码:
  1.         Triangle* t1 = static_cast<Triangle*>(p1);
  2. 00DF5B17  mov         eax,dword ptr [p1]  
  3. 00DF5B1A  mov         dword ptr [t1],eax  
  4.         printf("x=%d, y=%d,z=%d", t1->x, t1->y, t1->z);
  5. 00DF5B1D  mov         eax,dword ptr [t1]  
  6. 00DF5B20  mov         ecx,dword ptr [eax+0Ch]  
  7. 00DF5B23  push        ecx  
  8. 00DF5B24  mov         edx,dword ptr [t1]  
  9. 00DF5B27  mov         eax,dword ptr [edx+8]  
  10. 00DF5B2A  push        eax  
  11. 00DF5B2B  mov         ecx,dword ptr [t1]  
  12. 00DF5B2E  mov         edx,dword ptr [ecx+4]  
  13. 00DF5B31  push        edx  
  14. 00DF5B32  push        offset string "x=%d, y=%d,z=%d" (0DF8C80h)  
  15. 00DF5B37  call        _printf (0DF145Bh)  
  16. 00DF5B3C  add         esp,10h
复制代码
从代码中看,它其实就是将 p1 的首地址给了 t1,然后依次把copy偏移值 +4,+8,+0C, 除了转换这个,还可以做一些 int ,long ,double 之间的强转,当然也是一样,编译时汇编代码就已经生成好了。
好了,本篇就说这么多,希望对你有帮助。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

自由的羽毛

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表