C/C++:对象/变量初始化相关
一、 直接初始化和复制初始化
1.定义:直接初始化使用"( )"符号,如:string s("hello");复制初始化使用"="符号,如:string s="hello".
2.区别:对于内置类型,直接初始化和复制初始化没有差别:其操作都是"提供一个值,并且把这个值复制到新定义的对象中"(《C++ Primer》).
对于类类型,直接初始化根据参数类型调用相应构造函数,复制初始化调用拷贝构造函数,对于以下初始化:
String s1("hello");//String是自定义的字符串类 String s2="hello";
s1的初始化调用String的String(char*)构造函数直接对s1进行初始化,s2的初始化则是先调用String(char*)构造函数构造一临时对象,再调用拷贝构造函数String(const String&)对s2进行初始化.也就是说,如果复制初始化的参数不是同类型的数值,就会多一次临时对象的构造和析构成本.
(如果实验验证,会发现两行代码都调用String(char*)构造函数,这是因为大多数编译器都实行RVO/NRVO((具名)返回值优化)从而避免了临时对象的产生,但了解实际过程,写出不依赖于编译器优化的代码也是很重要的)
二、全局/静态变量(对象)的初始化
(参考自:http://bbs.csdn.net/topics/390527051?page=1
http://www.tuicool.com/articles/QRBF3qN)
1.全局变量的初始化分为静态初始化和动态初始化:
静态初始化:编译期进行的初始化,所谓编译期进行的初始化,即在编译期直接将数据放在程序虚拟地址空间的数据段中,因此静态初始化在程序加载到内存时完成.静态初始化又分为 zero-initialization(零初始化)和constant initialization(常量初始化),zero-inltilization指的是对于没有指明初始化式的全局对象,就由编译器用0初始化,并存储在程序的.BSS段中(由于初始化为0,因此实际上不需要占用空间,在加载到内存时直接初始化为0即可,所以.BSS段的段内容长度为0),const inilitization指的是对于指明常量初始化式的全局对象, 就由指明的初始化式进行初始化.
所有全局对象都会发生静态初始化:指明常量初始化式的进行constant initilization,未指明初始化式的进行zero-initilization,指明非常量初始化式的也会进行zero-initilization(然后在运行期进行动态初始化)
动态初始化:运行时进行的初始化,所谓运行时进行的初始化,不是指在main函数中,事实上,操作系统加载完程序之后,会有默认入口(比如 mainCRTStartup(void) /wmainCRTStartup(void)/WinMainCRTStartup(void)/wWinMainCRTStartup(void)等),visual C++下是mainCRTStartup(void),mainCRTStartup的任务之一就是进行相关初始化操作,然后调用main函数,因此动态初始化发生在mainCRTStartup执行中,main函数执行前.
指明非常量初始化式的全局对象进行动态初始化,包括需要调用函数的和用其他全局对象初始化的.例如string s,int a=b;
举例:
int a; //静态初始化为0 struct Point{ int x; int y; } temp={1,2};//静态初始化x,y为1,2 int b=a;//静态初始化为0,然后动态初始化为a,稍后会说明只有c++允许 string s;//先进行动态初始化,然后调用默认构造函数进行动态初始化
2. C和C++的全局对象初始化
(摘抄自http://www.cnblogs.com/catch/p/4314256.html)
C语言只支持静态初始化,也就是说变量和结构体只能进行zero-initilization或constant initilization,因此像int a=b是不允许的.
由于C++引入了类的概念,因此对象可能经由构造函数进行初始化,全局对象先进行zero-initilization,然后在mainCRTStartup中调用构造函数进行动态初始化(具有结构体性质的对象也可以采用C语言类型的初始化),此外C++也支像int b=a的初始化,其过程和对象初始化相同:先进行zero-initilization,然后在mainCRTStartup将b置为a.
3. C++全局对象初始化顺序
C/C++都支持多文件编译,由于C语言全局对象的初始化为静态初始化,因此程序加载到内存中时所有全局对象就都被初始化完成,也就不涉及初始化顺序问题.但对于C++,由于其支持动态初始化,因此在运行时进行的全局对象初始化就涉及顺序问题:
对于同一编译单元的全局对象,其初始化顺序与声明顺序一致(销毁的顺序相反),但对于不同编译单元的全局对象,c++ 标准并没有规定它们之间的初始化顺序(以及销毁顺序),实现上就由编译器决定,一个比较普遍的认识是:不同编译单元间的全局变量的初始化顺序是不固定的,哪怕对同一个编译器,同一份代码,任意两次编译的结果都有可能不一样.因此如果某个文件的全局对象的初始化依赖于另一个文件的全局对象的值,由于其初始化顺序的不固定,前者可能不能被正确初始化,从而引发错误.
因此C++编程的原则之一就是尽量避免全局对象的使用(也利于节省堆栈空间),即使使用全局对象也要尽量避免初始化之间的依赖关系,即使全局变量的初始化之间存在依赖关系也尽量限制在同一编译单元,但由于种种原因(例如多个文件对于cout对象的使用),仍然需要处理全局对象初始化顺序的问题.4将介绍处理全局对象初始化顺序的方法.
4. 处理全局对象初始化顺序的技巧
1). Construct On First Use
将对全局对象的引用改为函数调用,同时把全局对象改为函数的static对象,由于函数的static对象在函数第一次调用时被初始化,因此可以保证通过函数调用引用全局对象时对象 已被初始化. 例如:
int get_global_x(){ static X x; return x.Value(); }
这种方法可以解决全局对象解决全局变量未初始化就被引用的问题,但是由于静态对象(属于全局对象)的析构顺序仍然不能确定,因此如果X被析构但get_global_X还被调用,就会出现错误,可能的解决方法像这样:
int get_global_x() { static X* x = new X; return x->Value(); }
但又会产生内存泄露问题,如果X的析构函数还有其他事情要做,由此引发的错误更严重!
2). Nifty counter
"Nifty counter的原理是通过头文件引用,在所有需要引用 x 的地方都增加一个 static 全局变量,然后在该 static 变量的构造函数里初始化我们所需要引用的全局变量 x,在其析构函数里再清理x".这也是GCC采用的办法.
示例如下:
// global.h #ifndef _global_h_ #define _global_h_ extern X x; class initializer { public: initializer() { if (s_counter_++ == 0) init(); } ~initializer() { if (--s_counter_ == 0) clean(); } private: void init(); void clean(); static int s_counter_; }; static initializer s_init_val; #endif
//"global.cpp" #include "global.h" static X x; int initializer::s_counter_ = 0; void initializer::init() { new(&x) X; } void initializer::clean() { (&x)->~X(); }
initializatizer的实现有点类似于智能指针类,只不过在这里它起一个"全局对象管理类"的作用:确保全局对象x第一次使用的时候已经被初始化,并且在没有对象引用该全局对象时析构该对象.其中有几点值得注意:由于s_counter_ 的初始化是静态初始化,因此能保证在程序加载后执行前就初始化完成;初始化x采用初始化 x 用到了placement new的技巧,而析构则直接调用析构函数.
至此,改进还未结束,以上代码仍存在问题:由于在initializatizer中手动调用x的构造函数,因此程序在初始化x所在编译单元时,会第二次调用x的构造函数,同理析构函数也会被调用两次,这很可能会引发问题,解决方法就是将x改为引用:
// global.cpp #include "global.h" // need to ensure memory alignment?? static char g_dummy[sizeof(X)]; static X& x = reinterpret_cast<X&>(g_dummy); int initializer::s_counter_ = 0; void initializer::init() { new(&x) X; } void initializer::clean() { (&x)->~X(); }
由于x只是一个引用,编译器不会调用x的构造函数和析构函数.
至此,这个方案已经比较完美,但它还需要一个成功运行的条件:所有引用x的地方都会include头文件global.h(从而保证s_init_val已定义),"如果某一个全局变量 y 的初始化函数里没有直接引用 x, 而是间接调用了另一个函数 foo,再通过 foo 引用了 x,此时就可能出错了,因为 y 所在的编译单元里可能并没有直接引用 x,因此很有可能就没有 include 头文件global.h,那么 y 的初始化就很有可能发生在 x 之前",在此情况下,此方案没能发挥作用.
"这个问题在 gcc c++ 的标准库里也没有得到解决,有兴趣的可以看看这个讨论".