转载链接 https://blog.csdn.net/shitao827194819/article/details/38335953
最近学习了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>
如愿以偿被初始化为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;
- }
在使用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>
使用初始化列表的第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的CString提供了一个完美的例子。假定你有一个类CMyClass具有一个CString类型的成员m_str,你想把它初始化为"Hi,how are you."。你有两种选择:
- <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>
在CString的例子里这是无所谓的,因为缺省构造函数是内联的,CString只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。
在内建类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:- x=y=z=0;
- memset(this,0,sizeof(this));
当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于C++初始化类成员的,它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
- <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>
- <span style="font-family:KaiTi_GB2312;">struct _XXX{
- _XXX() : y(0xc0) {}
- };</span>
(4)类名冒号后面的是用来定义类的继承。
- <span style="font-family:KaiTi_GB2312;">class 派生类名 : 继承方式 基类名
- {
- 派生类的成员
- };</span>
下面重点讲一下构造函数后的(:)
构造函数后加冒号是初始化表达式:
有四种情况下应该使用初始化表达式来初始化成员:
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>
常见的三种情况
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
使用初始化成员列表对对象进行初始化,有时是必须的,有时是出于提高效率的考虑
双冒号(::)用法
例:声明了一个类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函数名前加::