前言

在使用资源前,我们需要做一些准备工作保证资源能正常使用,在使用完资源后,我们需要做一些扫尾工作保证资源没有泄露,这就是构造与析构了,这和编程语言是无关的,而是使用资源的一种方式。C++只不过是把这个过程内置到了语言本身,规定了构造函数和析构函数的形式以及执行时机。

编译器的无私奉献

下面这段代码很好理解

#include <iostream>
class A
{
public:A(){std::cout << "A\n";}~A(){std::cout << "~A\n";}
};
int main()
{A local;return 0;
}

如果执行的话,会输出

A
~A

对于一个从C转到C++的人,我就很纠结为什么我没有调用A::A()A::~A(),它们却执行了。

在GDB面前,程序是没有秘密的,因此就让我们开始GDB,看看剥去高级语言的外衣后的程序是什么样子。
用GDB的disassemble命令查看汇编代码,可以看到实际上调用了A::A()callq 0x400888 <A::A()>)和A::~A()callq 0x4008a6 <A::~A()>)。果然没有任何神奇的地方,函数都是需要被调用才会执行的,只不过我没有做的时候,编译器帮我做了。

(gdb) disassemble
Dump of assembler code for function main():                                                                          0x0000000000400806 <+0>:     push   %rbp                                                                          0x0000000000400807 <+1>:     mov    %rsp,%rbp                                                                     0x000000000040080a <+4>:     push   %rbx                                                                          0x000000000040080b <+5>:     sub    $0x18,%rsp
=> 0x000000000040080f <+9>:     lea    -0x11(%rbp),%rax                                                              0x0000000000400813 <+13>:    mov    %rax,%rdi                                                                     0x0000000000400816 <+16>:    callq  0x400888 <A::A()>                                                             0x000000000040081b <+21>:    mov    $0x0,%ebx                                                                     0x0000000000400820 <+26>:    lea    -0x11(%rbp),%rax                                                              0x0000000000400824 <+30>:    mov    %rax,%rdi                                                                     0x0000000000400827 <+33>:    callq  0x4008a6 <A::~A()>                                                            0x000000000040082c <+38>:    mov    %ebx,%eax                                                                     0x000000000040082e <+40>:    add    $0x18,%rsp                                                                    0x0000000000400832 <+44>:    pop    %rbx                                                                          0x0000000000400833 <+45>:    pop    %rbp                                                                          0x0000000000400834 <+46>:    retq
End of assembler dump.

编译器除了帮我们调用构造函数和析构函数外,如果我们没有写构造函数和析构函数,编译器会帮我们补上默认的构造函数和析构函数吗?
在下面的情况下,编译器会帮我们补上默认的构造函数

  • 类成员变量有构造函数:默认的构造函数里就是为了调用一下类成员变量的构造函数
  • 类的父类有构造函数:默认的构造函数就是为了调用一下父类的构造函数。父类是否有默认构造函数,同样取决于上一种情况。
  • 类的父类有虚函数:默认的构造函数就是为了设置一下虚函数表

在下面的情况下,编译器会帮我们补上默认的析构函数

  • 类成员变量有自己的析构函数:默认的析构函数里就只是为了调用一下类成员变量的析构函数
  • 类的父类有自己的析构函数:默认的析构函数为了调用父类的析构函数

从上面我们也可以看出编译器不做无用之事。当不需要构造函数或析构函数时,编译器就不会补上默认的构造函数和析构函数。我们知道C语言中是没有构造函数和析构函数的,可以简单的认为符合C语言语法的自定义类型,编译器都不会补上默认的构造函数和析构函数。大家可以了解下POD类型。

”符合C语言语法的自定义类型“的描述是不准确的,这是在将class视为struct,忽略权限关键字publicprotectedprivate的基础上说的,毕竟C中没有这些关键字。

构造和析构的时机

下面描述的前提是存在构造函数和析构函数

当实例化对象时,会执行构造函数,而实例化对象分为两种情况

  • 定义变量,如A a;。需要特别注意的是通过thread_local修饰定义的变量,在首次在线程中使用时才会执行构造函数。
  • new实例化,如new A;

    构造函数是无法主动调用的

