C++系列总结——构造与析构
前言
在使用资源前,我们需要做一些准备工作保证资源能正常使用,在使用完资源后,我们需要做一些扫尾工作保证资源没有泄露,这就是构造与析构了,这和编程语言是无关的,而是使用资源的一种方式。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
,忽略权限关键字public
、protected
、private
的基础上说的,毕竟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
。
类成员函数的第一个参数是
this
,mov $0x601171,%esi
的0x601171
就是变量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上运行调试的。不同平台,不同编译器,其底层实现会存在差异,高级语言本就是为了隐藏这些底层差异,因此不必纠结于具体实现,而是要关注思维方式。
C++系列总结——构造与析构的更多相关文章
- STL—对象的构造与析构
STL内存空间的配置/释放与对象内容的构造/析构,是分开进行的. 对象的构造.析构 对象的构造由construct函数完成,该函数内部调用定位new运算符,在指定的内存位置构造对象 ...
- C++浅析——继承类中构造和析构顺序
先看测试代码,CTEST 继承自CBase,并包含一个CMember成员对象: static int nIndex = 1; class CMember { public: CMember() { p ...
- Effective C++ -----条款09:绝不在构造和析构过程中调用virtual函数
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层).
- 【09】绝不在构造和析构过程中调用virtual方法
1.绝不在构造和析构过程中调用virtual方法,为啥? 原因很简单,对于前者,这种情况下,子类专有成分还没有构造,对于后者,子类专有成分已经销毁,因此调用的并不是子类重写的方法,这不是程序员所期望的 ...
- C++不能中断构造函数来拒绝产生对象(在构造和析构中抛出异常)
这是我的感觉,具体需要研究一下- 找到一篇文章:在构造和析构中抛出异常 测试验证在类构造和析构中抛出异常, 是否会调用该类析构. 如果在一个类成员函数中抛异常, 可以进入该类的析构函数. /// @f ...
- STL——空间配置器(构造和析构基本工具)
以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体地说是指容器,container)的背后,默默工作,默默付出.但若以STL的实现角度而言,第一个需要介绍的就是空间配 ...
- 魔法方法:构造和析构 - 零基础入门学习Python041
魔法方法:构造和析构 让编程改变世界 Change the world by program 构造和析构 什么是魔法方法呢?我们来系统总结下: - 魔法方法总是被双下划线包围,例如__init__ - ...
- 再探Delphi2010 Class的构造和析构顺序
发了上一篇博客.盒子上有朋友认为Class的构造和析构延迟加载.是在Unit的初始化后调用的Class的构造.在Unit的反初始化前调用的Class的析构函数. 为了证明一下我又做了个试验 unit ...
- Effective C++_笔记_条款09_绝不在构造和析构过程中调用virtual函数
(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 为方便采用书上的例子,先提出问题,在说解决方案. 1 问题 1: ...
随机推荐
- idea注册码
2019 idea 注册码: N757JE0KCT-eyJsaWNlbnNlSWQiOiJONzU3SkUwS0NUIiwibGljZW5zZWVOYW1lIjoid3UgYW5qdW4iLC ...
- Golang Go Go Go part1:安装及运行
golang 知识图谱 https://www.processon.com/view/link/5a9ba4c8e4b0a9d22eb3bdf0 一.安装 最新版本安装包地址:https://gola ...
- SpringBoot 集成 Swageer2
添加Maven依赖 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox ...
- [Swift]LeetCode676. 实现一个魔法字典 | Implement Magic Dictionary
Implement a magic directory with buildDict, and search methods. For the method buildDict, you'll be ...
- [Swift]LeetCode836. 矩形重叠 | Rectangle Overlap
A rectangle is represented as a list [x1, y1, x2, y2], where (x1, y1) are the coordinates of its bot ...
- mybatis 详解------动态SQL
mybatis 详解------动态SQL 目录 1.动态SQL:if 语句 2.动态SQL:if+where 语句 3.动态SQL:if+set 语句 4.动态SQL:choose(when,o ...
- markdown反射型xss漏洞复现
markdown xss漏洞复现 转载至橘子师傅:https://blog.orange.tw/2019/03/a-wormable-xss-on-hackmd.html 漏洞成因 最初是看到Hack ...
- 剖析项目多个logback配置(上)
来源:http://www.cnblogs.com/guozp/p/5949744.html 以下两个是我在使用slf4j + logback时候日志提示的问题,问题不大,都是WARN,并不真正影响运 ...
- selenium下拉到页面最底端
selenium操控浏览器下拉到页面最底端: #!/usr/bin/env python # -*- coding: utf-8 -*- from selenium import webdriver ...
- java集合框架整理
一.总体框架 Java集合是java提供的工具包,包含了常用的数据结构:集合.链表.队列.栈.数组.映射等.Java集合工具包位置是java.util.* .Java集合主要可以划分为4个部分:Lis ...