C++类成员冒号初始化以及构造函数内赋值

原文:http://blog.csdn.net/zj510/article/details/8135556

通常我们对类成员进行“初始化”有两种方式:

1. 构造函数后面跟冒号;

2. 构造函数里面对成员进行赋值。

有些人不太注意这个小细节,或者根本不知道他们的区别,认为两种方式是一样的。这个误解有时可能会对程序带来影响,这里我来介绍一下这两种方式。

首先我们看这么一段代码:

[cpp]  view plain copy
 
  1. class A  
  2. {  
  3. public:  
  4.     A(int& c)  
  5.     {  
  6.         _a = 1;  
  7.     }  
  8. protected:  
  9.     int _a;  
  10.     const int _b;  
  11.     int& _c;  
  12. };  

这段代码正确吗?答案是否定,这段代码无法通过编译。我们会看到下面的编译错误

1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_b' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(20) : see declaration of 'A::_b'
1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_c' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(21) : see declaration of 'A::_c'
意思是说成员_b和_c必须在构造函数的成员初始化列表里面初始化。那么_a为什么没有报错呢?看看成员的声明,我们看到_a是一个int类型,_b是一个const int类型,_c是一个int&类型。根据C++的规则,const类型和引用不可以被赋值,只能被初始化。这里我们先花一点点时间来看一下const类型和引用。

大家看看下面的这段代码是否正确:

[cpp]  view plain copy
 
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     int a;  
  4.     const int b;  
  5.     int& c;  
  6.   
  7.     return 0;  
  8. }  

编译一下就会看到这2个错误:
1>d:\study\myconsole\myconsole\myconsole.cpp(30) : error C2734: 'b' : const object must be initialized if not extern
1>d:\study\myconsole\myconsole\myconsole.cpp(31) : error C2530: 'c' : references must be initialized

 哦,原来const和引用必须在声明的时候就初始化(其实就是因为const和引用不可以在变量创建完成后再被赋值,所以编译器做了这个限制)。ok,把代码改一下:

[cpp]  view plain copy
 
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     int a;  
  4.     const int b=5;  
  5.     int& c = a;  
  6.   
  7.     return 0;  
  8. }  


现在 编译就没有问题了。细心的朋友会发现我这里在b和c声明代码那里使用的=号而不是(),其实我们也可以这么做:

[cpp]  view plain copy
 
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     int a;  
  4.     const int b(5);  
  5.     int& c(a);  
  6.   
  7.     return 0;  
  8. }  

在这种情况下用括号和等于号初始化,效果是一样的。具体就不细讲了。

OK,我们举这个小例子的目的就是想加深大家对const和引用的印象:const和引用必须在声明的时候就初始化,换句话说就是在给const和引用类型变量分配内存的时候就初始化。

好了,现在我们回到class A的问题,类A里面有const成员和引用成员,当系统要给类A的对象分配内存的时候,系统需要给A的对象的3个成员_a, _b, _c分配内存。_a没有问题,系统直接给它一块内存。_b和_c就出问题了,分配内存的时候没有初始化。所以编译就出问题了。其实C++给类成员初始化的唯一方式就是成员初始化列表,也就是构造函数后面跟冒号的那种形式。将class A的代码调整一下:

[cpp]  view plain copy
 
  1. class A  
  2. {  
  3. public:  
  4.     A(int& c): _b(2), _c(c)  
  5.     {  
  6.         _a = 1;  
  7.     }  
  8. protected:  
  9.     int _a;  
  10.     const int _b;  
  11.     int& _c;  
  12. };  
  13.   
  14.   
  15.   
  16.   
  17. int _tmain(int argc, _TCHAR* argv[])  
  18. {  
  19.     int number = 3;  
  20.     A a(number);  
  21.   
  22.     return 0;  
  23. }  

我们在A的构造函数的后面用冒号来初始化_b和_c。现在可以通过编译了。因为系统可以在给_b和_c分配内存的时候就初始化了。那么假如我们把代码改成下面的形式:

[cpp]  view plain copy
 
  1. class A  
  2. {  
  3. public:  
  4.     A(int& c)  
  5.     {  
  6.         _a = 1;  
  7.         _b = 2;  
  8.         _c = c;  
  9.     }  
  10. protected:  
  11.     int _a;  
  12.     const int _b;  
  13.     int& _c;  
  14.   
  15. };  


这样 能行吗?编译一下就得到下面的错误:

1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_b' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(22) : see declaration of 'A::_b'
1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_c' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(23) : see declaration of 'A::_c'
1>d:\study\myconsole\myconsole\myconsole.cpp(17) : error C2166: l-value specifies const object

这3个错误包含2个意思:

1. const和引用变量没有初始化;

2. 不可以对const变量_b进行赋值,也可以说const变量不可以当作左值(error C2166: l-value specifies const object)。

现在我们就可以知道了,其实在构造函数里面调用等于号并不是真正意义上的“初始化”。这个过程相当于:

