先进后出原则,最先初始化的最后析构!

1.C++中全局对象、变量的构造函数调用顺序是跟声明有一定关系的,即在同一个文件中先声明的先调用。对于不同文件中的全局对象、变量,它们的构造函数调用顺序是未定义的,取决于具体的编译器

2.C++总是按成员变量在类声明中出现的顺序来初始化成员变量的,为什么C++不按初始化列表的顺序来初始化成员变量呢?因为我们知道初始化的顺序应该与析构的顺序相反,而对一个类来说 constructor 可能有多个,初始化列表也会有多个,所以C++就选择了简单的点的方法,按成员变量出现的顺序来初始化。

3.基类的静态变量先初始化,然后是它的派生类。直到所有的静态变量都被初始化。这里需要注意全局变量和静态变量的初始化是不分次序的。这也不难理解,其实静态变量和全局变量都被放在公共内存区。可以把静态变量理解为带有“作用域”的全局变量

顺序为:  1基类的静态变量或全局变量 2派生类的静态变量或全局变量 3基类的成员变量 4派生类的成员变量

C++标准中,处于同一编译单元(cpp)的全局对象按其声明次序初始化并倒序析构,但标准中没有说处于不同编译单元的全局对象的初始化顺序。这带来了很多问题。

假如有个Log对象负责程序日志的记录。如果程序结束时,有某个全局对象出现类似于资源释放失败的错误,该对象会调用Log记录错误,这时,Log可能已经被销毁了…… 这就是所谓的dead-reference问题。

本篇介绍的#pragama init_seg 预处理器指令提供了一种解决方案,在Effective C++ —— 让自己习惯C++(一)条款03中提供了另一种方案。

init_seg是C++的一条预处理pragma命令,用于C++全局变量与静态变量的初始化的定制。
C++程序中,全局变量、静态变量的初始化分两种。一种是静态初始化(static initialization),即用常量值来初始化,在程序被装入(load)的时候就完成了。另一种是动态初始化(dynamic initialization),简单地说就是需要调用一个函数来执行初始化,在C运行时刻库(CRT)的启动代码中完成这个工作,即在调用main或者WinMain函数之前已经完成全局变量或静态变量的初始化。本文讨论动态初始化。
 
处于同一编译单元(translate unit)中的全局变量、静态变量的动态初始化是按照出现顺序依次执行,程序结束时销毁全局变量、静态变量是按照其初始化顺序逆序执行,即后进先出(First-In-Last-Out)。但是,处于不同编译单元的全局变量、静态变量的动态初始化是按照什么顺序执行?一般说来,这是不能确定的,因此编写程序时一般不应该依赖跨编译单元的初始化顺序。
但是,有些情况下确实需要明确不同编译单元的全局变量、静态变量的初始化先后顺序。
 