当对象存储期结束时,就会执行析构函数,存储期分为

  • 静态存储期:进程退出时执行析构函数,如全局变量和静态局部变量
  • 自动存储期:离开变量的作用域时执行析构函数,如普通局部变量
  • 动态存储期:new实例化的对象,在delete时会执行析构函数。
  • 线程存储期:线程退出时执行析构函数,如thread_local修饰的变量

    因为析构函数是可以主动调用的,所以delete也可以只释放内存而不调用析构函数。

构造和析构的顺序

顺序就一句话:先构造后析构。分两部分来理解

  • 为什么需要先构造后析构
  • 如何实现先构造后析构

    先构造意味着先定义,但这只在同一文件中生效,不同文件之间的全局变量构造顺序是不确定的。

为什么需要先构造后析构

原因很朴素:先构造的对象说明其可能会被后续的对象使用,因此为了程序运行安全,必须等到其使用者结束使用后,才能析构该对象即在那些之后构造的对象析构后才能析构。

先构造后析构也是保证我们安全使用资源的一个原则。假设我们把一个功能的初始化封装为init(),把功能的销毁封装为destroy(),一般destroy()中资源销毁的顺序是init()中资源申请的逆序。

基于以上,我们就能很容易的理解

  • 为什么父类的构造函数先执行:因为本类的构造函数可能要用到父类的东西
  • 为什么类成员变量的构造函数先执行:因为本类的构造函数内可能要用到类成员变量

如何实现先构造后析构

普通局部变量的先构造后析构,就是编译器按照定义顺序插入对应的构造函数,然后再逆序插入析构函数。

int main()
{A local_1;A local_2;return 0;
}

其汇编如下

0x000000000040088f <+9>:     lea    -0x12(%rbp),%rax
0x0000000000400893 <+13>:    mov    %rax,%rdi               # local_1的地址
0x0000000000400896 <+16>:    callq  0x40093c <A::A()>
0x000000000040089b <+21>:    lea    -0x11(%rbp),%rax
0x000000000040089f <+25>:    mov    %rax,%rdi                 # local_2的地址
0x00000000004008a2 <+28>:    callq  0x40093c <A::A()>
0x00000000004008a7 <+33>:    mov    $0x0,%ebx
0x00000000004008ac <+38>:    lea    -0x11(%rbp),%rax
0x00000000004008b0 <+42>:    mov    %rax,%rdi                 # local_2的地址
0x00000000004008bf <+57>:    callq  0x40095a <A::~A()>
0x00000000004008c4 <+62>:    mov    %ebx,%eax
0x00000000004008c6 <+64>:    jmp    0x4008e2 <main()+92>
0x00000000004008c8 <+66>:    mov    %rax,%rbx
0x00000000004008cb <+69>:    lea    -0x12(%rbp),%rax
0x00000000004008cf <+73>:    mov    %rax,%rdi                # local_1的地址
0x00000000004008d2 <+76>:    callq  0x40095a <A::~A()>

全局变量和静态局部变量在析构函数的设置上稍有差别。

A global;
int main()
{return 0;
}

因为全局变量的构造在main()之前,所以在A::A()上设置断点。执行后,打印调用栈如下

(gdb) bt
#0  A::A (this=0x601171 <gloabl>) at main.cpp:15
#1  0x0000000000400856 in __static_initialization_and_destruction_0 (                                                __initialize_p=1, __priority=65535) at main.cpp:23
#2  0x0000000000400880 in _GLOBAL__sub_I_gloabl () at main.cpp:27
#3  0x000000000040090d in __libc_csu_init ()
#4  0x00007ffff771fe55 in __libc_start_main (main=0x400806 <main()>, argc=1,                                         argv=0x7fffffffec48, init=0x4008c0 <__libc_csu_init>,                                                            fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffec38)                                       at libc-start.c:246
#5  0x0000000000400739 in _start ()

让我们回到frame 1截取一小段它的汇编代码

   0x0000000000400851 <+64>:    callq  0x400882 <A::A()>
=> 0x0000000000400856 <+69>:    mov    $0x601058,%edx                                                                0x000000000040085b <+74>:    mov    $0x601171,%esi                                                                0x0000000000400860 <+79>:    mov    $0x4008a0,%edi                                                                0x0000000000400865 <+84>:    callq  0x4006d0 <__cxa_atexit@plt>

