·天新网首页·加入收藏·设为首页·网站导航
数码笔记本手机摄像机相机MP3MP4GPS
硬件台式机网络服务器主板CPU硬盘显卡
办公投影打印传真
家电电视影院空调
游戏网游单机动漫
汽车新车购车试驾
下载驱动源码
学院开发设计
考试公务员高考考研
业界互联网通信探索
您现在的位置:天新网 > 软件开发 > 开发语言 > C/C++
揭示C++中全局类变量的构造与析构顺序
http://www.21tx.com 2012年09月25日

在完成《专业嵌入式软件开发 — 全面走向高质高效编程》一书后,我将下一本书的创作集点放在了基于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设计模式,这一话题让我们留到另一篇博文中探讨。

本文出自 “李云” 博客,请务必保留此出处http://yunli.blog.51cto.com/831344/636281

上一篇: 在C++中通过模板规避潜在错误
下一篇: C语言学习之12个小问答

关于我们 | 联系我们 | 加入我们 | 广告服务 | 投诉意见 | 网站导航
Copyright © 2000-2011 21tx.com, All Rights Reserved.
晨新科技 版权所有 Created by TXSite.net