深入理解C++中的初始化
C++经过这么多年的发展,已然成了一种文化和艺术,而这种艺术和文化并不是C++所固有的,是C++在各个方面的应用的总结和艺术化的结果。C++看起来比较复杂,但是深入其中你会发现C++是那么优美而富有哲学感。为了使C++更艺术化,C++语言大师们都为此而付出甚多,他们都在追求简单,追求编程的艺术。
几乎所有的编程语言都有这样或那样的初始化方法,比较新的语言如C#就将很多的初始化技术都集成到编译器里面去了,而很多编程语言都需要程序员用特定的方法去实现。C++的初始化技术很大程度上就依赖于程序员。本文介绍了几种常见的、实用的初始化技术。
1. 编译期初始化
所谓的编译期初始化,指的是符号的值在编译期就可以确定下来,一般包括普通常量的初始化和已经利用模板技术构造常量的初始化技术。
1.1. 常量
如const int Scale = 1;
enum ColorSpace{RGBColorSpace,CMYKColorSpace};
class Example{
static const int Count = 1;
}
1.2. 利用模板
这是模板元编程实用到的一个最基本的技术。这种技术利用编译器对模板进行递归编译的机理来实现某个常量的初始化。如:
template factorial;
template< > factorial<1>{enum {result=1};};
template< > factorial{enum {result=N* factorial ::result};};
2. 静态初始化
所谓的静态初始化,指的是程序启动的时候初始化全局(静态)变量或静态成员编程的行为。
2.1. 全局变量初始化
如int g_globalCount = 1;
static int g_s_globalCount = 1;
2.2. 静态成员变量的初始化
如class Example{
static Example NullExample;
}
Example Example::NullExample;
3. 动态初始化
这种初始化技术利用了类的构造和析构函数的基本特性。
如我们需要在程序启动的时候注册某个类的创建函数到类工厂,我们可以按照下面的方式进行做:
/////////////////////////////////////定义初始化所需要的宏等
typedef Geometry* (*CreateFuncPtr)();
struct auto_register{
auto_register(CreateFuncPtr func){//注册}
~auto_register(){//取消注册}
};
#define AUTO_REGISTER(class_name)\
static const auto_register _register## class_name ##( class_name::CreateGeometry);
//////////////////////////////////定义基于上面的初始化技术的实现
class Geometry{
public: static Geometry* CreateGeometry(){…}
}
class DiamondGeometry{
public: static DiamondGeometry* CreateGeometry(){…}
}
AUTO_REGISTER(DiamondGeometry)
class RectangleGeometry{
public: static RectangleGeometry* CreateGeometry(){…}
}
AUTO_REGISTER(RectangleGeometry)
来自实际项目的一段代码,简化形式如下:
switch (t)
{
case 0:
int a = 0;
break;
default:
break;
}
有什么问题吗?似乎没有。请用编译器编译一下……
嗯?!一个错误“error C2361: initialization of 'a' is skipped by 'default' label”。这怎么可能?
几番思琢,悟出解释:C++约定,在块语句中,对象的作用域从对象的声明语句开始直到块语句的结束,也就是说default标号后的语句是可以使用对象a的。如果程序执行时从switch处跳到default处,就会导致对象a没有被正确地初始化。确保对象的初始化可是C++的重要设计哲学,所以编译器会很严格地检查这种违例情况,像上述的示例代码中default语句后面并没有使用a,但考虑到以后代码的改动可能无意中使用,所以一样被封杀。
明白了原因,解决起来就很容易了。只要明确地限制对象a的作用域就行了。
switch (t)
{
case 0:
{ //added for fix problem
int a = 0;
break;
} //added for fix problem
default:
break;
}
如果确实需要在整个switch语句中使用对象a,那就把int a = 0;移到switch语句之前即可。不过从原先的语句看,其意图似乎并不是这样的,所以推荐前面的解决方案。
结束了吗?没有。让我们继续考究错误提示信息中“initialization”(也就是初始化)的确切含义。C++很看重初始化,所以往往会给我们造成一种错觉,似乎对象在定义处一定会经过初始化过程。真实情况如何呢?还是用实例来证明吧。
switch (t)
{
case 0:
int a;
a = 0;
break;
default:
break;
}
编译,这次没有报错。很明显int a;定义了对象,但没有进行初始化,否则就应该报告原先的错误。
再看看用户自定义类型。
class B
{
};
switch (t)
{
case 0:
B b;
break;
default:
break;
}
编译结果也没有错误,所以没有提供构造器的类仍然没有初始化过程。
如果给类加入构造器,情况就不同了。
class B
{
public: //added for initialization
B(){} //added for initialization
};
这样就能重现原先的错误。证明有了构造器,编译器就将进行初始化处理并对之进行安全检查。
从上面的实验,可以直观地体验到一些基本的C++观念和原理,并提高认识深度。
1.int a = 0;既是声明也是定义,还包括初始化;int a;是声明还是定义依上下文而定,但如果是定义就不会包括初始化;a = 0;仅仅是赋值语句,在此句前对象已经存在了。
2.为了避免不必要的开销,默认情况下,即程序员没有在代码中明确指示时,编译器不提供初始化过程。某些需要确保初始化的类,请提供构造器。这里透露出一个C++的设计哲学:通常你会面对多种选择,所以请精确地控制代码,其收益则是可以自由取舍调配的安全性、速度、内存开销等程序特性。
3.严密注意程序中标号的使用情况,特别是case、default等常规标号,否则他们可能会破坏对象的正确状态。如果提供了对象初始化,则能够获得编译器的额外帮助。