揭示C++中全局类变量的构造与析构顺序
在完成《专业嵌入式软件开发 — 全面走向高质高效编程》一书后,我将下一本书的创作集点放在了基于C++的面象对象设计与开发上。从现在开始我将陆续推出关于C++和面高对象设计的博文。下面我们切入主题。
我们可以通过图1所示的示例程序观察到C++中一个关于全局类变量初始化顺序的有趣的现象。
class1.cpp
#include <iostream>
class class1_t
{
public:
class1_t ()
{
std::cout << "class1_t::class1_t ()" << std::endl;
}
};
static class1_t s_class1;
main.cpp
#include <iostream>
class class2_t
{
public:
class2_t ()
{
std::cout << "class2_t::class2_t ()" << std::endl;
}
};
static class2_t s_class2;
int main ()
{
return 0;
}
图1
示例程序分别在两个文件中定义了一个类和该类的一个静态全局变量,各类在其构造函数中输出其名。为了简单我们让main()函数的实现是空的。我们知道,全局类变量会在进入main()函数之前被构造好,且是在退出main()函数后才被析构。
图2示例了不同编译方法所获得可执行程序的运行结果。两种编译方法的区别是交换main.cpp和class1.cpp在编译命令中的顺序。从结果来看,示例程序内两个全局变量的构造顺序与文件编译时的位置有关。
$ g++ main.cpp class1.cpp -o example
$ ./example.exe
class1_t::class1_t ()
class2_t::class2_t ()
$ g++ class1.cpp main.cpp -o example
$ ./example.exe
class2_t::class2_t ()
class1_t::class1_t ()
图2
为什么会出现这样的有趣现象呢?我们需要了解编译器是如何处理全局类变量的,这需要查看编译器的源码和使用binutils工具集。
可以肯定的是,编译时的文件顺序会影响ld链接器对目标文件的处理顺序。让我们先了解ld链接器的默认链接脚本。通过图3的命令可以获得ld自带的链接脚本,图4例出了这里需要关心的脚本片断。
$ ld --verbose > ldscript
图3
ldscript
/* Script for ld --enable-auto-import: Like the default script except
read only data is placed into .data */
SECTIONS
{
/* Make the virtual address and file offset synced if the
alignment is lower than the target page size. */
. = SIZEOF_HEADERS;
. = ALIGN(__section_alignment__);
.text __image_base__ + ( __section_alignment__ < 0x1000 ? . : __section_alignment__ ) :
{
*(.init)
*(.text)
*(SORT(.text$*))
*(.text.*)
*(.glue_7t)
*(.glue_7)
___CTOR_LIST__ = .; __CTOR_LIST__ = . ;
LONG (-1);*(.ctors); *(.ctor); *(SORT(.ctors.*)); LONG (0);
___DTOR_LIST__ = .; __DTOR_LIST__ = . ;
LONG (-1); *(.dtors); *(.dtor); *(SORT(.dtors.*)); LONG (0);
*(.fini)
/* ??? Why is .gcc_exc here? */
*(.gcc_exc)
PROVIDE (etext = .);
*(.gcc_except_table)
}
……
}
图4
请注意脚本中的18~21行。这几行的作是将所有程序文件(包括目标文件和库文件)中的全局变量构造和析构函数的函数指针放入对应的数组中。从C++语言的角度来看,__CTOR_LIST__数组被用于存放全局类变量构造函数的指针,而__DTOR_LIST__数组被用于存放析构函数的。注意,对于构造函数数据,它是由各程序文件中的.ctors、.ctor和包含.ctors.的程序段组成的。此外,两个数据的第一项一定是-1,最后一项则一定是0。
通过查看gcc的源代码(g++的实现也位于其中),可以从gbl-ctors.h中看到两个数组的声明,从libgcc2.c文件中了解各全局类变量的构造与析构函数是如何被调用的,如图5所示。注意,这里示例的代码出于简化的目的有所删减。
gbl-ctors.h
typedef void (*func_ptr) (void);
extern func_ptr __CTOR_LIST__[];
extern func_ptr __DTOR_LIST__[];
#define DO_GLOBAL_CTORS_BODY \
do { \
unsigned long nptrs = (unsigned long) __CTOR_LIST__[0]; \
unsigned i; \
if (nptrs == (unsigned long)-1) \
for (nptrs = 0; __CTOR_LIST__[nptrs + 1] != 0; nptrs++); \
for (i = nptrs; i >= 1; i--) \
__CTOR_LIST__[i] (); \
} while (0)
libgcc2.c
void __do_global_dtors (void)
{
static func_ptr *p = __DTOR_LIST__ + 1;
while
补充:软件开发 , C++ ,