标签:
总所周知,C++对象在创建之时,会由构造函数进行一系列的初始化工作。以没有继承关系的单个类来看,除了构造函数本身的产生与指定,还涉及到初始化步骤,以及成员初始化方式等一些细节,本篇笔记主要对这些细节进行介绍,弄清C++对象在初始化过程中一些基本运行规则。
构造函数指定
通常,我们在设计一个类的时候,会为这个类编写对应的default constructor、copy constructor、copy assignment operator,还有一个deconstructor。即便我们仅仅编写一个空类,编译器在编译时仍旧会为其默认声明一个default constructor、copy constructor、copy assignment operator与deconstructor,如果在代码里面存在着它们的使用场景,那么这个时候编译器才会创建它们。
class MyCppClass { }
一旦我们为一个类编写了default constructor,那么编译器也就不会为其默认生成default constructor,对于其他几个函数也一样。对于编译器默认生成的constructor来说,它会以一定规则对每一个数据成员进行初始化。考虑到成员初始化的重要性,在编写自己的constructor时就需要严谨认真了,特别是在类的派生与继承情况下这点显得尤为重要。对于copy constructor和assignment operator的运用场景,这里不得不多说一点,见如下代码:
#include <iostream> using std::cout; using std::endl; class MyCppClass { public: MyCppClass() { std::cout <<"In Default Constructor!" <<std::endl; } MyCppClass(const MyCppClass& rhs) { std::cout <<"In Copy Constructor!" <<std::endl; } MyCppClass& operator= (const MyCppClass& rhs) { std::cout <<"In Copy Assignment Operator!" <<std::endl; return *this; } }; int main() { MyCppClass testClass1; // default constructor MyCppClass testClass2(testClass1); // copy constructor testClass1 = testClass2; // copy assignment operator MyCppClass testClass3 = testClass1; // copy constructor return 0; }
执行结果:
这里需要注意的是,一般情况下我们总是以为在‘=’运算符出现的地方都是调用copy assignment operator,上面这种情况却是个例外。也就是,当一个新对象被定义的时候,即便这个时候是使用了‘=‘运算符,它真实调用的是初始化函数copy constructor,而不是调用copy assignment operator去进行赋值操作。
Why初始化列表
一个对象在初始化时包括了两个步骤:首先,分配内存以保存这个对象;其次,执行构造函数。在执行构造函数的时候,如果存在有初始化列表,则先执行初始化列表,之后再执行构造函数的函数体。那么,为什么会引入初始化列表呢?
C++与C相比,在程序组织上由“以函数为基本组成单位的面向过程”变迁到“基于以类为中心的面向对象”,与此同时类也作为一种复合数据类型,而初始化列表无非就是进行一些数据的初始化工作。考虑到这里,也可以较为自然的推测初始化列表与类这种数据类型的初始化有着关联。
在引入初始化列表之后,一个类对应数据成员的初始化就存在有两种方式。下面是类的数据成员类型分别为内置类型、自定义类型时的一个对比。
// 数据成员类型为内置类型 class MyCppClass { public: // 赋值操作进行成员初始化 MyCppClass { counter = 0; } // 初始化列表进行成员初始化 MyCppClass : counter(0) { } private: int counter; }
当类的数据成员类型为内置类型时,上面两种初始化方式的效果一样。当数据成员的类型同样也为一个类时,初始化的过程就会有不一样的地方了,比如:
// 数据成员类型为自定义类型:一个类 class MyCppClass { public: // 赋值操作进行成员初始化 MyCppClass(string name) { counter = 0; theName = name; } // 初始化列表进行成员初始化 MyCppClass : counter(0), theName(name) { } private: int counter; string theName; }
在构造函数体内的theName = name这条语句,theName先会调用string的default constructor进行初始化,之后再调用copy assignment opertor进行拷贝赋值。而对于初始化列表来说,直接通过copy constructor进行初始化。明显起见,可以通过如下的代码进行测试。
#include <iostream> #include <string> class SubClass { public: SubClass() { std::cout <<" In SubClass Default Constructor!" <<std::endl; } SubClass(const SubClass& rhs) { std::cout <<" In SubClass Copy Constructor!" <<std::endl; } SubClass& operator= (const SubClass& rhs) { std::cout <<" In SubClass Copy Assignment Operator!" <<std::endl; return *this; } }; class BaseClass { public: BaseClass(const SubClass &rhs) { counter = 0; theBrother = rhs; std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const SubClass &rhs, int cnt):theBrother(rhs),counter(cnt) { std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const BaseClass& rhs) { std::cout <<" In BaseClass Copy Constructor!" <<std::endl; } BaseClass& operator= (const BaseClass& rhs) { std::cout <<" In BaseClass Copy Assignment Operator!" <<std::endl; return *this; } private: int counter; SubClass theBrother; }; int main() { SubClass subClass; std::cout <<"\nNo Member Initialization List: " <<std::endl; BaseClass BaseClass1(SubClass); std::cout <<"\nMember Initialization List: " <<std::endl; BaseClass BaseClass2(SubClass, 1); return 0; }
执行结果:
也就是,在涉及到自定义类型初始化的时候,使用初始化列表来完成初始化在效率上会有着更佳的表现。这也是初始化列表的一大闪光点。即便对于内置类型,在一些情况下也是需要使用初始化列表来完成初始化工作的,比如const、references成员变量。这里有篇笔记,对初始化列表有着非常详尽的描述。
几个初始化名词
在阅读《Accelerated C++》中文版时,总是碰到“缺省初始化”、“隐式初始化”以及“数值初始化”,最初在理解这几个名词的时候几费周折,总觉得为什么一个初始化操作造出了如此多的名词,为此没少花时间来弄清楚它们之间的关系。
为了更好的理解它们,先对C++当中的数据类型进行简单划分。在C++里面,数据类型大致可以分为两种:第一种是内置类型,比如float, int, double等;第二种是自定义类型,也就是我们常用的class, struct定义的类。在对这些类型的数据进行初始化时,差别就体现出来了:对于内置类型,在使用之前必须进行显示的初始化,而对于自定义类型,初始化责任则落在了构造函数身上。
int x = 0; // 显示初始化x SubClass subClass; // 依赖SubClass的default constructor进行初始化
上面的名词“缺省初始化”描述的就是当内置类型或者自定义类型的数据没有进行显示初始化时的一种初始化状态,而“隐式初始化”描述的是在该状态下面进行的具体操作方式,比如对于内置类型来说,缺省初始化状态下进行的隐式初始化实际上是未定义的,而自定义类型的隐式初始化则依赖于其constructor。
前面提到过C++不保证内置类型的初始化,但是当内置类型在作为一个类的成员时,在某些特定的条件下该内置类型的成员会被编译器主动进行初始化,对于这个过程也就是所谓的数值初始化。在《Accelerated C++》当中列出了如下的几种情况:
对象被用来初始化一个容器元素
为映射表添加一个新元素,对象是这个添加动作的副作用
定义一个特定长度的容器,对象为容器的元素
测试如下:
#include <iostream> #include <vector> #include <map> #include <string> using std::cout; using std::endl; using std::vector; using std::map; using std::string; class NumbericInitTestClass { public: void PrintCounter() { cout <<"counter = " <<counter <<endl; } private: int counter; }; int main() { NumbericInitTestClass tnc; tnc.PrintCounter(); map<string, int> mapTest; cout <<mapTest["me"] <<endl; vector<NumbericInitTestClass> vecNumbericTestClass(1); vecNumbericTestClass[0].PrintCounter(); return 0; }
对于没有进行初始化的内置类型,是一个未定义的值2009095316,而对于2, 3种情况来说,均被初始化为0,对于第1种情况我还没有想到合适的场景。
回过头想想,为了书中的一些相似的名词,去想办法把它们凑在一起总是显得有些牵强附会:)
一些规则
这里附上几条有关初始化的基本规则,它们多来源于《Effective C++》:
1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。
2. 构造函数最好使用成员初值列(member initialization list),而不要在构造函数体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中声明的次序相同。
3. C++不喜欢析构函数吐出异常。
4. 在构造函数与析构函数期间不要调用virtual函数,因为这类调用从不下降至derived class。
5. copying函数应该确保复制“对象内所有成员变量”及“所有base class成分”。
标签:
原文地址:http://my.oschina.net/u/2269715/blog/363284