C++类构造函数初始化列表

初始化列表的定义

在使用C++编程的过程当中,常常需要对类成员进行初始化,通常的方法有两种:一种是构造函数内对类的成员赋值,一种则是使用初始化列表的构造函数显式的初始化类的成员。

构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:

1 class CExample 
 2 {
 3 public:
 4     int a;
 5     float b;
 6     //构造函数初始化列表
 7     CExample(): a(0),b(8.8) {}
 8     //构造函数内部赋值
 9     CExample()
10     {
11         a=0;
12         b=8.8;
13     }
14 };

从技术上说,用初始化列表来初始化类成员比较好,但是在大多数情况下,两者实际上没有什么区别。第二种语法被称为成员初始化列表,之所以要使用这种语法有两个原因:一个原因是必须这么做,另一个原因是出于效率考虑

初始化列表的必要性

初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。但在一些情况下,初始化列表可以做到构造函数做不到的事情:

1、类里面有const类型的成员,它是不能被赋值的,所以需要在初始化列表里面初始化它;

2、引用类型的成员(也就是名字成员,它作为一个现有名字的别名),也是需要在初始化列表里面初始化的,目的是为了生成了一个其名字成员在类外可以被修改而在内部是只读的对象;

3、需要调用基类的构造函数,且此基类构造函数是有参数的;

4、类里面有其他类类型的成员,且这个“其他类”的构造函数是有参数的。

举个例子:设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。

class CExampleOld { public: CExampleOld(int x) { ... } };

因为CExampleOld有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建CExampleOld的一个实例。

CExampleOld* pEO = new CExampleOld;   // 出错!!
CExampleOld* pEO = new CExampleOld(2); // OK

如果CExampleOld是另一个类的成员,你怎样初始化它呢?答案是你必须使用成员初始化列表。

class CExampleNew { CExampleOld m_EO; public: CExampleNew(); };
// 必须使用初始化列表来初始化成员 m_EO
//CExampleNew::CExampleNew() : m_EO(2) {……}

没有其它办法将参数传递给m_EO。

情况3和4其实一样的道理。如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。

初始化列表与构造函数赋值的效率比较

首先把数据成员按类型分类并分情况说明:

1、内置数据类型,复合类型(指针,引用)

在成员初始化列表和构造函数体内进行,两者在性能和结果上都是一样的

2、用户定义类型(类类型)

两者在结果上相同,但是性能上存在很大的差别。

因为编译器总是确保所有成员对象在构造函数体执行之前初始化,所以对于用户自定义类型(类),在初始化列表中只会调用类的构造函数,在构造函数体中赋值就会先调用一次类的构造函数,然后再调用一次类的赋值操作符函数。

显然后者在性能上有所损失,特别对于构造函数和赋值操作符都需要分配内存空间的情况,使用初始化列表,就可以避免不必要的多次内存分配。

举个例子:假定你有一个类CExample具有一个CString类型的成员m_str,你想把它初始化为"Hi,how are you."。你有两种选择:

1、使用构造函数赋值

CExample::CExample()
{
    // 使用赋值操作符 // CString::operator=(LPCTSTR);
    m_str = _T("Hi,how are you.");
}

2、使用初始化列表

CExample::CExample() : m_str(_T("Hi,how are you.")) {}

编译器总是确保所有成员对象在构造函数体执行之前被初始化,因此在第一个例子中编译的代码将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将"Hi,how are you."传递给这个函数。结果是在第一个例子中调用了两个CString函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。

在CString的例子里这是无所谓的,因为缺省构造函数是内联的,CString只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不必要的分配两次内存。

在内建类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:

x=y=z=0;

或者

memset(this, 0, sizeof(this));

初始化列表的成员初始化顺序

C++初始化类成员时,是按照成员声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。

因为一个类可以有多个构造函数,那么初始化列表可能各有不同,但是却只有一个析构函数,析构函数的析构顺序是和构造的顺序相反的。如果按照初始化列表来初始化,而且有多个构造函数的情况下,那么析构的时候就不能确定析构的顺序。只有按照声明的顺序,无论构造函数中初始化列表是何顺序,都可以按照确定的顺序析构。

保持一致性最主要的作用是避免以下类似情况的发生:

class CExample
{
    CExample(int x, int y);
    int m_x;
    int m_y;
};
CExample::CExample(int x, int y) : m_y(y), m_x(m_y){}

你可能以为上面的代码将会首先做m_y=y,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。结果是m_x将有一个不可预测的值。

有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
原文链接: https://www.cnblogs.com/happyhaoyun/archive/2011/08/15/4196287.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/30520

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 上午7:50
下一篇 2023年2月8日 上午7:52

相关推荐