浅谈c++类型转换const_cast、reinterpret_cast
c++类型有四种类型转换,分别是static_cast、const_cast、reinterpret_cast以及dynamic_cast,下面浅谈下个人理解。
1.const_cast
首先看一个错误的用法:
1 const int i = 10; 2 int *p = const_cast<int*>(&i); 3 *p = 20; //行为未定义 4 std::cout << i << std::endl;
上面这段代码输出如下:
1 10
i的结果是10,并不是想想中20,原因是c++标准规定使用const_cast把一个原本是const的变量转换为非const,其行为是未定义的。
从汇编的角度看这段代码:
1 main: 2 .LFB1493: 3 .cfi_startproc 4 pushq %rbp 5 .cfi_def_cfa_offset 16 6 .cfi_offset 6, -16 7 movq %rsp, %rbp 8 .cfi_def_cfa_register 6 9 subq $32, %rsp 10 movq %fs:40, %rax 11 movq %rax, -8(%rbp) 12 xorl %eax, %eax 13 movl $10, -20(%rbp) 14 leaq -20(%rbp), %rax 15 movq %rax, -16(%rbp) 16 movq -16(%rbp), %rax 17 movl $20, (%rax) 18 movl $10, %esi //第一个参数 19 leaq _ZSt4cout(%rip), %rdi // std::cout地址存入寄存器 %rdi,即第二个参数 20 call _ZNSolsEi@PLT //调用函数 std::basic_ostream<char, std::char_traits<char> >::operator<<(int),通过c++filt _ZNSolsEi得知。
通过18、19行,可以看出,编译器直接将立即数10作为operator<<(int)的参数,与*p没有任何关系。
故,const_cast用法不是将一个const变量变成非const变量的。
看个正确的用法:
1 int i = 10; 2 const int& rci = i; //或者const指针 3 int& j = const_cast<int&>(rci); 4 j = 20; 5 std::cout << i << std::endl;
其输出结果如下:
1 20
变量i原本是非const的,但由于某种特殊原因无意间变成了const,如行2,使用了一个const引用指向了本来不是const的对象。但是后来发现需要修改,这个时候const_cast就发挥作用了。
从汇编的角度看这段代码逻辑:
1 main: 2 .LFB1493: 3 .cfi_startproc 4 pushq %rbp 5 .cfi_def_cfa_offset 16 6 .cfi_offset 6, -16 7 movq %rsp, %rbp 8 .cfi_def_cfa_register 6 9 subq $32, %rsp 10 movq %fs:40, %rax 11 movq %rax, -8(%rbp) 12 xorl %eax, %eax 13 movl $10, -28(%rbp) 14 leaq -28(%rbp), %rax 15 movq %rax, -24(%rbp) 16 movq -24(%rbp), %rax 17 movq %rax, -16(%rbp) 18 movq -16(%rbp), %rax 19 movl $20, (%rax) 20 movl -28(%rbp), %eax 21 movl %eax, %esi 22 leaq _ZSt4cout(%rip), %rdi 23 call _ZNSolsEi@PLT
栈帧如下:
图1对应汇编指令:
pushq %rbp //保存previous frame .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp // .cfi_def_cfa_register 6 subq $32, %rsp //栈空间增加32个字节 movq %fs:40, %rax movq %rax, -8(%rbp) //canary栈包含,防溢出,这里忽略 xorl %eax, %eax movl $10, -28(%rbp) //将立即数10压入栈中,即局部变量 i = 10
逻辑是,保存之前的栈帧,栈空间增加32字节,将立即数10压入栈中,即局部变量 i = 10。
图片2对应剩余的汇编指令,如下:
//const int& rci = i;
leaq -28(%rbp), %rax movq %rax, -24(%rbp)
创建i的const引用rci,将i的地址(0x7fffffffe394)压入栈中,位于-24(%rbp)。
// int& j = const_cast<int&>(rci); // j = 20 movq -24(%rbp), %rax // rax值为i的地址,即0x7fffffffe394 movq %rax, -16(%rbp) // -16(%rbp)存放的i的地址 movq -16(%rbp), %rax // 将i的地址传入rax movl $20, (%rax) // 将20写入i
通过const_cast创建引用j,并对j重新赋值,因为操作的始终是i的地址,所以最终改变了i的值。
总结:const修饰的对象本就不该被改变,而且const修饰的对象还会参与编译期优化,如第一个例子,编译器直接将立即数10送到寄存器esi中,并没有使用栈空间。
只是有时候,非const对象意外被修饰成了const对象,此时才是const_cast发挥作用的时候。
通过const_cast修改const对象的行为是未定义的,完全看编译器心情。
2.reinterpret_cast
通过重新解释底层位模式在类型间转换,纯粹是编译期指令,个人认为能不能绝对不要用。
看一段代码:
double pi = 3.14; int *p = reinterpret_cast<int*>(&pi); std::cout << *p << std::endl;
其输出结果为:
1374389535
reinterpret_cast将双进度浮点数指针重新解释为int指针,x86_64架构上,double占用8个字节,int占用4个字节,其行为就是将pi的低4字节解释为一个int,结果为1374389535。
从汇编的角度看这段代码:
1 pushq %rbp 2 .cfi_def_cfa_offset 16 3 .cfi_offset 6, -16 4 movq %rsp, %rbp 5 .cfi_def_cfa_register 6 6 subq $32, %rsp 7 movq %fs:40, %rax 8 movq %rax, -8(%rbp) 9 xorl %eax, %eax 10 movsd .LC0(%rip), %xmm0 //将8字节只读数据送到%xmm0低64位上 11 movsd %xmm0, -24(%rbp) //将xmm0低64位压入栈中 ,位置为 -24(%rbp) 12 leaq -24(%rbp), %rax //将-24(%rbp)地址送到 rax 13 movq %rax, -16(%rbp) //将-24(%rbp)地址压入栈中,8字节,位置为 -16(%rbp) 14 movq -16(%rbp), %rax //将-24(%rbp)地址送入rax 15 movl (%rax), %eax //解释-24(%rbp)地址前4字节为int 16 movl %eax, %esi 17 leaq _ZSt4cout(%rip), %rdi 18 call _ZNSolsEi@PLT
其中.LC0定义如下:
.section .rodata .align 8 .LC0: .long 1374389535 .long 1074339512
栈帧如下所示:
pi是由2个int位模式构成,低32位是1374389535、高32位是1074339512,对p指针解引用得出的是1374389535,即低32位二进制解释。
同理,如果执行以下语句:
dobule pii = *reinterpret_cast<double*>(p); std::cout << pii << std::endl;
输出结果为:
3.14
总结:通过这个例子可以看出reinterpret_cast是直接重新解释位模式的,极其不安全,能不用就不用。