例如,在一个类的构造函数(ctor)中通过流输出来显示一些文本信息:
ClassA::ClassA(){
cout<<"ClassA is constructing!"<<endl;
}
这就要求全局对象std::cout在用户的类初始化之前就已经可以工作了。
Visual C++、Intel C++等编译器为此提供了一套机制,使得编程者可以定制不同编译单元的全局变量、静态变量的初始化顺序。把全局变量、静态变量分为三组,分别称作compiler组、lib组、user组,初始化顺序依次由先至后。而程序结束时的全局变量、静态变量的销毁顺序为初始化顺序的逆序,即user组为先,lib组居中,compiler组最后。源程序中,使用预处理时的pragma命令(proprecossor Pragma Directives)来指定全局变量初始化的分组:
#pragma init_seg(compiler)
#pragma init_seg(lib)
#pragma init_seg(user)
每个编译单元最多只能有一条pragma init_seg语句,必须放在函数或类的声明、定义作用域以外,从该条pragma init_seg语句到编译单元结束范围内的全部的全局变量、静态变量都被编译器处理为具有相应的初始化分组。compiler组一般是C运行时刻库(C run-time library)使用,如流处理、伪随机数等等;lib组一般是第三方类库使用;user组供程序开发者使用。
编译与链接时的缺省行为是:
(1) 指向全局变量初始化代码的函数指针被放到二进制文件(obj或exe)的.CRT$XCU节中;
(2) 链接器会合并所有相同名字的节。当section名字中含有$符号时,链接器把$之前的名字做为最终的节名,$之后的字符串的字典顺序将作为相同名字的节合并时配列先后的依据;
(3) Visual C++的运行时库中已经定义了两个节:.CRT$XCA和.CRT$XCZ,并且在每个节中分别定义了一个变量:__xc_a和__xc_z。这样, .CRT$XCU中的数据以及__xc_a和__xc_z就构成一个函数指针表(function pointer table), __xc_a和__xc_z分别标识表头和表尾。C运行时刻库的_initterm函数遍历这个函数指针数组,依次调用每个函数指针,完成全局变量的初始化工作。(参考Visual C++所带的cinitexe.c文件)。
Visual C++也比照上述缺省机制,允许程序开发者自行指定全局变量、静态变量初始化的函数指针表所在的节(section),并且不再由编译器、链接器、C运行时库隐式完成初始化工作,而必须由程序自身显式完成这部分全局变量的初始化。使用[1]
#pragma init_seg("section-name", func-name)
从该语句到编译单元结束的所有全局变量、静态变量,其动态初始化的函数的指针放入section-name节,将由程序自身显式地依次遍历该节依次调用这些函数指针来完成初始化工作。上述pragma init_seg语句中的func-name形式参数是可选的,如果使用,func-name函数用来在一个全局函数指针数组上登记pragma init_seg语句作用范围内的全局变量、静态变量的析构函数。在编译时,依全局变量、静态变量初始化顺序,这些析构函数被编译器登记入一个全局的函数指针数组中保存。在程序结束前,逆序遍历该数组,依次显式调用这些析构函数,销毁pragma init_seg语句作用范围内的全局变量、静态变量。因此,func-name形参与C语言库函数atexit具有相同的规范(specification)与相似的功能。
下面是一个完整示例,可以在Visual C++编译下正确运行,但Intel C++编译后没有预期效果:
// pragma_directive_init_seg.cpp
#include <stdio.h>
#pragma warning(disable : 4075)
//定义一个保存全局对象析构函数的指针的数组,供程序结束前显示调用来销毁全局对象:
typedef void (__cdecl *PF)(void); //析构函数必须是无参数、无返回值
int cxpf = ; // 我们需要调用的析构函数的数量
PF pfx[]; // 保存析构函数的指针的数组.
//编译器调用该函数,把pragma init_seg语句作用范围内的全局变量的析构函数全部登记入dtor函数指针数组
int myexit (PF pf) {
pfx[cxpf++] = pf;
return ;
}
//全局对象的类型定义
struct A
{
A(int i)
{
value=i;
printf("A():%d\n",i);
}
~A()
{
printf("~A():%d\n",value);
}
int value;
};
// 由于在pragma init_seg语句之前,这个全局变量的ctor & dtor仍然由CRT启动代码来调用,
/* CRT startup code,就是在main函数(或者WinMain函数)之前和之后执行的代码,
完成运行环境初始化与事后的清理释放工作*/
A aaaa(); //下述四行语句,模仿了编译器与链接器默认构建全局变量初始化的.CRT的开头和结尾函数指针的行为方式,
//构建了用户自己的.mine节来保存全局变量初始化函数指针表的开头和结尾。
//节(Section)名字必须不多于8个字符;
//在$号之前具有相同字符串的节,将被链接器合并为同一个节,合并顺序依照$号之后字符串的字典序;
// InitSegStart与InitSegEnd作为边界值,使得我们可以发现那些真正的需被调用的全局变量初始化函数
#pragma section(".mine$a", read)
__declspec(allocate(".mine$a")) const PF InitSegStart = (PF);
#pragma section(".mine$z",read)
__declspec(allocate(".mine$z")) const PF InitSegEnd = (PF); // InitializeObjects是由程序显示调用执行,来初始化pragma init_seg语句作用下的那些全局变量
// 编译时,每个节是256个字符。链接器合并这些节时,以256个字节为单位,对于空闲的字节填入0
// 所以调用函数指针前,必须与0值做比较,排除这些0值
void InitializeObjects ()
{
const PF *x = &InitSegStart;
for (++x ; x < &InitSegEnd ; ++x)
if (*x) (*x)();
}
//DestroyObjects由程序显示调用执行,销毁pragma init_seg语句作用下的那些全局变量
void DestroyObjects ()
{
while (cxpf>)
{
--cxpf;
(pfx[cxpf])();
}
}
//从这条pragma init_seg语句起,所有全局变量、静态变量不再由编译器、链接器默认处理其初始化与析构,
//而是由程序自身来显示执行其初始化、析构:
#pragma init_seg(".mine$m", myexit)
A bbbb(); //全局变量
A cccc(); //全局变量
int main ()
{
InitializeObjects(); //显示初始化pragma init_seg语句作用下的那些全局变量
DestroyObjects(); //显示销毁pragma init_seg语句作用下的那些全局变量
} 程序运行结果:
A():
A():
A():
~A():
~A():
~A():

