在完成《專業嵌入式軟件開發 — 全面走向高質高效編程》一書後,我將下一本書的創作集點放在了基於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設計模式,這一話題讓我們留到另一篇博文中探討。
本文出自 “李雲” 博客