深入探索c++对象模型(五、程序转化语义)
我们上一篇开头列举了好几个例子,其中都没有调用拷贝构造函数,这一篇我们还是以那几个例子,并且这次我们定义了拷贝构造函数,来分析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++对象模型(五、程序转化语义)相关推荐
- Cpp 对象模型探索 / 程序转化语义
一.定义时初始化 源码 #include <iostream> using namespace std;class A { public:A(){std::cout << &q ...
- 《深度探索C++对象模型》读书笔记第五章:构造析构拷贝语意学
<深度探索C++对象模型>读书笔记第五章:构造析构拷贝语意学 对于abstract base class(抽象基类),class中的data member应该被初始化,并且只在constr ...
- 《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记
来源:http://dsqiu.iteye.com/blog/1669614 之前一直对C++内部的原理的完全空白,然后找到<Inside The C++ Object Model>这本书 ...
- 第2章构造函数语义学读书笔记——深度探索c++对象模型
深度探索c++对象模型 第2章 构造函数语义学 2.1 Default Constructor的构建操作 2.2 Copy Constructor的构造操作 2.3 程序转化语义学 2.4 成员的初始 ...
- 深度探索C++ 对象模型(4)-Default Copy Constructor(3)
程序转化语意学 1. 显式初始化 原代码为: X x0; void foo_bar() { X x1(x0);X x2 = x0; X x3 = X(x0);} 编译器将产生拷贝构造函数,调用拷贝构造 ...
- 深度探索C++对象模型笔记
一.关于对象 C 语言是程序性的,语言本身并没有支持数据和函数之间的关联性 C++ 中可能采取抽象数据类型,或者是多层次的类结构完成 C++ 的封装并没有增加多少成本,每一个成员函数虽然在class中 ...
- 深度探索C++对象模型第2章 构造函数语义学
默认构造函数 两个误区: 1 任何class如果没有定义默认构造函数,就会被合成一个出来:只有在某些情况下被合成 2 编译器合成出来的默认构造函数会明确设定class中每一个数据成员的默认值 :默认值 ...
- [读书笔记]《深度探索C++对象模型》
文章目录 前言 思维导图 第一章 关于对象 第二章 构造函数语意学 构造函数 拷贝构造函数 初始化列表 第三章 Data 语意学 第四章 Function 语意学 非静态成员函数 静态成员函数 虚成员 ...
- 解释:《深度探索C++对象模型》对NRV优化的讨论
原文地址:http://blog.csdn.net/zha_1525515/article/details/7170059 感谢作者! 大纲: 函数返回局部对象的拷贝的一般实现方式. NRV(Name ...
最新文章
- leetcode-102 二叉树的层次遍历
- Java中Connection方法笔记
- DES加密实现的思想及代码
- 制作一个状态栏中跑马灯效果_snapseed制作“照片中的照片”画中画效果的方法...
- 云南计算机专升本数据结构_怎么查找云南省2019年专升本计算机专业试题
- css 盒模型的属性
- 如何解决EDM邮件营销中的图片难题
- 计算机密码忘了 开不了机怎么办,电脑设了开机密码现在忘了开不了机怎么处理?...
- 爬虫-urlencode与parse_qs函数
- 11GR DATAGRUAD环境BROKER配置Fast-Start Failover
- php二叉树层序遍历 带层数和不带层数 需要用到队列
- Xcode 5中缺少Provisioning Profiles菜单项
- npm install全局安装的模块路径自定义管理
- Neural3DMM与螺旋卷积
- Python 资源索引[绝对适合PYTHON人]
- 「Codeforces 335E」Counting Skyscrapers
- MSSQL2005的新功能创建数据库快照
- idea toggle offline mode
- 姜小白的Python日记Day6 集合的用法
- 云主机创建网络失败:Unable to create the network. No tenant network is available for allocation.