#pragma init_seg的更多相关文章

  1. #pragma详解

    在#Pragma是预处理指令它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#pragma指令对每个编译器给出了一个方法,在保持与C和C ++语言完全兼容的情况下,给出主机或操作系统专有 ...

  2. #pragma详细解释(一)

    #pragma详细解释 #pragma详细解释(一) 2010-04-18 14:21:00|  分类: 默认分类 |  标签: |字号大中小订阅     在#Pragma是预处理指令它的作用是设定编 ...

  3. 关于Pragma

    /** This is a introduction of how to use pragma. */ #pragma once /// This is used for include the he ...

  4. #pragma命令详解

    每种C和C++的实现支持对其宿主机或操作系统唯一的功能.例如,一些程序需要精确控制超出数据所在的储存空间,或着控制特定函数接受参数的方式.#pragma指示使每个编译程序在保留C和C++语言的整体兼容 ...

  5. 【转载】C++内存分配

    原文:C++内存分配 内存泄露相信对C++程序员来说都不陌生.解决内存泄露的方案多种多样,大部分方案以追踪检测为主,这种方法实现起来容易,使用方便,也比较安全. 首先我们要确定这个模块的主要功能: 能 ...

  6. VC++全局变量初始化

    目录 第1章说明    2 1.1 程序启动    2 1.2 强符号.弱符号    2 1.3 动态初始化顺序    3 1.4 exe调用dll    4 1.5 禁用动态初始化    4 1.6 ...

  7. [原] blade中C++ singleton的实现

    最近看了龚大大KalyGE中的singleton, 觉得非常不错(C++中线程安全并且高效的singleton). 可惜blade的代码都是C++03的, 没有使用C++11的任何特性. 笔者对于si ...

  8. [百度空间] [转] 在 Visual C++ 中控制全局对象的初始化顺序

    from: http://blog.csdn.net/classfactory/archive/2004/08/07/68202.aspx 在 C++ 中,同一个翻译单位(.cpp文件)里的全局对象的 ...

  9. 使用Visual Leak Detector检测内存泄漏[转]

      1.初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题 ...

随机推荐

  1. 关于Unity中DOTween插件的使用(专题一)

    DOTween flash里面的一个概念叫补间动画,DOTween就是干这个事情的. 补间动画:在1秒钟之内从A点移动到B点,在这个之间会把动画补间补好. 当我们安装好DOTween后,它就会提供很多 ...

  2. linux 将终端进行换行

    原始 修改 修改方法 vim .bashrc fi if [ "$color_prompt" = yes ]; then PS1='${debian_chroot:+($debia ...

  3. IDEA中 @override报错的解决方法

    今天用IDEA导入一个java工程时,碰上一个问题,代码中所有@override处标红,并提示:@override不支持对接口的实现. 网上百度了一下发现, 原因是引用JDK5版本中存在小bug的问题 ...

  4. 利用函数或映射进行数据转换 (map)

    先来看个数据 df = DataFrame({"food":["bacon", "pulled pork", "bacon&quo ...

  5. 【Python】#!/usr/bin/python 作用

    脚本语言的第一行,目的就是指出,你想要你的这个文件中的代码用什么可执行程序去运行它 #!/usr/bin/python是告诉操作系统执行这个脚本的时候,调用/usr/bin下的python解释器.#! ...

  6. 【转载】K-NN算法 学习总结

    声明:作者:会心一击 出处:http://www.cnblogs.com/lijingchn/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接, ...

  7. Prolog学习:用八卦的精神走进Prolog

    最近枕头书是<七周七语言:理解多种编程范型>这本,前面两章分别看了Ruby和IO,都是命令式语言.虽然它们在语法上跟之前接触过的C,C#,Java这些C家族的语言差别很大,但是编程范型却是 ...

  8. e770. 确定按钮租中已选的单选按钮

    When you ask a button group for the currently selected radio button, it returns the selected radio b ...

  9. SciTE配置信息

    超强文本编辑器SciTE配置方法详细实例 转载 2006年12月28日 17:07:00 标签: 文本编辑 / 文档 / 语言 / html / python / api 32800 关于scite文 ...

  10. (转)libhybris及EGL Platform-在Glibc生态中重用Android的驱动

    原文地址:http://blog.csdn.net/jinzhuojun/article/details/41412587 libhybris主要作用是为了解决libc库的兼容问题,目的是为了在基于G ...