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

在完成《專業嵌入式軟件開發  全面走向高質高效編程》一書後,我將下一本書的創做集點放在了基於C++的面象對象設計與開發上。從如今開始我將陸續推出關於C++和麪高對象設計的博文。下面咱們切入主題。
 
咱們能夠經過 1所示的示例程序觀察到C++中一個關於全局類變量初始化順序的有趣的現象。 
  
  
  
  
  1. class1.cpp                                                                         
  2. #include <iostream>                                                  
  3.                                                                                
  4. class class1_t                                                          
  5. {                                                                             
  6. public
  7.     class1_t () 
  8.     { 
  9.         std::cout << "class1_t::class1_t ()" << std::endl; 
  10.     } 
  11. }; 
  12. static class1_t s_class1; 
  13. main.cpp 
  14. #include <iostream> 
  15. class class2_t 
  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.     return 0; 
圖1
 
示例程序分別在兩個文件中定義了一個類和該類的一個靜態全局變量,各種在其構造函數中輸出其名。爲了簡單咱們讓main()函數的實現是空的。咱們知道,全局類變量會在進入main()函數以前被構造好,且是在退出main()函數後才被析構。
 
 2示例了不一樣編譯方法所得到可執行程序的運行結果。兩種編譯方法的區別是交換main.cppclass1.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 () 
  圖2
 
爲何會出現這樣的有趣現象呢?咱們須要瞭解編譯器是如何處理全局類變量的,這須要查看編譯器的源碼和使用binutils工具集。
 
能夠確定的是,編譯時的文件順序會影響ld連接器對目標文件的處理順序。讓咱們先了解ld連接器的默認連接腳本。經過 3的命令能夠得到ld自帶的連接腳本, 4例出了這裏須要關心的腳本片段。 
  
  
  
  
  1. ld --verbose > ldscript 
圖3 
  
  
  
  
  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.     /* Make the virtual address and file offset synced if the 
  6.         alignment is lower than the target page size. */ 
  7.    . = SIZEOF_HEADERS; 
  8.     . = ALIGN(__section_alignment__); 
  9.    .text __p_w_picpath_base__ + ( __section_alignment__ < 0x1000 ? . : __section_alignment__ ) : 
  10.    { 
  11.         *(.init) 
  12.         *(.text) 
  13.         *(SORT(.text$*)) 
  14.         *(.text.*) 
  15.         *(.glue_7t) 
  16.         *(.glue_7) 
  17.         ___CTOR_LIST__ = .; __CTOR_LIST__ = . ; 
  18.         LONG (-1);*(.ctors); *(.ctor); *(SORT(.ctors.*)); LONG (0); 
  19.         ___DTOR_LIST__ = .; __DTOR_LIST__ = . ; 
  20.         LONG (-1); *(.dtors); *(.dtor); *(SORT(.dtors.*)); LONG (0); 
  21.         *(.fini) 
  22.         /* ??? Why is .gcc_exc here? */ 
  23.         *(.gcc_exc) 
  24.         PROVIDE (etext = .); 
  25.         *(.gcc_except_table) 
  26.     } 
  27.     …… 
 圖4
 
