我们上一篇开头列举了好几个例子,其中都没有调用拷贝构造函数,这一篇我们还是以那几个例子,并且这次我们定义了拷贝构造函数,来分析c++编译器是怎么理解我们写的代码,也理解编译器是怎么转化我们的代码的。

5.1 明确的初始化操作

侯捷老师翻译过来的,就抄过来了,按我们大白话就是直接赋值,下面还是上代码吧。

5.1.1 例子

#include <iostream>using namespace std;class A
{
public:A() {cout << "构造函数" << endl;}A(const A& a){  cout << "拷贝构造函数" << endl;}
};int main(int argc, char **argv)
{A a0;           // 这个会调用一个构造函数A a1(a0);       // 定义a1,会调用拷贝构造函数A a2 = a0;      // 定义a2,会调用拷贝构造函数A a3 = A(a0);   // 定义a3,会调用拷贝构造函数return 0;
}

编译运行:

root@ubuntu:~/c++_mode/05# ./5_1
构造函数
拷贝构造函数
拷贝构造函数
拷贝构造函数
root@ubuntu:~/c++_mode/05#

这次调用的现象就是老师说的了,3种初始化方式,都调用拷贝构造函数。但是我们这一篇不会停留在这么简单的层次,所以我们反汇编查看一下,编译器是怎么理解我们写的代码的。

5.1.2 反汇编代码

我们就直接看反汇编代码:

main:
.LFB1027:.cfi_startproc     # .cfi_startproc被用在每个函数的开头,这些函数应该在.eh_frame中有一个条目pushq   %rbp        # 保存父函数的栈帧.cfi_def_cfa_offset 16    # .cfi_def_cfa_offset修改一个计算CFA的规则。寄存器保持不变,但偏移量是新的。注意,绝对偏移量将被添加到一个已定义的寄存器中来计算CFA地址。.cfi_offset 6, -16      # 寄存器先前的值保存在从CFA的offset offset处。movq    %rsp, %rbp      # 把父函数的栈顶指针赋值给当前函数栈底指针.cfi_def_cfa_register 6subq   $32, %rsp       #  预留0x20字节的空间movl  %edi, -20(%rbp) #  edi进栈movq    %rsi, -32(%rbp) # rsi进栈movq %fs:40, %rax    movq    %rax, -8(%rbp)xorl  %eax, %eaxleaq  -12(%rbp), %rax # 这个是取地址,a0的地址movq   %rax, %rdicall  _ZN1AC1Ev   # 调用类A的构造函数leaq -12(%rbp), %rdxleaq -11(%rbp), %raxmovq %rdx, %rsimovq  %rax, %rdicall  _ZN1AC1ERKS_leaq    -12(%rbp), %rdxleaq -10(%rbp), %raxmovq %rdx, %rsimovq  %rax, %rdicall  _ZN1AC1ERKS_leaq    -12(%rbp), %rdxleaq -9(%rbp), %raxmovq  %rdx, %rsimovq  %rax, %rdicall  _ZN1AC1ERKS_movl    $0, %eaxmovq    -8(%rbp), %rcxxorq  %fs:40, %rcxje  .L5call __stack_chk_fail

刚刚学习了一波汇编,发现编译完成之后,栈的大小确实是固定下来了,真的很神奇啊,之前一直以为是动态进栈之类,不过也确实是动态进栈,但是栈的大小是已经申请好了。

我们就提取拷贝构造函数这部分来分析:

 leaq    -12(%rbp), %rdx     # a0的地址leaq -11(%rbp), %rax     # a1的地址movq %rdx, %rsi          # 把a0的地址做为函数参数1movq %rax, %rdi          # 把a1的地址做为函数参数2call _ZN1AC1ERKS_

汇编函数参数传递是使用了这6个寄存器:位置也是对应的:

%rdi,%rsi,%rdx,%rcx,%r8,%r9。

所以上面我们看的就是在准备_ZN1AC1ERKS_的参数。

那接下来就有人问了,怎么知道哪几句汇编代码是取a0,a1的地址,下面就来介绍一下怎么看。

5.1.3 gdb反汇编的使用

很简单,我是使用gdb来分析的。

这里隆重的介绍一位很牛逼的命令:disassemble。

反汇编命令,=》指向的位置就是当前执行的命令语句:

(gdb) disassemble
Dump of assembler code for function main(int, char**):0x00000000004008b6 <+0>:   push   %rbp0x00000000004008b7 <+1>:  mov    %rsp,%rbp0x00000000004008ba <+4>: sub    $0x20,%rsp0x00000000004008be <+8>:    mov    %edi,-0x14(%rbp)0x00000000004008c1 <+11>: mov    %rsi,-0x20(%rbp)
=> 0x00000000004008c5 <+15>: mov    %fs:0x28,%rax    # 当前pc指向的地址0x00000000004008ce <+24>: mov    %rax,-0x8(%rbp)0x00000000004008d2 <+28>:  xor    %eax,%eax0x00000000004008d4 <+30>:    lea    -0xc(%rbp),%rax0x00000000004008d8 <+34>:  mov    %rax,%rdi0x00000000004008db <+37>:    callq  0x400988 <A::A()>0x00000000004008e0 <+42>:  lea    -0xc(%rbp),%rdx0x00000000004008e4 <+46>:  lea    -0xb(%rbp),%rax0x00000000004008e8 <+50>:  mov    %rdx,%rsi0x00000000004008eb <+53>:    mov    %rax,%rdi0x00000000004008ee <+56>:    callq  0x4009b4 <A::A(A const&)>0x00000000004008f3 <+61>:  lea    -0xc(%rbp),%rdx0x00000000004008f7 <+65>:  lea    -0xa(%rbp),%rax0x00000000004008fb <+69>:  mov    %rdx,%rsi0x00000000004008fe <+72>:    mov    %rax,%rdi
--Type <RET> for more, q to quit, c to continue without paging--q

我们要分析一下调用构造函数之前,寄存器的里的值,这样看到寄存器的值,再分析汇编代码,相互认证就不会错太多。

我们看到的调用构造函数的地址为0x00000000004008d8,gdb也提供了直接断点在固定地址上,命令如下:(需要在地址前面加个*)

(gdb) b *0x00000000004008d8
Breakpoint 3 at 0x4008d8: file 5_1.cpp, line 22.

点已经断到了,接下来就需要看寄存器的值了,怎么看,还是gdb命令:

gdb) info r
rax            0x7fffffffe524      140737488348452
rbx            0x0                 0
rcx            0xc0                192
rdx            0x7fffffffe628      140737488348712
rsi            0x7fffffffe618      140737488348696
rdi            0x1                 1
rbp            0x7fffffffe530      0x7fffffffe530
rsp            0x7fffffffe510      0x7fffffffe510
r8             0x7ffff7dd4ac0      140737351862976
r9             0x7ffff7dc9780      140737351817088
r10            0x32f               815
r11            0x7ffff76c5290      140737344459408
r12            0x4007c0            4196288
r13            0x7fffffffe610      140737488348688
r14            0x0                 0
r15            0x0                 0
rip            0x4008d8            0x4008d8 <main(int, char**)+34>
eflags         0x246               [ PF ZF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
--Type <RET> for more, q to quit, c to continue without paging--
es             0x0                 0
fs             0x0                 0
gs             0x0                 0

这个是常见寄存器的值查看,只要是看rax的值,因为调用构造函数之前是rax赋值给rdi,rax的值是0x7fffffffe524。这个一看就有点懵逼,有点想栈的地址,所以就用gdb打印一下a0的值,已打印就吓了一跳:

(gdb) p &a0
$6 = (A *) 0x7fffffffe524
(gdb) p &a1
$7 = (A *) 0x7fffffffe525
(gdb) p &a2
$8 = (A *) 0x7fffffffe526

就刚好就是a0的地址,因为我们这是一个空类,所以类对象大小在栈中占了一个字节,这也响应了我们第一篇文章写的。

5.1.4 构造函数反汇编代码查看

_ZN1AC2ERKS_:
.LFB1025:.cfi_startprocpushq    %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6movq   %rdi, -8(%rbp)      # 保存rdimovq %rsi, -16(%rbp)     # 保存rsi的值noppopq    %rbp.cfi_def_cfa 7, 8ret.cfi_endproc

我们这个拷贝构造函数是空的,所以并没有做什么操作,就这样就返回了。

5.1.5 总结

总结一波,直接我们直接赋值的操作是编译器提供给我们的语法糖,事实上编译器最后还是需要转换成这样子:

A a1;
_ZN1AC1ERKS_(&a0, &a1);

这个风格是c风格,c会把类的函数全部通过名字修饰,成这种独一无二的格式。

如果是c++的方式,可以是这样写:

// c++的方式
A a1;
a1.A::A(a0);

好像编译还是不通过,由a1的对象调用拷贝构造函数来初始化。当然最后还是会转化成c语言那种方式。

5.2 参数的初始化

第二种是参数的初始化,说是参数的初始化,还不如说对象作为函数的参数,我们来看看。

5.2.1 例子

#include <iostream>using namespace std;class A
{
public:A() {cout << "构造函数" << endl;}A(const A& a){  cout << "拷贝构造函数" << endl;}~A(){cout << "析构函数" << endl;}};int foo(A a)
{return 0;
}int main(int argc, char **argv)
{A a0;           // 这个会调用一个构造函数foo(a0);return 0;
}

编译运行:

root@ubuntu:~/c++_mode/05# ./5_2
构造函数
拷贝构造函数
析构函数
析构函数
root@ubuntu:~/c++_mode/05#

这个也符号我们的想法,对象做为函数参数的时候,会调用一次拷贝构造函数,函数退出之后,这个变量的作用域已经失效了,就会调用析构函数。

main:
.LFB1031:.cfi_startprocpushq    %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6pushq  %rbxsubq    $40, %rsp.cfi_offset 3, -24movl %edi, -36(%rbp)movq %rsi, -48(%rbp)movq %fs:40, %raxmovq    %rax, -24(%rbp)xorl %eax, %eaxleaq  -26(%rbp), %raxmovq %rax, %rdicall  _ZN1AC1Ev// 这里开始leaq    -26(%rbp), %rdxleaq -25(%rbp), %raxmovq %rdx, %rsimovq  %rax, %rdicall  _ZN1AC1ERKS_leaq    -25(%rbp), %raxmovq %rax, %rdicall  _Z3foo1Aleaq    -25(%rbp), %raxmovq %rax, %rdicall  _ZN1AD1Ev// 这里结束movl    $0, %ebxleaq    -26(%rbp), %raxmovq %rax, %rdicall  _ZN1AD1Evmovl   %ebx, %eaxmovq  -24(%rbp), %rcxxorq %fs:40, %rcxje  .L8call __stack_chk_fail

看着这个反汇编代码就很熟悉,在5.1中已经详细分析了方法,这里就不详细分析了。

5.2.2 分析

通过上面的反汇编代码,明显看到了先申请了一个临时对象,然后调用拷贝构造函数,最后把这个临时对象的引用传给foo函数,等到函数退出之后,然后再析构这个临时对象。

转化成c++的方式:

A temp;   // 编译器产生一个临时对象
temp.A::A(a0);   //编译器对拷贝构造函数调用
foo(&temp);     // foo函数调用
temp.A::~A();   //  函数返回了,析构临时对象

如果转化成c语言:

A temp;   // 编译器产生一个临时对象
_ZN1AC1ERKS_(&a0, &temp);   //编译器对拷贝构造函数调用
foo(&temp);     // foo函数调用
_ZN1AD1Ev(&temp);   //  函数返回了,析构临时对象

c调用的方式其实就是把c++中类的方式全部转换成c的函数。

5.2.3 疑问

分析到这里就有一个疑问了,侯捷老师说有另一种实现方法,是以"拷贝构造"的方式把实际参数直接建构在其应该的位置上,该位置视函数活动范围的不同记录于程序堆栈中,在函数返回之前,局部对象的析构函数(如果有定义的话)会被执行。

安排g++分析的,感觉不属于这种,是属于老编译器的方法,所以就比较奇怪,难道是我分析出错了,还是就是这样,新方式又是如何?以后碰到知道了答案在回来分析了。

5.3 返回值的初始化

还有一哥情况,函数返回一个类对象,这时候会调用什么?我们来试试。

5.3.1 例子

#include <iostream>using namespace std;class A
{
public:A() {cout << "构造函数" << endl;}A(const A& a){  cout << "拷贝构造函数" << endl;}~A(){cout << "析构函数" << endl;}int a;// int fun1(int a) {return a;}
};A foo(int a)
{A x;x.a = a;return x;
}int main(int argc, char **argv)
{// A a0;           // 这个会调用一个构造函数A a1 = foo(0);// a1.fun1(1);return 0;
}

编译运行:

root@ubuntu:~/c++_mode/05# g++ 5_3.cpp -o 5_3
root@ubuntu:~/c++_mode/05# ./5_3
构造函数
析构函数

结果发现,只有构造函数,没有拷贝构造函数,真的是尴尬了。这是因为编译器优化了,g++编译器对这种返回对象的会进行优化,这个优化我们下节分析,目前我们就设置参数为不优化,编译运行看看:(不进行优化的参数:-fno-elide-constructors)

root@ubuntu:~/c++_mode/05# g++ -fno-elide-constructors 5_3.cpp -o 5_3
root@ubuntu:~/c++_mode/05# ./5_3
构造函数
拷贝构造函数
析构函数
拷贝构造函数
析构函数
析构函数
root@ubuntu:~/c++_mode/05#

这一波确实不优化了,但是好像多调用了一个拷贝构造函数,临时变量会再次把值赋值给真正的变量。

5.3.2 main函数反汇编代码查看

虽然结果不太一样,还是要硬着头皮看一下反汇编代码:

main:
.LFB1046:.cfi_startproc.cfi_personality 0x3,__gxx_personality_v0.cfi_lsda 0x3,.LLSDA1046pushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6pushq  %rbxsubq    $56, %rsp.cfi_offset 3, -24movl %edi, -52(%rbp)movq %rsi, -64(%rbp)movq %fs:40, %raxmovq    %rax, -24(%rbp)xorl %eax, %eaxleaq  -32(%rbp), %rax  // 申请一个temp0临时对象,作为参数,进入foo函数movl    $0, %esimovq    %rax, %rdi
.LEHB4:call _Z3fooi     // 调用
.LEHE4:leaq -32(%rbp), %rdx     leaq    -48(%rbp), %raxmovq %rdx, %rsi          // foo函数返回temp0movq %rax, %rdi      // a1
.LEHB5:call _ZN1AC1ERKS_  // a1.A::A(temp0)  通过拷贝构造函数,构造一个a1
.LEHE5:leaq -32(%rbp), %raxmovq %rax, %rdi
.LEHB6:call _ZN1AD1Ev   // 析构temp0临时对象
.LEHE6:movl $0, %ebxleaq    -48(%rbp), %raxmovq %rax, %rdi
.LEHB7:call _ZN1AD1Ev   // 析构a1对象
.LEHE7:movl %ebx, %eaxmovq  -24(%rbp), %rcxxorq %fs:40, %rcxje  .L14jmp .L17

分析了一下,编译器没有优化的代码确实做了很多无用功。

上面的汇编代码比较难看,我这里转化成c++代码:

A temp0;         // 会申请一个临时对象
foo(&temp0);        // 临时变量会作为参数传入
a1.A::A(temp0);     // 通过函数操作一般后,temp是函数操作后的对象,然后用拷贝构造函数赋值给a1
temp0.A::~A();      // 然后把临时对象析构
a1.A::~A();         // 最后才把a1对象析构

确实很复杂。下面我们来看看foo函数内部的代码。

5.3.3 foo函数反汇编代码查看

_Z3fooi:
.LFB1045:.cfi_startproc.cfi_personality 0x3,__gxx_personality_v0.cfi_lsda 0x3,.LLSDA1045pushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6pushq  %rbxsubq    $40, %rsp.cfi_offset 3, -24movq %rdi, -40(%rbp)movl %esi, -44(%rbp)movq %fs:40, %raxmovq    %rax, -24(%rbp)xorl %eax, %eaxleaq  -32(%rbp), %raxmovq %rax, %rdi
.LEHB0:call _ZN1AC1Ev       // 构造一个x对象
.LEHE0:movl -44(%rbp), %eaxmovl %eax, -32(%rbp)leaq -32(%rbp), %rdxmovq -40(%rbp), %raxmovq %rdx, %rsimovq  %rax, %rdi
.LEHB1:call _ZN1AC1ERKS_    // temp0.A::A(x) 通过x拷贝构造一个temp0
.LEHE1:nopleaq  -32(%rbp), %rax  movq   %rax, %rdi
.LEHB2:call _ZN1AD1Ev   // 析构x对象
.LEHE2:nopmovq  -40(%rbp), %raxmovq -24(%rbp), %rcxxorq %fs:40, %rcxje  .L7jmp  .L9

直接看反汇编代码比较难看,我们转换成c++代码:

A x;  // 定义一个x对象
x.A::A();       // 调用构造函数
temp0.A::A(x);   // 通过x拷贝构造一个temp0
x.A::~A();      // 析构x对象

确实发现这种做法很麻烦,之后还是不要设置这个参数,按照默认的就好,我这里是为了分析。

5.4 其他

重点是前面的3项,不过侯捷老师,在最后还提供了两个函数的调用,我们也来看看吧。

foo(11).fun1(11);   // 返回类对象,然后在调用函数

这个可能被转化为:

X temp;    // 编译器产生临时对象
(foo(temp, 11), temp).fun1(11);

这个是c语言的逗号表达式,先求左边的值,然后整体的值是右边的,这样去调用函数,反汇编代码已经看不出这个。

同样道理,如果程序声明了一个函数指针,像这样:

A(*pf)(int);
pf = foo;
pf(11).fun1(11);

可以转化成:

A x;
void (*pf)(A&, int);
pf = foo;
pf(x, 11);
x.fun1(11);

感觉就是展开的意思。

5.5 总结

今天这一篇就到这里了,今天学习了汇编知识,也终于分析了汇编代码,汇编能用于分析就差不多了,也不用仔细学习。

深入探索c++对象模型(五、程序转化语义)相关推荐

  1. Cpp 对象模型探索 / 程序转化语义

    一.定义时初始化 源码 #include <iostream> using namespace std;class A { public:A(){std::cout << &q ...

  2. 《深度探索C++对象模型》读书笔记第五章:构造析构拷贝语意学

    <深度探索C++对象模型>读书笔记第五章:构造析构拷贝语意学 对于abstract base class(抽象基类),class中的data member应该被初始化,并且只在constr ...

  3. 《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记

    来源:http://dsqiu.iteye.com/blog/1669614 之前一直对C++内部的原理的完全空白,然后找到<Inside The C++ Object Model>这本书 ...

  4. 第2章构造函数语义学读书笔记——深度探索c++对象模型

    深度探索c++对象模型 第2章 构造函数语义学 2.1 Default Constructor的构建操作 2.2 Copy Constructor的构造操作 2.3 程序转化语义学 2.4 成员的初始 ...

  5. 深度探索C++ 对象模型(4)-Default Copy Constructor(3)

    程序转化语意学 1. 显式初始化 原代码为: X x0; void foo_bar() { X x1(x0);X x2 = x0; X x3 = X(x0);} 编译器将产生拷贝构造函数,调用拷贝构造 ...

  6. 深度探索C++对象模型笔记

    一.关于对象 C 语言是程序性的,语言本身并没有支持数据和函数之间的关联性 C++ 中可能采取抽象数据类型,或者是多层次的类结构完成 C++ 的封装并没有增加多少成本,每一个成员函数虽然在class中 ...

  7. 深度探索C++对象模型第2章 构造函数语义学

    默认构造函数 两个误区: 1 任何class如果没有定义默认构造函数,就会被合成一个出来:只有在某些情况下被合成 2 编译器合成出来的默认构造函数会明确设定class中每一个数据成员的默认值 :默认值 ...

  8. [读书笔记]《深度探索C++对象模型》

    文章目录 前言 思维导图 第一章 关于对象 第二章 构造函数语意学 构造函数 拷贝构造函数 初始化列表 第三章 Data 语意学 第四章 Function 语意学 非静态成员函数 静态成员函数 虚成员 ...

  9. 解释:《深度探索C++对象模型》对NRV优化的讨论

    原文地址:http://blog.csdn.net/zha_1525515/article/details/7170059 感谢作者! 大纲: 函数返回局部对象的拷贝的一般实现方式. NRV(Name ...

最新文章

  1. leetcode-102 二叉树的层次遍历
  2. Java中Connection方法笔记
  3. DES加密实现的思想及代码
  4. 制作一个状态栏中跑马灯效果_snapseed制作“照片中的照片”画中画效果的方法...
  5. 云南计算机专升本数据结构_怎么查找云南省2019年专升本计算机专业试题
  6. css 盒模型的属性
  7. 如何解决EDM邮件营销中的图片难题
  8. 计算机密码忘了 开不了机怎么办,电脑设了开机密码现在忘了开不了机怎么处理?...
  9. 爬虫-urlencode与parse_qs函数
  10. 11GR DATAGRUAD环境BROKER配置Fast-Start Failover
  11. php二叉树层序遍历 带层数和不带层数 需要用到队列
  12. Xcode 5中缺少Provisioning Profiles菜单项
  13. npm install全局安装的模块路径自定义管理
  14. Neural3DMM与螺旋卷积
  15. Python 资源索引[绝对适合PYTHON人]
  16. 「Codeforces 335E」Counting Skyscrapers
  17. MSSQL2005的新功能创建数据库快照
  18. idea toggle offline mode
  19. 姜小白的Python日记Day6 集合的用法
  20. 云主机创建网络失败:Unable to create the network. No tenant network is available for allocation.

热门文章

  1. cct 二级java复习资料_年第一次全国高校安徽考区计算机水平考试(CCT)
  2. python Numpy 生成一个随机矩阵(整数型)
  3. Loopring(路印协议)——去中心化交易协议真的有未来吗?
  4. 吾爱破解160个crackme之008 009 0010 0011
  5. 推荐:我的在线电子书和培训咨询
  6. group by rollup 和grouping的使用实例
  7. 制作可以随身携带的系统盘
  8. mipcms栏目分类调用
  9. GIS二维电子地图开发总结
  10. Ubuntu下Python使用指南