1、作用
- 注册函数,main函数返回或者exit函数调用,函数执行顺序与注册顺序相反
-程序退出时调用静态全局变量析构函数(包含静态成员变量)
2、静态对象
全局静态对象构造函数最早,到局部成员静态对象构造函数,析构则相反
想在程序退出时析构,要在atexit注册
1> 在C/C++中,怎样在程序退出main函数后仍然进行一些操作,比如资源堆栈方面的清理?
2> 在C++程序中,我们要怎样才能够得到程序退出main后对象析构所需要的时间?
我们知道,在C++中,全局对象或者main域中对象的析构,是在退出main函数后进行的。所以对于问题1,我们可以在一个全局对象的析构函数中调用相关的函数来实现。但是,C语言不支持对象,所以这种方法对于C程序就无能为力了。
对于问题2,我们可以通过对象的构造与析构来勉强实现这一功能。我们可以定义两个类A,B,其中A的析构函数可以打印出当前时间,B的构造函数可以打印出当前时间,此外A,B什么也不做。如果我们在所有全局对象的实例定义前定义A的一个实例,而在main函数退出 (return 0 )前定义B的一个实例,那么这两个时间的差就是我们所要的。
然而上述对问题2的解决是基于程序中没有调用exit()函数,若否,对象b就不会被创建,我们也就不能得到想要的时间了。
其实C/C++的CRT中有一个回调函数atexit(…),可以用来注册需要在程序exit的时候需要调用的函数,详细请参考:
http://www.cprogramming.com/fod/atexit.html 或者C:/Program Files/Microsoft Visual Studio 8/VC/crt/src/atonexit.c:123 上述问题1可以完全通过atexit函数实现,而问题2则可以通过综合atexit和对象的析构来完成。事实上,大多数winodws上的C++编译器,就是通过使用atexit函数来完成全局对象的析构的。
这个问题在GCC的后期版本中得到解决,包括C/C++标准库和链接器。基本上,C析构函数应该使用__cxa_atexit函数而不是atexit(3)进行注册。
有关__cxa_atexit的完整技术细节,请参见Itanium C++ ABI specification。
您的问题不清楚您正在使用哪个版本的gcc,链接器和标准C库。此外,您提供的代码不符合POSIX标准,因为没有定义RTDL_NOW或RTDL_LOCAL宏。它们是RTLD_NOW和RTLD_LOCAL(参见dlopen)。
如果您的C标准库不支持__cxa_atexit,则可能需要通过指定-fno-use-cxa-atexit gcc标志来禁用它:
-fuse-cxa-atexit
Register destructors for objects with static storage
duration with the _cxa atexit
function rather than the atexit
function. This option is required for
fully standards-compliant handling of
static destructors, but will only work
if your C library supports
__cxa_atexit.
但是这可能会导致一个问题,其中析构函数被调用不同的顺序或根本不被调用。因此,在破坏__cxa_atexit支持或根本不支持的情况下,最好的解决方案是不要在共享库中使用具有析构函数的静态对象。
C++ 标准要求当程序以相反的顺序退出时,为全局对象调用析构函数。 大多数实现都通过调用C 库atexit例程来注册析构函数来处理这个问题。 这是有问题的,因为 1999 C 标准只要求实现支持 32注册函数,尽管大多数实现都支持更多的。 更重要的是,它并不在所有处理能力在大多数实现去除图像从一个运行的程序都是通过调用dlclose程序之前终止。
这里问题在 C/C++的后续版本中解决,包括标准库和链接器。 基本上,C++ 析构函数应该使用 __cxa_atexit 函数注册,而不是使用 atexit ( 3 ) 。
有关 __cxa_atexit的完整技术细节,请参阅 Itanium C++ ABI规范 。
你的问题是什么版本的gcc,链接器和标准的C 库,你正在使用的问题。 此外,你提供的代码不符合 POSIX 标准,因为没有定义 RTDL_NOW 或者 RTDL_LOCAL 宏。 它们是 RTLD_NOW 和 RTLD_LOCAL ( 请参见 dlopen ) 。
如果C 标准库不支持 __cxa_atexit,则可能需要通过指定 -fno-use-cxa-atexit flag标志来禁用它:
-fuse-cxa-atexit
注册对象的析构函数与静态存储时间_ cxa atexit函数而不是atexit函数。 这里选项对于静态析构函数完全standards-compliant句柄是必需的,但只有当你的C 库支持__cxa_atexit时才能工作。
但这可能导致在不同的顺序调用析构函数,或者根本不调用析构函数。 所以在 __cxa_atexit 支持中断或者根本不支持的情况下最好的解决方案是不在共享库中使用带有析构函数的静态对象。
1、 在介绍静态对象、全局对象与程序的运行机制之间的关系之前,我们首先看一下atexit函数。
atexit函数的声明为:int atexit( void ( __cdecl *func )( void ) );
参数为函数指针,返回值为整型,0表示成功,其他表示失败。当程序运行结束时,他调用atexit函数注册的所有函数。如果多次调用atexit函数,那么系统将以LIFO(last-in-first-out)的方式调用所有的注册函数。
举例如下(代码摘自MSDN):
1#include
2#include
3
4void fn1( void ), fn2( void ), fn3( void ), fn4( void );
5
6void main( void )
7{
8 atexit( fn1 );
9 atexit( fn2 );
10 atexit( fn3 );
11 atexit( fn4 );
12 printf( "This is executed first.\n" );
13}
14
15void fn1()
16{
17 printf( "next.\n" );
18}
19
20void fn2()
21{
22 printf( "executed " );
23}
24
25void fn3()
26{
27 printf( "is " );
28}
29
30void fn4()
31{
32 printf( "This " );
33}
34
编译、运行程序后,程序的输出为:
This is executed first.
This is executed next.
注册函数的顺序为:fn1、fn2、fn3、fn4,但是调用顺序为fn4、fn3、fn2、fn1。
2、理解了atexit函数之后,我们就可以来看看局部静态对象了。
1 class AAA{ … } ;
2 AAA* createAAA()
3 {
4 static AAA a ;
5 return &a ;
6 }
7
在调试状态下,汇编代码如下(请观察蓝色标记出来的代码):
AAA* createAAA()
{
…
static AAA a ;
…
[1] 00401056 call AAA::AAA (4010A0h)
[2] 0040105B push offset createAAA'::
2’::a::dynamic atexit destructor' (442410h)
createAAA’::
[3] 00401060 call atexit (409A50h)
00401065 add esp,4
00401068 mov dword ptr [ebp-4],0FFFFFFFFh
return &a ;
0040106F mov eax,offset a (452620h)
}
…
00401091 ret
注:[1]、[2]、[3]为方便说明加入的字符,实际代码中并不存在。
[1]语句很明显的调用AAA的构造函数。
[2]语句将442410h压入栈中。
[3]语句调用atexit函数,根据我们的了解,atexit的参数应该是函数指针。那么我们来分析一下442410h处的代码,从注释来看,我们看到了destructor。代码如下:
2'::a::
dynamic atexit destructor’:
…
[1] 0044242E mov ecx,offset a (452620h)
[2] 00442433 call AAA::~AAA (403A90h)
…
0044244B ret
[1]语句将a的地址放在ecx寄存器中,这是this调用规范的风格。
[2]语句调用AAA的析构函数。
程序结束时,将调用atexit函数注册的442410h处的函数,进而调用了AAA的析构函数。从而保证了析构函数的调用。
3、 了解了局部静态对象之后,我们来看看全局对象。
我们知道全局对象必须在main函数前已经被构造。为了弄清楚全局对象何时被构造,我在全局对象的实例化处设置了断点,调用堆栈如下:
static.exe!aaaa::`dynamic initializer’() Line 22 C++
static.exe!_initterm(void (void)* * pfbegin=0x00451038, void (void)* * pfend=0x00451064) Line 707 C
static.exe!_cinit(int initFloatingPrecision=1) Line 208 + 0xf bytes C
static.exe!mainCRTStartup() Line 266 + 0x7 bytes C
作为对比,我在AAA的析构函数出设置了断点,调用堆栈如下:
static.exe!AAA::~AAA() Line 19 C++
static.exe!aaaa::`dynamic atexit destructor’() + 0x28 bytes C++
static.exe!doexit(int code=0, int quick=0, int retcaller=0) Line 451 C
static.exe!exit(int code=0) Line 311 + 0xd bytes C
static.exe!mainCRTStartup() Line 289 C
由此我们可以看出程序的实际入口点位mainCRTStartup而不是main函数(相对于ANSI的控制台程序而言)。
我们来分析一下_cinit函数:
注释中有一句[3. General C initializer routines],看来该函数的功能之一是完成C的初始化例程。
函数的核心代码如下:
/*
* do initializations
/
initret = _initterm_e( __xi_a, __xi_z );
/
* do C++ initializations
*/
_initterm( __xc_a, __xc_z );
看来该函数主要进行C、C++的初始化。我们进一步分析函数_initterm_e和_initterm,两个函数的功能进本相同,都是遍历函数指针(由参数指定函数指针的开始位置[__xi_a、__xi_z]、结束位置[__xc_a、__xc_z]),如果函数指针不为null,那么调用该函数。
那么__xi_a、__xi_z和__xc_a、__xc_z到底代表了什么呢?在cinitexe.c文件中有如下代码:
#pragma data_seg(“.CRT$XIA”)
_CRTALLOC(“.CRT$XIA”) _PVFV __xi_a[] = { NULL };
#pragma data_seg(“.CRT$XIZ”)
_CRTALLOC(“.CRT$XIZ”) _PVFV __xi_z[] = { NULL };/* C initializers */
#pragma data_seg(“.CRT$XCA”)
_CRTALLOC(“.CRT$XCA”) _PVFV __xc_a[] = { NULL };
#pragma data_seg(“.CRT$XCZ”)
_CRTALLOC(“.CRT$XCZ”) _PVFV __xc_z[] = { NULL };/* C++ initializers */
#pragma comment(linker, “/merge:.CRT=.data”)
可以看出这四个变量分别在数据段.CRT$XIA、.CRT$XIZ、.CRT$XCA、.CRT$XCZ中。当连接器布局代码时,它按根据的名称,按照字母排序的规则,排列所有段。这样在段.CRT$XIA中的变量出现在段.CRT$XIZ所有变量之前,从而形成链表。对于.CRT$XCA、.CRT$XCZ数据段同理。最后这四个数据段被合并到.data数据段中。
再看看这些变量的类型,typedef void (__cdecl *_PVFV)(void); 所以这些变量组成了2个初始化函数指针链表。
调试过程中,看到__xc_a、__xc_z链表中,指向的初始化函数很多是构造函数,如:
static std::_Init_locks initlocks;
static filebuf fout(_cpp_stdout);
extern _CRTDATA2 ostream cout(&fout);
cout对象也在此时被构造。
对于析构函数的调用也是采用相同的方式,只是此时每一种初始化,都有一种终止函数与之对应。
4、 总结
l 编译、连接程序时,编译器将所有全局对象的初始化函数放入.CRT$Xx中,连接器将所有的.CRT$XCx段合并成为.rdata数据段。在.CRT$XCA 到 .CRT$XCZ的所有段的数据组成初始化函数指针列表。
l 函数执行时,_initterm( __xc_a, __xc_z )函数调用所有的初始化函数。构造全局对象。构造对象完毕,调用atexit函数来保证析构函数的调用。Modern C++ Design就是通过控制调用atexit函数来决定对象的析构顺序的。
l 对于静态对象使用atexit来保证析构函数的调用。
l 程序结束时,调用exit来析构全局对象或静态对象。
std::atexit
C++ 工具库 程序支持工具
定义于头文件
(1)
int atexit( /*c-atexit-handler*/* func );
int atexit( /*atexit-handler*/* func );
(C++11 前)
int atexit( /*c-atexit-handler*/* func ) noexcept;
int atexit( /*atexit-handler*/* func ) noexcept;
(C++11 起)
extern "C++" using /*atexit-handler*/ = void(); // 仅为说明
extern "C" using /*c-atexit-handler*/ = void(); // 仅为说明
(2)
注册 func 所指向的函数,使得在正常程序中止(通过 std::exit() 或从 main 函数返回)时调用它。
可能在静态对象析构期间以逆序调用函数:若 A 先于 B 被注册,则对 B 的调用先进行于对 A 的调用。同样的规则应用于静态对象构造函数和到 atexit 的调用:见 std::exit 。
(C++11 前)
函数可能与拥有静态存储期的对象的析构函数,或彼此间并发调用,这保持保证若 A 的注册先序于 B 的注册,则对 B 的调用先序于对 A 的调用,同样的规则应用于静态对象构造函数和到 atexit 的调用:见 std::exit 。
(C++11 起)
可以注册同一函数多于一次。
若函数通过异常退出,则调用 std::terminate 。
atexit 是线程安全的:从数个线程调用函数不引入数据竞争。
保证实现支持注册至少 32 个函数。准确限制是实现定义的。
参数
func - 指向正常程序终止时要调用的函数的指针
返回值
若注册成功则为 0 ,否则为非零值。
注意
二个重载有别,因为参数 func 类型有别(语言链接是其类型的一部分)。
示例
运行此代码
#include
#include
void atexit_handler_1()
{
std::cout « “at exit #1\n”;
}
void atexit_handler_2()
{
std::cout « “at exit #2\n”;
}
int main()
{
const int result_1 = std::atexit(atexit_handler_1);
const int result_2 = std::atexit(atexit_handler_2);
if ((result_1 != 0) or (result_2 != 0)) {
std::cerr << "Registration failed\n";
return EXIT_FAILURE;
}
std::cout << "returning from main\n";
return EXIT_SUCCESS; } 输出:
returning from main
at exit #2
at exit #1
参阅
abort
导致非正常的程序终止(不进行清理)
(函数)
exit
导致正常的程序终止并进行清理
(函数)
quick_exit
(C++11)
导致快速程序终止,不进行完全的清理
(函数)
at_quick_exit
(C++11)
注册将于调用 quick_exit 时被调用的函数
(函数)
atexit 的 C 文档