一:背景
在玩 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 个强制类型转换运算符。
- const_cast
- reinterpret_cast
- dynamic_cast
- 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(&i); *(j) = 12; }
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(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(p1); if (t1 == nullptr) { printf("p1 不能转成 Triangle"); } }
对,场景就是这个,p1 其实是 Rectangle
转上去的, 这时候你肯定是不能将它向下转成 Triangle
, 问题就在这里,很多时候你并不知道此时的 p1 是哪一个子类。
接下来的一个问题是,C++ 并不像C# 有元数据,那它是如何鉴别呢? 其实这用了 RTTI 技术,哪里能看出来呢?哈哈,看汇编啦。
Triangle* t1 = dynamic_cast(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(p1); printf("x=%d, y=%d,z=%d", t1->x, t1->y, t1->z); }
我们发现居然转成功了,而且 Triangle
的值也是莫名奇怪,直接取了 Rectangle
的前三个值,如果这是生产代码,肯定要挨批了。。。
接下来简单看下汇编代码:
Triangle* t1 = static_cast(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 之间的强转,当然也是一样,编译时汇编代码就已经生成好了。
到此这篇关于一起聊聊C++中的四种类型转换符 的文章就介绍到这了,更多相关C++类型转换符 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!