在完成《专业嵌入式软件开发 — 全面走向高质高效编程》一书后,我将下一本书的创作集点放在了基于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 (*p) {
- p++;
- (*(p-1)) ();
- }
- }
-
- void __do_global_ctors (void)
- {
- DO_GLOBAL_CTORS_BODY;
- atexit (__do_global_dtors);
- }
图5 结合图中的两个文件可以知晓,全局类变量的构造函数是通过__do_global_ctors()函数来调用的。从DO_GLOBAL_CTORS_BODY宏的实现来看,在11和12行获得数组中构造函数的个数,并在13和14行以逆序的方式调用每一个构造函数。__do_global_ctors()函数在最后调用C库的atexit()函数注册__do_gloabl_dtors()函数,使得程序退出时该函数得以被调用。 从__do_global_dtors()函数的实现来看,各全局变量的析构函数是顺序调用的,与调用构造函数的顺序是相反的。这就保证做到“先构造的全局类变量后析构。” 对__do_gloable_ctors() 和__do_gloable_dtors()函数的调用是由C++语言的环境构建代码来调用的。总的说来,它们分别在进入和退出main()函数时被调用。 我们可以借助binutils工具集中的objdump来印证前面所述内容。图 6示例了class1.o目标文件的反汇编代码。读者不需要细读其中的汇编代码,但请留意位置为4a和66的两个函数。前者是class1.cpp文件中s_class1变量的析构函数,后者则是对应的构造函数。 - $ g++ -c –g class1.cpp
- $ objdump -S -d --demangle=gnu-v3 class1.o
-
- class1.o: file format pe-i386
-
-
- Disassembly of section .text:
-
- ……内容有删减……
- 0000004a <global destructors keyed to class1.cpp>:
- 4a: 55 push %ebp
- 4b: 89 e5 mov %esp,%ebp
- 4d: 83 ec 08 sub $0x8,%esp
- 50: c7 44 24 04 ff ff 00 movl $0xffff,0x4(%esp)
- 57: 00
- 58: c7 04 24 00 00 00 00 movl $0x0,(%esp)
- 5f: e8 9c ff ff ff call 0
- 64: c9 leave
- 65: c3 ret
-
- 00000066 <global constructors keyed to class1.cpp>:
- 66: 55 push %ebp
- 67: 89 e5 mov %esp,%ebp
- 69: 83 ec 08 sub $0x8,%esp
- 6c: c7 44 24 04 ff ff 00 movl $0xffff,0x4(%esp)
- 73: 00
- 74: c7 04 24 01 00 00 00 movl $0x1,(%esp)
- 7b: e8 80 ff ff ff call 0
- 80: c9 leave
- 81: c3 ret
- 82: 90 nop
- 83: 90 nop
图6 图 7示例了如何通过objdump工具查看class1.o文件中.ctors和.dtors段中的内容。从内容中可以看到存在前面提到的4a和66两个值,而这两个值会最终被ld链接器分别放入__CTOR_LIST__和__DTOR_LIST__数组中。 - $ objdump -s -j .ctors class1.o
-
- class1.o: file format pe-i386
-
- Contents of section .ctors:
- 0000 66000000 f...
- $ objdump -s -j .dtors class1.o
-
- class1.o: file format pe-i386
-
- Contents of section .dtors:
- 0000 4a000000 J...
图7 了解了编译器是如何处理全局类对象的构造和析构函数后,我们就不难理解开始提到的有趣现象了。这是因为文件编译时的位置顺序会最终影响各类全局变量的构造与析构函数在__CTOR_LIST__和__DTOR_LIST__数组中的先后顺序。 了解这一内容有什么意义呢?这有助于我们掌握如何在C++中正确实现singleton设计模式,这一话题让我们留到另一篇博文中探讨。
|