我们发现在执行完A::A()后,还调用了__cxa_atexit@plt。看到__cxa_atexit@plt,有没有觉得很熟悉,是不是立即就想到int atexit( void (*function)(void))。我们知道atexit()用于注册一个在进程退出时执行的函数,那么这里是不是注册了全局变量的析构函数?
mov $0x4008a0,%edi就是给`__cxa_atexit@plt传递参数,可知注册的函数地址是0x4008a0。我们可以轻易地发现A::~A()的地址就是0x4008a0

类成员函数的第一个参数是thismov $0x601171,%esi0x601171就是变量global的地址。

由此我们知道全局变量的析构函数是在执行构造函数后,注册为进程退出时的执行函数。我们知道通过atexit()注册的函数的执行顺序是先注册的后执行即FILO,__cxa_atexit也是一样,也就实现了先构造后析构。静态局部变量的析构函数也是一样的设置方式

假设一个类继承自一个有构造函数的类,且其成员变量也拥有构造函数。我们知道会先执行父类和成员变量的构造函数,然后再执行本类的构造函数。按这样的描述,岂不是要在每个实例化该类的地方加上很多额外的代码了,编译器会这么蠢么?让我们来看一下

class A
{
public:A(){}~A(){}
};
class B
{
public:B(){}~B(){}
};
class C : public A
{
public:C(){ std::cout << "C\n" };~C(){}B a;
};
int main()
{C local;return 0;
}

查看汇编,可知实际调用的仍是C::C(),并非在C::C()之前插入A和B的构造函数,

   0x00000000004006e6 <+16>:    callq  0x400788 <C::C()>                    0x00000000004006eb <+21>:    mov    $0x0,%ebx                            0x00000000004006f0 <+26>:    lea    -0x11(%rbp),%rax                     0x00000000004006f4 <+30>:    mov    %rax,%rdi                            0x00000000004006f7 <+33>:    callq  0x4007b0 <C::~C()>

而是在C::C()的第一行代码前,插入了A和B的构造函数。

Dump of assembler code for function C::C(): 0x0000000000400788 <+0>:     push   %rbp
=> 0x0000000000400789 <+1>:     mov    %rsp,%rbp                            0x000000000040078c <+4>:     sub    $0x10,%rsp                           0x0000000000400790 <+8>:     mov    %rdi,-0x8(%rbp)                      0x0000000000400794 <+12>:    mov    -0x8(%rbp),%rax                      0x0000000000400798 <+16>:    mov    %rax,%rdi                            0x000000000040079b <+19>:    callq  0x400758 <A::A()>                    0x00000000004007a0 <+24>:    mov    -0x8(%rbp),%rax                      0x00000000004007a4 <+28>:    mov    %rax,%rdi                            0x00000000004007a7 <+31>:    callq  0x400770 <B::B()>                    0x00000000004007ac <+36>:    nop                                         0x00000000004007ad <+37>:    leaveq                                      0x00000000004007ae <+38>:    retq

当然C::~C()的最后一行代码之后,也会插入A和B的析构函数。

常见问题

问题通常都来自于错误的构造顺序。
一种情况是a.cpp中定义的全局变量A使用了b.cpp中定义的全局变量B,实际A先构造,此时A使用到了还未构造的B,程序会出现异常。建议是保证不同全局变量之间是独立的。如果存在使用关系,则定义为指针类型,延迟到main()中再按预期的顺序依次实例化。

还有一种更常见的情况是使用了静态局部变量。因为静态局部变量包含在函数内部,更隐晦,所以更容易出现问题。问题通常是在进程退出时出现的,静态局部变量先析构了,导致程序异常。

后话

构造与析构是一种资源使用机制,我们常用C++的构造函数和析构函数来实现RAII(Resource Acquisition Is Initialization),保证诸如锁、内存等资源的正确使用和释放。

以上的代码都在www.onlinegdb.com上运行调试的。不同平台,不同编译器,其底层实现会存在差异,高级语言本就是为了隐藏这些底层差异,因此不必纠结于具体实现,而是要关注思维方式。

转载于:https://www.cnblogs.com/yizui/p/10590840.html

C++系列总结——构造与析构相关推荐

  1. Effective C++条款09:绝不在构造和析构过程中调用virtual函数

    Effective C++条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruc ...

  2. php构造和析构方法,php5构造函数与析构函数实例

    自php5起,有了构造函数与析构函数. 这使得php更富有面向对象的魅力了. 在php4时,构造函数用的是与类同名的函数来进行构造这个动作. 例如: 复制代码 代码示例: /* * myclass.p ...

  3. 内核中的对象操作的方法模块 和 C++ 构造和析构的对比

    1.内核中有很多的模块儿,就是module,但是后来我发现和C++的class中的 构造和析构完全一样,首先都需要init,然后都exit 退出之后都做些什么事情 2. 在不做内核编程的情况下,在应用 ...

  4. C++对象模型8——构造函数和析构函数中对虚函数的调用、全局对象构造和析构、局部static数组的内存分配

    一.构造函数和析构函数中对虚函数的调用 仍然以https://blog.csdn.net/Master_Cui/article/details/109957302中的代码为例 base3构造函数和析构 ...

  5. 【设计原则和建议】 构造和析构对象

    良好的构造和析构对象,控制对象生命周期可以较大的提高程序的性能,降低GC的压力,减少BUG几率. 本文还是比较简单的,主要还是经验的总结,很多东西也许各位已经知道,也许不知道.希望大家一起讨论. 1. ...

  6. 声明及赋值_重述《Effective C++》二——构造、析构、赋值运算

    关于本专栏,请看为什么写这个专栏.如果你想阅读带有条款目录的文章,欢迎访问我的主页. 构造和析构一方面是对象的诞生和终结:另一方面,它们也意味着资源的开辟和归还.这些操作犯错误会导致深远的后果--你需 ...

  7. C++绝不在构造和析构过程中调用virtual函数

    绝不在构造和析构过程中调用virtual函数 如果希望在继承体系中根据类型在构建对象时表现出不同行为,可以会想到在基类的构造函数中调用一个虚函数: class Transaction { //所有交易 ...

  8. C++继承中构造和析构顺序

    C++继承中构造和析构顺序 继承中构造和析构顺序 问题:父类和子类的构造和析构顺序是谁先谁后? 示例 继承中构造和析构顺序 子类继承父类后,当创建子类对象,也会调用父类的构造函数 问题:父类和子类的构 ...

  9. c++继承中的构造和析构

    c++继承中的构造和析构 类型兼容性原则 类型兼容规则中所指的替代包括以下情况: 继承中的对象模型 继承中构造和析构 继承中的构造析构调用原则 继承与组合混搭情况下,构造和析构调用原则 继承中的同名成 ...

最新文章

  1. 百度地图之根据地图上的点确定地图的放缩比例
  2. 自然语言处理的现实应用
  3. C风格字符串和C++ string 对象赋值操作的性能比较
  4. Reactor构架模式--转载
  5. HttpServletResponse和HttpServletRequest详解——Web网络学习笔记
  6. 【渝粤题库】广东开放大学 经济学基础 形成性考核
  7. 如何保证elasticsearch和mysql数据库的数据同步?
  8. Spring源码学习一,下载Spring源码并配置gradle环境
  9. python KM算法
  10. 计算机网络知识点汇总
  11. 自然语言处理 | (30) 文本相似度计算与文本匹配问题
  12. 旅游背包(多维有界的背包问题)
  13. 2021免费领取微软onedrive云盘1T空间
  14. 微信小程序radio单选框
  15. 分数阶微分方程c语言,第一讲分数阶微分方程.PDF
  16. 从词嵌入到文档距离论文笔记(From Word Embeddings To Document Distances)
  17. Windows批处理文件*.bat
  18. 从三个维度分析DeFi连环清算问题的解决方案 | 链捕手
  19. SWUSTOJ #616 排序查找
  20. LEADTOOLS V22.0 Patch

热门文章

  1. Protocol Buffer基本语法
  2. 《spring揭秘》读书笔记二
  3. 《编码:隐匿在计算机软硬件背后的语言(美)》读书笔记三
  4. PDMan-2.1.3 发布:用心开源,免费的国产数据库建模工具
  5. lua元表和元方法 《lua程序设计》 13章 读书笔记
  6. MySQL配置全文索引
  7. SCCM 2012 R2实战系列之三:独立主站点部署
  8. linux挂载点的容量设置
  9. Erlang基础学习总结2
  10. GIS 缓冲区应用及算法实现