揭示C++中全局類變量的構造與析構順序

 在完成《專業嵌入式軟件開發 — 全面走向高質高效編程》一書後,我將下一本書的創作集點放在了基於C++的面象對象設計與開發上。從現在開始我將陸續推出關於C++和麪高對象設計的博文。下面我們切入主題。

我們可以通過代碼 1所示的示例程序觀察到C++中一個關於全局類變量初始化順序的有趣的現象。
  1. class1.cpp
  2. #include <iostream>
  3. class class1_t
  4. {
  5. public:
  6. class1_t ()
  7. {
  8. std::cout << "class1_t::class1_t ()" << std::endl;
  9. }
  10. };
  11. static class1_t s_class1;
  12. main.cpp
  13. #include <iostream>
  14. class class2_t
  15. {
  16. public:
  17. class2_t ()
  18. {
  19. std::cout << "class2_t::class2_t ()" << std::endl;
  20. }
  21. };
  22. static class2_t s_class2;
  23. int main ()
  24. {
  25. return 0;
  26. }

代碼 1
示例程序分別在兩個文件中定義了一個類和該類的一個靜態全局變量,各類在其構造函數中輸出其名。爲了簡單我們讓main()函數的實現是空的。我們知道,全局類變量會在進入main()函數之前被構造好,且是在退出main()函數後才被析構。
代碼 2示例了不同編譯方法所獲得可執行程序的運行結果。兩種編譯方法的區別是交換main.cpp和class1.cpp在編譯命令中的順序。從結果來看,示例程序內兩個全局變量的構造順序與文件編譯時的位置有關。
  1. $ g++ main.cpp class1.cpp -o example
  2. $ ./example.exe
  3. class1_t::class1_t ()
  4. class2_t::class2_t ()
  5. $ g++ class1.cpp main.cpp -o example
  6. $ ./example.exe
  7. class2_t::class2_t ()
  8. class1_t::class1_t ()
爲什麼會出現這樣的有趣現象呢?我們需要了解編譯器是如何處理全局類變量的,這需要查看編譯器的源碼和使用binutils工具集。
可以肯定的是,編譯時的文件順序會影響ld鏈接器對目標文件的處理順序。讓我們先了解ld鏈接器的默認鏈接腳本。通過代碼 3的命令可以獲得ld自帶的鏈接腳本,代碼 4例出了這裏需要關心的腳本片斷。
  1. $ ld --verbose > ldscript
  1. ldscript
  2. /* Script for ld --enable-auto-import: Like the default script except
  3. read only data is placed into .data */
  4. SECTIONS
  5. {
  6. /* Make the virtual address and file offset synced if the
  7. alignment is lower than the target page size. */
  8. . = SIZEOF_HEADERS;
  9. . = ALIGN(__section_alignment__);
  10. .text __image_base__ + ( __section_alignment__ < 0x1000 ? . : __section_alignment__ ) :
  11. {
  12. *(.init)
  13. *(.text)
  14. *(SORT(.text$*))
  15. *(.text.*)
  16. *(.glue_7t)
  17. *(.glue_7)
  18. ___CTOR_LIST__ = .; __CTOR_LIST__ = . ;
  19. LONG (-1);*(.ctors); *(.ctor); *(SORT(.ctors.*)); LONG (0);
  20. ___DTOR_LIST__ = .; __DTOR_LIST__ = . ;
  21. LONG (-1); *(.dtors); *(.dtor); *(SORT(.dtors.*)); LONG (0);
  22. *(.fini)
  23. /* ??? Why is .gcc_exc here? */
  24. *(.gcc_exc)
  25. PROVIDE (etext = .);
  26. *(.gcc_except_table)
  27. }
  28. ……
  29. }