1. 系统创建成员变量;

2. 创建完后再进行赋值操作。

而在构造函数后面跟冒号,就相当于:

1. 系统创建成员变量并且初始化。也就是系统为成员变量分配了一块内存并且把相应的数据给填了进去。而构造函数里面调用等于号的方式是分配好后再进行赋值,多了一个步骤。

 下面我们再来做一个实验:

[cpp]  view plain copy
 
  1. class A  
  2. {  
  3. public:  
  4.     A(int& c): _b(2), _c(c)  
  5.     {  
  6.         _a = 1;  
  7.     }  
  8. protected:  
  9.     int _a;  
  10.     const int _b;  
  11.     int& _c;  
  12.   
  13. };  
  14.   
  15. class B  
  16. {  
  17. public:  
  18.     B(int& c):_objA(c)  
  19.     {  
  20.         printf("B constructor\n");  
  21.     }  
  22.   
  23. protected:  
  24.     A _objA;  
  25. };  
  26.   
  27.   
  28.   
  29. int _tmain(int argc, _TCHAR* argv[])  
  30. {  
  31.     int number = 3;  
  32.     B obj2(number);  
  33.   
  34.     return 0;  
  35. }  

类B里面有个一个类A的对象,在类B的构造函数里面用冒号来初始化成员_objA。那么_objA是什么时候被初始化的呢?有图有真相:


 从callstack里面可以清楚的看到:

1. 进入B的构造函数;

2. 进入A的构造函数。

也就是说冒号后面的代码是在一进入构造函数的时候就被调用了。

然后从左下角的Watch里面也可以看到,在系统调用构造函数括号里面的第一行代码之前,_a,_b, _c就已经分配好了。我们可以看到_a是个没有初始化过的值(系统自己生成了一个),_b和_c都是我们初始化的。那么我可以得出一个结论:

构造函数后面跟的冒号代码是在进入构造函数并且在括号里面的第一行代码之前被执行。

 

假如在B的构造函数里面不显式初始化_objA,会发生什么事呢?用代码模拟一下就知道了,系统会调用A的默认构造函数来初始化_objA。

 

好了,讲完了。通俗的讲,构造函数后面的冒号就是初始化,而括号里面的等于号并不是初始化,而是变量生成以后的赋值而已(永远都是2个步骤)。

 

在C++类的构造函数中经常会看到如下格式的写法:

MyWindow::MyWindow(QWidget* parent , Qt::WindowFlags flag) : QMainWindow(parent,flag)
上述语句中单冒号(:)的作用是表示后面是初始化列表,一般有三种使用场景。

1、对父类进行初始化

(目的是什么呢?在创建子类时,能够把参数传给父类,从而完成对父类的初始化)

调用格式为“子类构造函数 : 父类构造函数”,如下,其中QMainWindow是MyWindow的父类:

MyWindow::MyWindow(QWidget* parent , Qt::WindowFlags flag) : QMainWindow(parent,flag)


2、对类成员进行初始化

调用格式为“构造函数 : A(初始值),B(初始值),C(初始值)……”,如下,其中A、B、C分别是类的成员变量:

class rectangle //头文件中类定义
{
public:
rectangle( int pointX, int pointY, int Width, int Length );
private:
CPoint m_point;
int m_Width;
int m_Length;
};

rectangle::rectangle(int pointX, int pointY, int Width, int Length) : m_point(pointX,pointY),m_Width(Width),m_Length(Length)//源文件中构造函数实现
{
todo......
}
当然,上面构造函数的实现与下面的写法等价

rectangle::rectangle(int pointX, int pointY, int Width, int Length)//源文件中构造函数实现
{
m_point.X = pointX;
m_point.Y = pointY;
m_Width = Width;
m_Length = Length;
todo......
}

3、对类的const成员变量进行初始化

由于const成员变量的值无法在构造函数内部初始化,因此只能在变量定义时赋值或使用初始化列表赋值。

对于2、3中的应用场景,有以下两点说明:

1、构造函数列表初始化执行顺序与成员变量在类中声明顺序相同,与初始化列表中语句书写先后无关。

2、相对于在构造函数中赋值,初始化列表执行效率更高。

 

附:

本文前面我提到一句话:const和引用不可以被赋值,只能被初始化。可能会有些朋友对这句话有意见,看下面的代码,这段代码是正确的,没有问题。那么怎么说不能被赋值呢?其实b=12只是把a的内容给改掉了(a和b的值都是12),而不是把引用b指向另外一个变量。换句话说:引用b初始化完成后,就永远指向初始化时候的那个变量,无法再改变了。我这里的“引用不可以被赋值”是指不能给引用本身赋值来改变它的指向,并不是说不可以改变引用指向的内存的内容。可能言语上面会有不同的理解,但是只要知道是这么回事情就可以了。

[cpp]  view plain copy
 
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     int a = 1;  
  4.     int& b = a;  
  5.     b = 12;  
  6.   
  7.     return 0;  
  8. }  
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值