浅谈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是直接重新解释位模式的,极其不安全,能不用就不用。

posted @ 2021-06-04 10:35  一瞬光阴  阅读(423)  评论(0编辑  收藏  举报