請注意腳本中的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.  
  4. extern func_ptr __CTOR_LIST__[]; 
  5. extern func_ptr __DTOR_LIST__[]; 
  6.  
  7. #define DO_GLOBAL_CTORS_BODY \ 
  8.     do { \ 
  9.         unsigned long nptrs = (unsigned long) __CTOR_LIST__[0]; \ 
  10.         unsigned i; \ 
  11.         if (nptrs == (unsigned long)-1) \ 
  12.             for (nptrs = 0; __CTOR_LIST__[nptrs + 1] != 0; nptrs++); \ 
  13.         for (i = nptrs; i >= 1; i--) \ 
  14.             __CTOR_LIST__[i] (); \ 
  15.     } while (0) 
  16. libgcc2.c 
  17. void __do_global_dtors (void
  18.     static func_ptr *p = __DTOR_LIST__ + 1; 
  19.     while (*p) { 
  20.         p++; 
  21.         (*(p-1)) (); 
  22.     } 
  23.  
  24. void __do_global_ctors (void
  25.     DO_GLOBAL_CTORS_BODY; 
  26.     atexit (__do_global_dtors); 
圖5 
 
結合圖中的兩個文件能夠知曉,全局類變量的構造函數是經過__do_global_ctors()函數來調用的。從DO_GLOBAL_CTORS_BODY宏的實現來看,在1112行得到數組中構造函數的個數,並在1314行以逆序的方式調用每個構造函數。__do_global_ctors()函數在最後調用C庫的atexit()函數註冊__do_gloabl_dtors()函數,使得程序退出時該函數得以被調用。

__do_global_dtors()函數的實現來看,各全局變量的析構函數是順序調用的,與調用構造函數的順序是相反的。這就保證作到「先構造的全局類變量後析構。」 __do_gloable_ctors() __do_gloable_dtors()函數的調用是由C++語言的環境構建代碼來調用的。總的說來,它們分別在進入和退出main()函數時被調用。
 
咱們能夠藉助binutils工具集中的objdump來印證前面所述內容。 6示例了class1.o目標文件的反彙編代碼。讀者不須要細讀其中的彙編代碼,但請留意位置爲4a66的兩個函數。前者是class1.cpp文件中s_class1變量的析構函數,後者則是對應的構造函數。
  
  
  
  
  1. g++ -c –g class1.cpp 
  2. objdump -S -d --demangle=gnu-v3 class1.o 
  3.   
  4. class1.o:     file format pe-i386 
  5.   
  6.   
  7. Disassembly of section .text: 
  8.   
  9. ……內容有刪減…… 
  10. 0000004a <global destructors keyed to class1.cpp>: 
  11.   4a:   55                      push   %ebp 
  12.   4b:   89 e5                   mov    %esp,%ebp 
  13.   4d:   83 ec 08                sub    $0x8,%esp 
  14.   50:   c7 44 24 04 ff ff 00    movl   $0xffff,0x4(%esp) 
  15.   57:   00 
  16.   58:   c7 04 24 00 00 00 00    movl   $0x0,(%esp) 
  17.   5f:   e8 9c ff ff ff          call   0 
  18.   64:   c9                      leave 
  19.   65:   c3                      ret 
  20.   
  21. 00000066 <global constructors keyed to class1.cpp>: 
  22.   66:   55                      push   %ebp 
  23.   67:   89 e5                   mov    %esp,%ebp 
  24.   69:   83 ec 08                sub    $0x8,%esp 
  25.   6c:   c7 44 24 04 ff ff 00    movl   $0xffff,0x4(%esp) 
  26.   73:   00 
  27.   74:   c7 04 24 01 00 00 00    movl   $0x1,(%esp) 
  28.   7b:   e8 80 ff ff ff          call   0 
  29.   80:   c9                      leave 
  30.   81:   c3                      ret 
  31.   82:   90                      nop 
  32.   83:   90                      nop 
圖6 

 7示例瞭如何經過objdump工具查看class1.o文件中.ctors.dtors段中的內容。從內容中能夠看到存在前面提到的4a66兩個值,而這兩個值會最終被ld連接器分別放入__CTOR_LIST____DTOR_LIST__數組中。ios

  
  
  
  
  1. objdump -s -j .ctors class1.o 
  2.   
  3. class1.o:     file format pe-i386 
  4.   
  5. Contents of section .ctors: 
  6.  0000 66000000                             f... 
  7. objdump -s -j .dtors class1.o 
  8.   
  9. class1.o:     file format pe-i386 
  10.   
  11. Contents of section .dtors: 
  12.  0000 4a000000                             J... 
圖7 
瞭解了編譯器是如何處理全局類對象的構造和析構函數後,咱們就不難理解開始提到的有趣現象了。這是由於文件編譯時的位置順序會最終影響各種全局變量的構造與析構函數在__CTOR_LIST____DTOR_LIST__數組中的前後順序。 
 
瞭解這一內容有什麼意義呢?這有助於咱們掌握如何在C++中正確實現singleton設計模式,這一話題讓咱們留到另外一篇博文中探討。
相關文章
相關標籤/搜索