請注意腳本中的18~21行。這幾行的作是將所有程序文件(包括目標文件和庫文件)中的全局變量構造和析構函數的函數指針放入對應的數組中。從C++語言的角度來看,__CTOR_LIST__數組被用於存放全局類變量構造函數的指針,而__DTOR_LIST__數組被用於存放析構函數的。注意,對於構造函數數據,它是由各程序文件中的.ctors、.ctor和包含.ctors.的程序段組成的。此外,兩個數據的第一項一定是-1,最後一項則一定是0。
通過查看gcc的源代碼(g++的實現也位於其中),可以從gbl-ctors.h中看到兩個數組的聲明,從libgcc2.c文件中瞭解各全局類變量的構造與析構函數是如何被調用的,如代碼 5所示。注意,這裏示例的代碼出於簡化的目的有所刪減。
  1. gbl-ctors.h
  2. typedef void (*func_ptr) (void);
  3. extern func_ptr __CTOR_LIST__[];
  4. extern func_ptr __DTOR_LIST__[];
  5. #define DO_GLOBAL_CTORS_BODY \
  6. do { \
  7. unsigned long nptrs = (unsigned long) __CTOR_LIST__[0]; \
  8. unsigned i; \
  9. if (nptrs == (unsigned long)-1) \
  10. for (nptrs = 0; __CTOR_LIST__[nptrs + 1] != 0; nptrs++); \
  11. for (i = nptrs; i >= 1; i--) \
  12. __CTOR_LIST__[i] (); \
  13. } while (0)
  14. libgcc2.c
  15. void __do_global_dtors (void)
  16. {
  17. static func_ptr *p = __DTOR_LIST__ + 1;
  18. while (*p) {
  19. p++;
  20. (*(p-1)) ();
  21. }
  22. }
  23. void __do_global_ctors (void)
  24. {
  25. DO_GLOBAL_CTORS_BODY;
  26. atexit (__do_global_dtors);
  27. }
結合代碼中的兩個文件可以知曉,全局類變量的構造函數是通過__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變量的析構函數,後者則是對應的構造函數。
  1. $ g++ -c –g class1.cpp
  2. $ objdump -S -d --demangle=gnu-v3 class1.o
  3. class1.o: file format pe-i386
  4. Disassembly of section .text:
  5. ……內容有刪減……
  6. 0000004a <global destructors keyed to class1.cpp>:
  7. 4a: 55 push %ebp
  8. 4b: 89 e5 mov %esp,%ebp
  9. 4d: 83 ec 08 sub $0x8,%esp
  10. 50: c7 44 24 04 ff ff 00 movl $0xffff,0x4(%esp)
  11. 57: 00
  12. 58: c7 04 24 00 00 00 00 movl $0x0,(%esp)
  13. 5f: e8 9c ff ff ff call 0
  14. 64: c9 leave
  15. 65: c3 ret
  16. 00000066 <global constructors keyed to class1.cpp>:
  17. 66: 55 push %ebp
  18. 67: 89 e5 mov %esp,%ebp
  19. 69: 83 ec 08 sub $0x8,%esp
  20. 6c: c7 44 24 04 ff ff 00 movl $0xffff,0x4(%esp)
  21. 73: 00
  22. 74: c7 04 24 01 00 00 00 movl $0x1,(%esp)
  23. 7b: e8 80 ff ff ff call 0
  24. 80: c9 leave
  25. 81: c3 ret
  26. 82: 90 nop
  27. 83: 90 nop
代碼 7示例瞭如何通過objdump工具查看class1.o文件中.ctors和.dtors段中的內容。從內容中可以看到存在前面提到的4a和66兩個值,而這兩個值會最終被ld鏈接器分別放入__CTOR_LIST__和__DTOR_LIST__數組中。
  1. $ objdump -s -j .ctors class1.o
  2. class1.o: file format pe-i386
  3. Contents of section .ctors:
  4. 0000 66000000 f...
  5. $ objdump -s -j .dtors class1.o
  6. class1.o: file format pe-i386
  7. Contents of section .dtors:
  8. 0000 4a000000 J...
瞭解了編譯器是如何處理全局類對象的構造和析構函數後,我們就不難理解開始提到的有趣現象了。這是因爲文件編譯時的位置順序會最終影響各類全局變量的構造與析構函數在__CTOR_LIST__和__DTOR_LIST__數組中的先後順序。
瞭解這一內容有什麼意義呢?這有助於我們掌握如何在C++中正確實現singleton設計模式,這一話題讓我們留到另一篇博文中探討。
 
總之,VC6.0實踐證明:
 
1、不同文件中,先編譯的文件中的全局變量先構造;
2、在一個文件中,上面定義的全局變量先構造。
 
本文出自 “李雲” 博客,請務必保留此出處http://yunli.blog.51cto.com/831344/636281
發佈了19 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章