2020-12-14(全局/静态对象的构造函数和析构函数调用的时机以及地址)

本文解析了C++中对象实例化的时机,特别是在main函数前全局/静态对象如何构造与析构的过程。全局对象构造发生在mainCRTStartup阶段的_cinit函数中,通过构造代理函数实现。而析构则在main函数执行完毕后的exit函数中调用。

一般的对象实例化在什么时候实例化的呢?
是不是在main函数运行到那里的时候,然后创建对象,会调用类里面的构造函数。
那当我们遇到全局/静态对象的时候,它是不是也是需要在main函数里面慢慢构造呢?
答案是 :
不是的。
全局/静态对象 的构造函数调用实在main函数之前的。
有人在疑问?main函数之前还有函数?不是从main函数开始运行的吗?
全局对象所在的内存地址空间为全局数据区,而局部对象的内存地址空间在栈中,他们触发构造函数和析构函数时机不同。

全局/静态对象构造函数实现:

在启动函数mainCRTStartup中有个_cinit,全局对象的构造函数就是在此函数中实现的,
在函数_cinit的_initterm函数调用中,初始化了全局对象。_initterm实现代码片段如下:

while(pfbegin<pfend){//pfbegin==_xc_a  pfend==_xc_z
	if(*pfbegin!=NULL)
	(**pfbgin)()//调用每一个初始化或构造代理函数
	++pfbegin;
}

当pfbegin不为NULL时进入if语句块。执行(**pfbegin)();
后并不会进入全局对象的构造函数中,而是进入编译器提供的构造代理函数中,由一个负责全局对象的构造代理函数完成对全局构造函数的调用过程。

构造代理函数代码如下:
在这里插入图片描述
在这里插入图片描述
由于构造函数需要传递对象的首地址作为this指针,而且构造函数可以带各类参数,因此编译器将为每个全局对象生成一段传递this指针和参数的代码,然后使用无参的代理函数去调用构造函数。

全局/静态对象析构函数实现:
全局/静态对象相同,其构造函数在函数_cinit的第二个_initterm调用中被构造。他们的析构函数的调用时机是在main函数执行完毕之后。既然构造函数出现在初始化过程中,对应的析构函数就会出现在程序结束出。我们来看一下mainCRTStartup函数,它在调用main函数结束后使用了exit用来终止程序,如下:

mainret=main(_argc,_argv,_environ);
//WPRFLAG

//_WINMAIN_
exit(mainret);

在main函数调用结束后,由exit来结束进程,从而终止程序的运行。全局对象的析构函数的调用也在其中,由exit函数内的doexit实现,关键代码如下:

if(_onexitbegin)//_onexitbegin为函数指针数组的首地址
{
_PVFV * pfend=_onexitend; //__onexitbegin为函数指针数组的尾地址
while(--pend >=__onexitbegin)//从后向前依次释放全局对象
	if(*pend!=NULL)
	(**pend)();//调用数组中保存的函数
}

__onexitbegin指向一个指向数组,该数组中保存着各类资源释放时的函数的首地址。编译器实在何时生成这样一个数组的呢?
全局构造函数的调用是在_cinit函数的第二个_initterm函数内完成,而在第二个_initterm函数的初始化函数指针数组。在执行每个全局对象构造代理函数时都会执行对象的构造函数,然后使用atexit注册析构代理函数。

举例:
如果定义一个全局对象CMyString G_MyStringTwo;,该对象的全局析构函数代理函数的分析如下所示:
;该代理函数由编译器添加,无源码对照
;函数入口对照

mov  ecx ,offset g_MyStringTwo(0042af7c)
call @ILT+35(MyStringTwo::~MyStringTwo)(00401028)
;函数退出部分
ret

由于函数数组中保存的析构代理函数被定义为无参函数,因此在调用析构函数时无法传递this指针。于是编译器需要为每个全局变量和静态对象建立一个中间代理的析构函数,用于传入全局对象的this指针。

在 C++ 中,构造函数析构函数调用顺序与对象生命周期密切相关,理解这些规则对于正确管理资源避免内存泄漏至关重要。 ### 构造函数调用顺序 构造函数的执行顺序遵循以下规则: - 基类构造函数先于派生类构造函数执行。 - 类中成员对象构造函数按照它们在类中声明的顺序执行。 - 派生类构造函数最后执行。 这种顺序确保了基类成员对象在派生类使用它们之前已经被正确初始化[^1]。 例如,在单继承的情况下: ```cpp class Base { public: Base() { /* 基类构造函数 */ } }; class Derived : public Base { public: Derived() { /* 派生类构造函数 */ } }; ``` `Derived` 对象的构造过程会首先调用 `Base` 的构造函数,然后是 `Derived` 的构造函数[^5]。 ### 析构函数调用顺序 析构函数的执行顺序与构造函数完全相反: - 派生类析构函数先于基类析构函数执行。 - 成员对象析构函数按照它们在类中声明的顺序反向执行。 - 基类析构函数最后执行。 例如: ```cpp class Base { public: ~Base() { /* 基类析构函数 */ } }; class Derived : public Base { public: ~Derived() { /* 派生类析构函数 */ } }; ``` 当 `Derived` 对象销毁时,首先调用 `~Derived()`,然后是 `~Base()`[^5]。 ### 对象生命周期 对象的生命周期决定了构造析构的时间点: - **局部对象**:定义在函数内部的对象,其生命周期从构造开始直到离开作用域为止。此时编译器自动调用析构函数- **全局对象**:程序启动时构造,程序结束前销毁。构造顺序为文件内定义顺序,析构顺序则相反。 - **静态对象**:仅在首次进入其作用域时构造一次,生命周期持续到程序结束。 - **堆上对象**:通过 `new` 创建的对象需要显式使用 `delete` 来销毁,否则不会自动释放内存或调用析构函数[^4]。 默认情况下,如果用户未显式定义析构函数,C++ 编译器将自动生成一个默认析构函数,并对类中的成员对象依次调用它们的析构函数[^2]。 ### 示例代码 下面是一个完整的示例,展示构造析构的调用顺序: ```cpp #include <iostream> using namespace std; class A { public: A() { cout << "A Constructor" << endl; } ~A() { cout << "A Destructor" << endl; } }; class B { public: B() { cout << "B Constructor" << endl; } ~B() { cout << "B Destructor" << endl; } }; class C : public A { public: C() { cout << "C Constructor" << endl; } ~C() { cout << "C Destructor" << endl; } private: B b; }; int main() { { C c; } // c 超出作用域,析构函数调用 return 0; } ``` 输出结果如下: ``` A Constructor B Constructor C Constructor C Destructor B Destructor A Destructor ``` 说明: 1. 首先调用基类 `A` 的构造函数。 2. 然后调用成员变量 `b`(属于类 `B`)的构造函数。 3. 最后执行派生类 `C` 的构造函数。 4. 当 `c` 离开作用域时,按相反顺序调用析构函数:`C` → `B` → `A` [^1]。 ###
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻梦&之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值