最近学习了C++中一些特殊符号的使用规则,查阅了一些资料和博客,对初始化列表、(:),(::)的用法进行了梳理,如有理解不周的地方欢迎大家指正
初始化列表
初始化列表其实就是类成员初始化列表(Member Initialization List)的简单说法。
1) 基本概念,初始化列表的形式
<span style="font-family:KaiTi_GB2312;">#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 style="font-family:KaiTi_GB2312;">
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 style="font-family:KaiTi_GB2312;">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)
{
}
的效率要比
MemberInitializationList(int val)
{
i = val;
j = I;
}
高。理由是后者会产生临时性的变量,然后要调用赋值运算符赋给真正的变量,再然后摧毁那个临时性的变量。是否真的是这样呢?我们来做一个试验证明之。验证程序如下:
#include <iostream>
#include <time.h>
using namespace std;
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;
}
结论:在VC6和VC2005的编译器上,两种方式似乎没有什么区别。或许在别的编译器上有所区别。也就是说,要么微软的编译器对于两种构造函数都使用了临时变量,或者都没有使用临时变量。
3) 调用一个成员函数设定成员变量的初值
#include <iostream>
#include <time.h>
using namespace std;
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 style="font-family:KaiTi_GB2312;">class CMember { public: CMember(int x) { ... } };</span>
<span style="font-family:KaiTi_GB2312;">CMember* pm = new CMember; // 出错!! CMember* pm = new CMember(2); // OK</span>
<span style="font-family:KaiTi_GB2312;">class CMyClass { CMember m_member; public: CMyClass(); }; // 必须使用初始化列表来初始化成员 m_member CMyClass::CMyClass() : m_member(2) { ••• }</span>
<span style="font-family:KaiTi_GB2312;">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>
或者x=y=z=0;
注意第二个片断绝对是非面向对象的。memset(this,0,sizeof(this));
<span style="font-size:12px;">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>
冒号(:)用法
(1)表示机构内位域的定义(即该变量占几个bit空间)
<span style="font-family:KaiTi_GB2312;">typedef struct _XXX{
unsigned char a:4;
unsigned char c;
} ; XXX</span>
(2)构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表,更适用于成员变量的常量const型。
<span style="font-family:KaiTi_GB2312;">struct _XXX{
_XXX() : y(0xc0) {}
};</span>
(3) public:和private:后面的冒号,表示后面定义的所有成员都是公有或私有的,直到下一个"public:”或"private:”出现为止。"private:"为默认处理。
(4)类名冒号后面的是用来定义类的继承。
<span style="font-family:KaiTi_GB2312;">class 派生类名 : 继承方式 基类名
{
派生类的成员
};</span>
继承方式:public、private和protected,默认处理是public。
下面重点讲一下构造函数后的(:)
构造函数后加冒号是初始化表达式:
有四种情况下应该使用初始化表达式来初始化成员:
1:初始化const成员
2:初始化引用成员
3:当调用基类的构造函数,而它拥有一组参数时
4:当调用成员类的构造函数,而它拥有一组参数时。
在程序中定义变量并初始化的机制中,有两种形式,一个是我们传统的初始化的形式,即赋值运算符赋值,还有一种是括号赋值,如:
<span style="font-family:KaiTi_GB2312;"> int a=10;
char b='r';//赋值运算符赋值
int a(10);/
char b('r');//括号赋值 </span>
以上定义并初始化的形式是正确的,可以通过编译,但括号赋值只能在变量定义并初始化中,不能用在变量定义后再赋值,
冒号初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。 对于在函数中初始化,是在所有的数据成员被分配内存空间后才进行的。这样是有好处的,有的数据成员需要在构造函数调入之后函数体执行之前就进行初始化如引用数据成员,常量数据成员和对象数据成员
<span style="font-family:KaiTi_GB2312;color:#330033;">class student
{public :
student ()
.
.
.
protected:
const int a;
int &b;
}
student ::student (int i,int j)
{
a=i;
b=j;
} </span>
在Student类中有两个数据成员,一个是常量数据成员,一个是引用数据成员,并且在构造函数中初始化了这两个数据成员,但是这并不能通过编译,因为常量初始化时必须赋值,它的值是不能再改变的,与常量一样引用初始化也需要赋值,定义了引用后,它就和引用的目标维系在了一起,也是不能再被赋值的。所以C
++":"后初始化的机制,使引用和常量数据成员变为可能的,Student类的构造函数应为: student ::student(int i,int j):a(i),b(j){}
常见的三种情况
1、对含有对象成员的对象进行初始化,例如,
类line有两个私有对象成员startpoint、endpoint,line的构造函数写成:
line(int sx,int sy,int ex,int ey):startpoint(sx,sy),endpoint(ex,ey){……}
初始化时按照类定义中对象成员的顺序分别调用各自对象的构造函数,再执行自己的构造函数
2、对于不含对象成员的对象,初始化时也可以套用上面的格式,例如,
类rectangle有两个数据成员length、width,其构造函数写成:
rectangle():length(1),width(2){}
rectangle(int x,int y):length(x),width(y){}
3、对父类进行初始化,例如,
CDlgCalcDlg的父类是MFC类CDialog,其构造函数写为:
CDlgCalcDlg(CWnd* pParent ): CDialog(CDlgCalcDlg::IDD, pParent)
其中IDD是一个枚举元素,标志对话框模板的ID
使用初始化成员列表对对象进行初始化,有时是必须的,有时是出于提高效率的考虑
双冒号(::)用法
1)表示“域操作符”
例:声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时,
就要写成void A::f(),表示这个f()函数是类A的成员函数。
2)直接用在全局函数前,表示是全局函数
例:在VC里,你可以在调用API 函数里,在API函数名前加::
3)表示引用成员函数及变量,作用域成员运算符
例:System::Math::Sqrt() 相当于System.Math.Sqrt()
VC中如下
::是C++里的“作用域分解运算符”。比如声明了一个类A,类A里声明了一个成员函数voidf(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成voidA::f(),表示这个f()函数是类A的成员函数。
:: 一般还有一种用法,就是直接用在全局函数前,表示是全局函数。当类的成员函数跟类外的一个全局函数同名时,考试,大提示在类内定义的时候,打此函数名默认调用的是本身的成员函数;如果要调用同名的全局函数时,就必须打上::以示区别。比如在VC里,你可以在调用API函数时,在API函数名前加::