首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
1
篇与
的结果
2016-06-29
C++类的构造函数与析构函数
C++中每个类都有其构造与析构函数,它们负责对象的创建和对象的清理和回收,即使我们不写这两个,编译器也会默认为我们提供这些构造函数。下面仍然是通过反汇编的方式来说明C++中构造和析构函数是如何工作的。编译器是否真的会默认提供构造与析构函数在一般讲解C++的书籍中都会提及到当我们不为类提供任何构造与析构函数时编译器会默认提供这样六种成员函数:不带参构造,拷贝构造,“=”的重载函数,析构函数,以及带const和不带const的取地址符重载。但是编译器具体是怎么做的,下面来对其中的部分进行说明不带参构造下面是一个例子:class test { private: char szBuf[255]; public: static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }下面是对应的汇编源码:00401400 push ebp 00401401 mov ebp,esp 00401403 sub esp,140h;栈顶向上抬了140h的空间用于存储类对象的数据 00401409 push ebx 0040140A push esi 0040140B push edi 0040140C lea edi,[ebp-140h] 00401412 mov ecx,50h 00401417 mov eax,0CCCCCCCCh 0040141C rep stos dword ptr [edi] 26: test t; 27: printhello(t); 0040141E sub esp,100h从上面可以看到,在定义类的对象时并没有进行任何的函数调用,在进行对象的内存空间分配时仅仅是将栈容量扩大,就好像定义一个普通变量一样,也就是说在默认情况下编译器并不会提供不带参的构造函数,在初始化对象时仅仅将其作为一个普通变量,在编译之前计算出它所占内存的大小,然后分配,并不调用函数。再看下面一个例子:class test { private: char szBuf[255]; public: virtual void sayhello() { cout<<"hello world"; } static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }30: test t; 0040143E lea ecx,[ebp-104h] 00401444 call @ILT+140(test::test) (00401091) ;构造函数 004014A0 push ebp 004014A1 mov ebp,esp 004014A3 sub esp,44h 004014A6 push ebx 004014A7 push esi 004014A8 push edi 004014A9 push ecx 004014AA lea edi,[ebp-44h] 004014AD mov ecx,11h 004014B2 mov eax,0CCCCCCCCh 004014B7 rep stos dword ptr [edi] 004014B9 pop ecx 004014BA mov dword ptr [ebp-4],ecx 004014BD mov eax,dword ptr [ebp-4] 004014C0 mov dword ptr [eax],offset test::`vftable' (0042f02c) 004014C6 mov eax,dword ptr [ebp-4] 004014C9 pop edi 004014CA pop esi 004014CB pop ebx 004014CC mov esp,ebp 004014CE pop ebp这段C++代码与之前的仅仅是多了一个虚函数,这个时候编译器为这个类定义了一个默认的构造函数,从汇编代码中可以看到,这个构造函数主要初始化了类对象的头4个字节,将虚函数表的地址放入到这个4个字节中,因此我们得出结论,一般编译器不会提供不带参的构造函数,除非类中有虚函数。下面请看这样一个例子:class Parent { public: Parent() { cout<<"parent!"<<endl; } }; class child: public Parent { private: char szBuf[10]; }; int main() { child c; return 0; } 下面是它的汇编代码:27: child c; 004013A8 lea ecx,[ebp-0Ch] 004013AB call @ILT+100(child::child) (00401069) ;构造函数 ;函数初始化代码略 004013E9 pop ecx 004013EA mov dword ptr [ebp-4],ecx 004013ED mov ecx,dword ptr [ebp-4] 004013F0 call @ILT+80(Parent::Parent) (00401055) ;最后函数的收尾工作,代码略从上面的代码看,当父类存在构造函数时,编译器会默认为子类添加构造函数,子类的构造函数主要是调用父类的构造函数。class Parent { public: virtual sayhello() { cout<<"hello"<<endl; } }; class child: public Parent { private: char szBuf[10]; }; int main() { child c; return 0; }27: child c; 004013B8 lea ecx,[ebp-10h] 004013BB call @ILT+100(child::child) (00401069) ;构造函数 004013FA mov dword ptr [ebp-4],ecx 004013FD mov ecx,dword ptr [ebp-4] 00401400 call @ILT+80(Parent::Parent) (00401055);调用父类的构造函数 00401405 mov eax,dword ptr [ebp-4] 00401408 mov dword ptr [eax],offset child::`vftable' (0042f01c);初始化虚函数表 0040140E mov eax,dword ptr [ebp-4]从上面的代码中可以看到,当父类有虚函数时,编译器也会提供构造函数,主要用于初始化头四个字节的虚函数表的指针。拷贝构造当我们不写拷贝构造的时候,仍然能用一个对象初始化另一个对象,下面是这样的一段代码int main(int argc, char* argv[]) { test t1; test t(t1); printhello(t); return 0; }我们还是用之前定义的那个test类,将类中的虚函数去掉,下面是对应的反汇编代码30: test t1; 31: test t(t1); 0040141E mov ecx,3Fh 00401423 lea esi,[ebp-100h] 00401429 lea edi,[ebp-200h] 0040142F rep movs dword ptr [edi],dword ptr [esi] 00401431 movs word ptr [edi],word ptr [esi] 00401433 movs byte ptr [edi],byte ptr [esi]从这段代码中可以看到,利用一个已有的类对象来初始化一个新的对象时,编译器仍然没有为其提供所谓的默认拷贝构造函数,在初始化时利用串操作,将一个对象的内容拷贝到另一个对象。当类中有虚函数时,会提供一个拷贝构造,主要用于初始化头四个字节的虚函数表,在进行对象初始化时仍然采用的是直接内存拷贝的方式。由于默认的拷贝构造是进行简单的内存拷贝,所以当类中的成员中有指针变量时尽量自己定义拷贝构造,进行深拷贝,否则在以后进行析构时会崩溃。另外几种就不再一一进行说明,它们的情况与上面的相似,有兴趣的可以自己编写代码验证。另外需要注意的是,只要定义了任何一个类型的构造函数,那么编译器就不会提供默认的构造函数。最后总结一下默认情况下编译器不提供这些函数,只有父类自身有构造函数,或者自身或父类有虚函数时,编译器才会提供默认的构造函数。何时会调用构造函数当对一个类进行实例化,也就是创建一个类的对象时,会调用其构造函数。对于栈中的局部对象,当定义一个对象时会调用构造函数对于堆对象,当用户调用new新建对象时调用构造函数对于全局对象和静态对象,当程序运行之处会调用构造函数下面重点说明当对象作为函数参数和返回值时的情况作为函数参数当对象作为函数参数时调用的是拷贝构造,而不是普通的构造函数下面是一个例子代码:class CA { public: CA() { cout<<"构造函数"<<endl; } CA(CA &ca) { cout<<"拷贝构造"<<endl; } private: char szBuf[255]; }; void Test(CA a) { return; } int main() { CA a; Test(a); return 0; }对应的汇编代码如下:33: Test(a); 004013F9 sub esp,100h 004013FF mov ecx,esp 00401401 lea eax,[ebp-100h];eax保存对象的首地址 00401407 push eax 00401408 call @ILT+15(CA::CA) (00401014) 0040140D call @ILT+35(Test) (00401028) ;拷贝构造代码 cout<<"拷贝构造"<<endl; 0040152D push offset @ILT+50(std::endl) (00401037) 00401532 push offset string "\xbf\xbd\xb1\xb4\xb9\xb9\xd4\xec" (0042f028) 00401537 push offset std::cout (00434088) 0040153C call @ILT+170(std::operator<<) (004010af) 00401541 add esp,8 00401544 mov ecx,eax 00401546 call @ILT+125(std::basic_ostream<char,std::char_traits<char> >::operator<<) (00401082) 21: } 0040154B mov eax,dword ptr [ebp-4] 0040154E pop edi 0040154F pop esi 00401550 pop ebx 00401551 add esp,44h 00401554 cmp ebp,esp 00401556 call __chkesp (004025d0) 0040155B mov esp,ebp 0040155D pop ebp 0040155E ret 4从上面的代码来看,当对象作为函数参数时,首先调用构造函数,将参数进行拷贝。作为函数的返回值class CA { public: CA() { cout<<"构造函数"<<endl; } CA(CA &ca) { cout<<"拷贝构造"<<endl; } private: char szBuf[255]; }; CA Test() { CA a; return a; } int main() { CA a = Test(); return 0; }34: CA a = Test(); 0040155E lea eax,[ebp-200h];eax保存的是对象a 的首地址 00401564 push eax 00401565 call @ILT+145(Test) (00401096);调用test函数 0040156A add esp,4 0040156D push eax;函数返回的临时存储区的地址 0040156E lea ecx,[ebp-100h] 00401574 call @ILT+15(CA::CA) (00401014);调用拷贝构造 ;test函数 28: CA a; 004013BE lea ecx,[ebp-100h] 004013C4 call @ILT+10(CA::CA) (0040100f) 29: return a; 004013C9 lea eax,[ebp-100h] 004013CF push eax 004013D0 mov ecx,dword ptr [ebp+8] 004013D3 call @ILT+15(CA::CA) (00401014);调用拷贝构造 004013D8 mov eax,dword ptr [ebp+8];ebp + 8是用来存储对象的临时存储区 通过上面的反汇编代码可以看到,在函数返回时会首先调用拷贝构造,将对象的内容拷贝到一个临时存储区中,然后通过eax寄存器返回,在需要利用函数返回值时再次调用拷贝构造,将eax中的内容拷贝到对象中。另外从这些反汇编代码中可以看到,拷贝构造以对象的首地址为参数,返回新建立的对象的地址。当需要对对象的内存进行拷贝时调用拷贝构造,拷贝构造只能传递对象的地址或者引用,不能传递对象本身,我们知道对象作为函数参数时会调用拷贝构造,如果以对象作为拷贝构造的参数,那么回造成拷贝构造的无限递归。何时调用析构函数对于析构函数的调用我们仍然分为以下几个部分:局部类对象:当对象所在的生命周期结束后,即一般语句块结束或者函数结束时会调用全局对象和静态类对象:当程序结束时会调用构造函数堆对象:当程序员显式调用delete释放空间时调用参数对象下面是一个例子代码:class CA { public: ~CA() { printf("~CA()\n"); } private: char szBuf[255]; }; void Test(CA a) { printf("test()\n"); } int main() { CA a; Test(a); return 0; }下面是它的反汇编代码;Test(a) 0040133A sub esp,100h;在main函数栈外开辟一段内存空间用于保存函数参数 00401340 mov ecx,3Fh;类大小为255个字节,为了复制这块内存,每次复制4字节,共需要63次 00401345 lea esi,[ebp-10Ch];esi保存的是对象的首地址 0040134B mov edi,esp;参数首地址 0040134D rep movs dword ptr [edi],dword ptr [esi];执行复制操作 0040134F movs word ptr [edi],word ptr [esi] 00401351 movs byte ptr [edi],byte ptr [esi];将剩余几个字节也复制 00401352 call @ILT+5(Test) (0040100a);调用test函数 ;调用Test函数 23: printf("test()\n"); 00401278 push offset string "test()\n" (0042f01c) 0040127D call printf (00401640) 00401282 add esp,4 24: } 00401285 lea ecx,[ebp+8];参数首地址 00401288 call @ILT+0(CA::~CA) (00401005)从上面的代码看,当类对象作为函数参数时,首先会调用拷贝构造(当程序不提供拷贝构造时,系统默认在对象之间进行简单的内存复制,这个就是提供的默认拷贝构造函数)然后当函数结束,程序执行到函数大括号初时,首先调用析构完成对象内存的释放,然后执行函数返回和做最后的清理工作函数返回对象下面是函数返回对象的代码:class CA { public: ~CA() { printf("~CA()\n"); } private: char szBuf[255]; }; CA Test() { printf("test()\n"); CA a; return a; } int main() { CA a = Test(); return 0; }30: CA a = Test(); 0040138E lea eax,[ebp-100h];eax保存了对象的地址 00401394 push eax 00401395 call @ILT+20(Test) (00401019) 0040139A add esp,4 31: return 0; 0040139D mov dword ptr [ebp-104h],0 004013A7 lea ecx,[ebp-100h] 004013AD call @ILT+0(CA::~CA) (00401005);调用类的析构函数 004013B2 mov eax,dword ptr [ebp-104h] 32: } ;test函数 24: CA a; 25: return a; 004012AA mov ecx,3Fh 004012AF lea esi,[ebp-10Ch];esi保存的是类对象的首地址 004012B5 mov edi,dword ptr [ebp+8];ebp+8是当初调用这个函数时传进来的类的首地址 004012B8 rep movs dword ptr [edi],dword ptr [esi] 004012BA movs word ptr [edi],word ptr [esi] 004012BC movs byte ptr [edi],byte ptr [esi] 004012BD mov eax,dword ptr [ebp-110h] 004012C3 or al,1 004012C5 mov dword ptr [ebp-110h],eax 004012CB lea ecx,[ebp-10Ch] 004012D1 call @ILT+0(CA::~CA) (00401005);调用析构函数 004012D6 mov eax,dword ptr [ebp+8]当类作为返回值返回时,如果定义了一个变量来接收这个返回值,那么在调用函数时会首先保存这个值,然后直接复制到这个内存中,但是接着执行类的析构函数析构在函数中定义的类对象,接受返回值得这块内存一直等到它所在的语句块结束才调用析构如果不要这个返回值时又如何呢,下面的代码说明了这个问题int main() { Test(); printf("main()\n"); return 0; } 30: Test(); 0040138E lea eax,[ebp-100h] 00401394 push eax 00401395 call @ILT+20(Test) (00401019) 0040139A add esp,4 0040139D lea ecx,[ebp-100h] 004013A3 call @ILT+0(CA::~CA) (00401005) 31: printf("main()\n"); 004013A8 push offset string "main()\n" (0042f030) 004013AD call printf (00401660) 004013B2 add esp,4同样可以看到当我们不需要这个返回值时,函数仍然会将对象拷贝到这块临时存储区中,但是会立即进行析构对这块内存进行回收。
2016年06月29日
7 阅读
0 评论
0 点赞