c++中冒号(:)、双冒号(::)、初始化列表详解
最近学习了C++中⼀些特殊符号的使⽤规则,查阅了⼀些资料和博客,对初始化列表、(:),(::)的⽤法进⾏了梳理,如有理解不周的地⽅欢迎⼤家指正初始化列表
初始化列表其实就是类成员初始化列表(Member Initialization List)的简单说法。
1) 基本概念,初始化列表的形式
<span >#include <iostream>
using namespace std;
class MemberInitializationList
{
private:
int i;
int j;
public:
MemberInitializationList(int val) : j(val), i(j)        // j(val), i(j)就是所谓的成员初始化列表
{
}
inline void printInfo()
{
东昌汽车cout << "i = " << i << ", j = " << j << endl;
}
};
int main(void)
{
MemberInitializationList MIL(10);
MIL.printInfo();
return 0;
}</span>
运⾏结果:
<span >
i = -858993460  j = 10</span>
j如愿以偿被初始化为10,但是i的值为什么是⼀个奇怪的数字,⽽不是意想中的10呢?
答案是有些细微的地⽅需要注意:成员初始化列表的初始化顺序是由类中的成员声明次序决定的,⽽不是由initialization list中的排列次序决定的。在本例中,先初始化i然后再初始化j。initialization list中的i(j),表明将j的值赋给i,⽽此时j还没有被初始化,其值不确定,所以i的值也就不能确定,这就是运⾏结果中为什么i的值⽐较奇怪的原因了。
在任何explicit user code之前,编译器会⼀⼀操作initialization list,以适当次序在构造函数内安插初始化操作。
需要说明的是,据说除了g++编译器会对这种情况给予warning外,其它的编译器都不会给出相关的警告信息。
如果把本例中的构造函数改成:
<span >MemberInitializationList(int val) : i(val), j(i)
{
}</span>
再运⾏的结果就正确了:
i = 10 j = 10
2) 为什么要使⽤member initialization list?
根据Stanley Lippman的Inside C++ Object Model,采⽤member initialization list的⽅法的效率⽐较⾼,
MemberInitializationList(int val) : i(val), j(i)
{
帝豪ec7油耗
}
的效率要⽐
MemberInitializationList(int val)
{
i = val;
j = I;
}
⾼。理由是后者会产⽣临时性的变量,然后要调⽤赋值运算符赋给真正的变量,再然后摧毁那个临时性的变量。是否真的是这样呢?我们来做⼀个试验证明之。验证程序如下:
class MemberInitializationList
{
private:
int i;
int j;
public:
//*
MemberInitializationList(int val) : i(val), j(i)
{
}
//*/
/
*
MemberInitializationList(int val)
{
i = val;
j = i;
}
//*/
越野车十万左右
inline void printInfo()
{
cout << "i = " << i << ", j = " << j << endl;
}
};
int main(void)
{
//cout << CLOCKS_PER_SEC << endl;
clock_t start = clock();
for(int i = 0; i < 3000000; i++)
{
MemberInitializationList* pMIL = new MemberInitializationList(i);
delete pMIL;
}
clock_t finish = clock();
cout << finish - start << " ms elapsed." << endl;
return 0;
}中华酷宝gt
结论:在VC6和VC2005的编译器上,两种⽅式似乎没有什么区别。或许在别的编译器上有所区别。也就是说,要么微软的编译器对于两种构造函数都使⽤了临时变量,或者都没有使⽤临时变量。
3) 调⽤⼀个成员函数设定成员变量的初值
class MemberInitializationList
{
private:
int i;
int j;
public:指标更新
MemberInitializationList(int val) : i(setI(val)), j(i)                  // ⽤成员函数设定成员变量的初始值也是可以的
{
}
inline int setI(int i)
{
return i;
}
inline void printInfo()
{
cout << "i = " << i << ", j = " << j << endl;
}
};
int main(void)
{
MemberInitializationList MIL(10);
MIL.printInfo();
return 0;
}
每个成员在成员初始化表中只能出现⼀次初始化的顺序不是由名字在初始化表中的顺序决定⽽是由成员在类中被声明的顺序决定的,但是在初始化表中出现或者在被隐式初始化的成员类对象中的成员总是在构造函数体内成员的赋值之前被初始化,初始化列表,跟在{}⾥⾯的初始化没有什么不同,但在⾮静态const类型以及引⽤型成员变量必须在初始化列表⾥⾯初始化越野拖拉机
在使⽤C++编程的过程当中,常常需要对类成员进⾏初始化,常⽤的2种⽅法:
第⼀种⽅法:
CMYClass::CSomeClass() { x=0; y=1; }
第⼆种⽅法:
CSomeClass::CSomeClass() : x(0), y(1) { }
下⾯探讨这两种⽅法的异同以及如何使⽤这两种⽅法。
从技术上说,第⼆种⽅法⽐较好,但是在⼤多数情况下,两者实际上没有什么区别。第⼆种语法被称为成员初始化列表,之所以要使⽤这种语法有两个原因:⼀个原因是必须这么做,另⼀个原因是出于效率考虑。
让我们先看⼀下第⼀个原因——必要性。设想你有⼀个类成员,它本⾝是⼀个类或者结构,⽽且只有⼀个带⼀个参数的构造函数。
<span >class CMember { public: CMember(int x) { ... } };</span>
因为CMember有⼀个显式声明的构造函数,编译器不产⽣⼀个缺省构造函数(不带参数),所以没有⼀个整数就⽆法创建CMember的⼀个实例。
<span >CMember* pm = new CMember; // 出错!! CMember* pm = new CMember(2); // OK</span>
如果CMember是另⼀个类的成员,你怎样初始化它呢?答案是你必须使⽤成员初始化列表。
<span >class CMyClass { CMember m_member; public: CMyClass(); }; // 必须使⽤初始化列表来初始化成员 m_member CMyCl
没有其它办法将参数传递给m_member,如果成员是⼀个常量对象或者引⽤也是⼀样。根据C++的规则,常量对象和引⽤不能被赋值,它们只能被初始化。
使⽤初始化列表的第⼆个原因是出于效率考虑,当成员类具有⼀个缺省的构造函数和⼀个赋值操作符时。MFC的CString提供了⼀个完美
的例⼦。假定你有⼀个类CMyClass具有⼀个CString类型的成员m_str,你想把它初始化为"Hi,how are you."。你有两种选择:
<span >CMyClass::CMyClass() { // 使⽤赋值操作符 // CString::operator=(LPCTSTR); m_str = _T("Hi,how are you."); } // 使⽤初始化列表 // 和构造函数 CString::CString(LPCTSTR) CMyClass::CMyClass() : m_str(_T("Hi,how are you.")) { }</span>
在它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执⾏之前被初始化,因此在第⼀个例⼦中编译的代码将调⽤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++初始化类成员的,它们是按照声明的顺序初始化的,⽽不
是按照出现在初始化列表中的顺序。
<span >class CMyClass { CMyClass(int x, int y); int m_x; int m_y; }; CMyClass::CMyClass(int i) : m_y(i), m_x(m_y) { }</span>
你可能以为上⾯的代码将会⾸先做m_y=i,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明
的。结果是m_x将有⼀个不可预测的值。这个例⼦是故意这样设计来说明这⼀点的,然⽽这种bug会很⾃然地出现。有两种⽅法避免它,⼀个是总是按照你希望
它们被初始化的顺序来声明成员,第⼆个是,如果你决定使⽤初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
冒号(:)⽤法
(1)表⽰机构内位域的定义(即该变量占⼏个bit空间)