C++中为什么构造函数初始化列表

时间:2021-08-06 05:55:42
已经有个构造函数负责初始化,为什么还需要构造函数初始化表呢?
在以下三种情况下需要使用初始化成员列表:
一,需要初始化的数据成员是对象的情况;
二,需要初始化const修饰的类成员;
三,需要初始化引用成员数据;
需要初始化引用成员数据
最近才发现C++可以定义引用类型的成员变量,引用类型的成员变量必须在构造函数的初始化列表中进行初始化。对于类成员是const修饰,或是引用类型的情况,是不允许赋值操作的,(显然嘛,const就是防止被错误赋值的,引用类型必须定义赋值在一起)因此只能用初始化列表对齐进行初始化。
#include <iostream>
using namespace std;
class Test
{
private:
    int &a;
public:
    Test(int &b) : a(b)
    {
    }
    void Modify(int value)
    {
        a = value;
    }
};
int main()
{
    int b = 3;
    Test test(b);
    cout <<"b="<<b<<endl;
    test.Modify(4);
    cout <<"b="<<b<<endl;
    return 0;
}
需要初始化const修饰的类成员
#include <iostream>
using namespace std;
class base
{
public:
 const int a;
 int& b;
public:
// base(int m, int n)
// {
//  a = m;
//  b = n;
// }
 base(int m, int n):a(m),b(n)
 {}
};
 
int main()
{
 base ba(1,2);
 cout << ba.a <<endl;
 cout << ba.b <<endl;
}
需要初始化的数据成员是对象的情况
#include <iostream>
using namespace std;
class point
{
protected:
 int m_x,m_y;
public:
 point(int m=0,int n=0)
 {
  m_x = m;
  m_y = n;
  printf("constructor called!\n");
 }
 point(point& p)
 {
  m_x = p.GetX();
  m_y = p.GetY();
  printf("copy constructor called!\n");
 }
 int GetX()
 {
  return m_x;
 }
 int GetY()
 {
  return m_y;
 }
};
 
class point3d
{
private:
 point m_p;
 int m_z;
public:
 point3d(point p, int k)
 {
  m_p = p;                              //这里是对m_p的赋值
  m_z=k;
 }
 point3d(int i,int j, int k):m_p(i,j)   // 相当于 point m_p(i,j)这样对m_p初始化
 {
  m_z=k;
 }
 void Print()
 {
  printf("%d,%d,%d \n",m_p.GetX(),m_p.GetY(),m_z);
 }
};
上述代码中Point3d是一个3D坐标,他有一个point的2D坐标和一个成员组成。
我们现在想定义一个3D坐标p3d,可以这样实现:
int main()
{
 point p(1,2);    //先定义一个2D坐标
 point3d p3d(p,3);
 p3d.Print();
}
从point3d实现体可以看出,我们是通过对m_p进行赋值,这样不仅调用copy constructor产生临时对象而且是对m_p的一个赋值操作。
而如果使用成员初始化列表,我们则可以这样:
int main()
{
 point p(1,2);
 point3d p3d(1,2,3);
 p3d.Print();
}
p3d中的point型成员是通过调用初始化的方式构建的。由于对象赋值比初始化要麻烦的多,因此也带来的性能上的消耗。这也是我们在对成员数据是对象成员的采用初始化列表进行初始始化的主要原因。
 
------------------------------------------------------------------------------------------------------------------------------------------------------------------
一 C++必须用带有初始化列表的情况:
(1)成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
(2)const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
 

我们定义一个如下的Person类:
class Person {
public:
    Person() { }  //default constructor function
    Person(string name, string phone, string addr)
    {
        m_name = name;   //想采用赋值初始化数据成员
        m_phone = phone;
        m_addr = addr;
    }
    
private:
    const string m_name; 
    const string m_phone;
    const string m_addr;
}; 编译后发现这个类的第二个带参数的构造函数是错误的。我们创建一个Person对象:
Person p("marcky", "13233232", "cqupt"); //调用带参数的构造函数创建一个Person对象 创建对象的过程分为了两步:
      一、从内存中分配实际的空间给对象p,其三个字符串对象的数据成员是调用的默认构造函数初始化为空。也就说,此时为止,对象p的三个数据成员都是一个空的字符串。
      二、执行调用的构造函数的函数体语句,完成对数据成员的赋值,以此达到我们期望的创建一个指定Person对象,而不是空对象。

从上面的第二步就可以看到,我们在对三个const对象进行赋值操作,这显然是不允许的操作,因此利用这个构造函数创建Person将以失败告终。要想成功的创建一个特定的Person对象,我们需要构造函数初始化列表:
   Person(string name, string phone, string addr)
        :m_name(name), m_phone(phone), m_addr(addr){ } //冒号开始定义初始化列表 使用初始化列表创建对象的构造函数同样是通过上述的两个步骤来完成的,不同之处在于创建对象的数据成员时使用的不是默认构造函数,而是根据指定参数调用了相应的构造函数,以此创建特定的对象,而不是空对象。这样一来,对象的数据成员的特定值在创建对象的时候就被赋予了相应的成员,而不是在创建对象完成之后再通过赋值语句去修改数据成员,因此利用构造函数初始化列表就可以成功的创建具有const数据成员的对对象了。

除了const的数据成员外,没有默认构造函数的类类型或者是引用类型的成员,都必须在构造函数的初始化列表中进行初始化。

没有默认构造函数的类类型成员,如果不在初始化列表中初始化的话,那么创建该对象的时候,由于没有指定相应的“实参”,编译器就会去调用默认构造函数来创建对象,必然会以失败而告终。
引用类型的成员和const类型成员一样,因为引用必须初始化,初始化后就不能修改,所以后期通过赋值来修改其值是错误的。

ps:数据成员被初始化的顺序与构造函数初始化列表中的次序无关,而是与成员的定义顺序一致。

二 范例

 

为了更好地理解构造函数初始化列表的使用规则,我们再来看下面的例子。
前面我们已经说了类的构造函数和析构函数,我们知道一个类的成员可以是另外一个类的对象,构造函数允许带参数,那么我们可能会想到在程序中我们可以这样做:在Student类中把它的teacher成员用带参数的形式调用Student类的构造函数,不必要再在Teacher类中进行操作,由于这一点构想我们把在2.1中提及的程序修改成如下形式:
#include <iostream>
using namespace std;

class Teacher  
{   
    char *director; 
public: 
    Teacher(char *temp)  
    {  
  cout << "class Teacher:";
  
        director = new char[10];    
        strcpy(director, temp);  
    }  
    ~Teacher()
 {
  cout << "释放堆区director内存空间\n";  
        delete[] director;  
        cin.get();  
 }  
    char* GetMember()
 {  
     return director;  
 }  
 void Show() 
 {
  cout << "director = " << director << endl; 
 }
};  
  
class Student  
{  
 int number;  
    int score;  
    Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,
       //不能在类中使用带参数的构造函数进行初始化  ;
public:  
    Student()  
    {  
  cout << "class Student:";
        number = 1;  
        score = 100;  
    }  
    ~Student()  
    {
  cout << "释放class Student 内存空间\n"; 
  cin.get();  
 }
    void Show() 
 {  
     cout << "\nteacher = " << teacher.GetMember() << endl;
  cout << "number = " << number << endl;
  cout << "score = " << score << endl;    
 }  
};  
  
int main()  
{  
    Student a;  
    Teacher b("内存空间");
    
    a.Show();  
    b.Show();  
    
    getchar();
 return 0;
}   
  可是很遗憾,程序不能够被编译成功,为什么呢? 
  因为:类是一个抽象的概念,并不是一个实体,并不能包含属性值(这里来说也就是构造函数的参数了),只有对象才占有一定的内存空间,含有明确的属性值! 
  这一个问题是类成员初始化比较尴尬的一个问题,是不是就没有办法解决了呢?呵呵。。。。。。 
  c++为了解决此问题,有一个很独特的方法,下面我们来看。
对于上面的那个尴尬问题,我们可以在构造函数头的后面加上冒号并指定调用那个类成员的构造函数来解决!
代码如下: 
#include <iostream>
using namespace std;

class Teacher  
{   
    char *director; 
public: 
    Teacher(char *temp)  
    {  
  cout << "class Teacher:";
  
        director = new char[10];    
        strcpy(director, temp);  
    }  
    ~Teacher()
 {
  cout << "释放堆区director内存空间\n";  
        delete[] director;  
        cin.get();  
 }  
    char* GetMember()
 {  
     return director;  
 }  
 void Show() 
 {
  cout << "director = " << director << endl; 
 }
};  
  
class Student  
{  
 int number;  
    int score;  
    Teacher teacher;
public:  
    Student(char *temp): teacher(temp)  //冒号后指定调用某成员构造函数
    {  
  cout << "class Student:";
        number = 1;  
        score = 100;  
    }  
    
    ~Student()  
    {
  cout << "释放class Student 内存空间\n"; 
  cin.get();  
 }
    void Show() 
 {  
     cout << "\nteacher = " << teacher.GetMember() << endl;
  cout << "number = " << number << endl;
  cout << "score = " << score << endl;    
 }  
};  
  
int main()  
{  
    Student a("王大力");  
    Teacher b("内存空间");
    
    a.Show();  
    b.Show();  
    
    getchar();
 return 0;
}   
程序将正确运行并输出:
class Teacher:: class Student: class Teacher:
teacher = 王大力
number = 1
score = 100
director = 内存空间
释放堆区director内存空间
释放class Student 内存空间
释放堆区director内存空间

大家可以发现最明显的改变在这里 :Student(char *temp): teacher(temp) 
冒号后的teacher就是告诉调用Student类的构造函数的时候把参数传递给成员teacher的Teacher类的构造函数,这样一来我们就成功的在类体外对teacher成员进行了初始化,既方便也高效,这种冒号后指定调用某成员构造函数的方式,可以同时指定多个成员,这一特性使用逗号方式,例如: 
Student(char* temp):teacher(temp),abc(temp),def(temp) 
由冒号后可指定调用那个类成员的构造函数的特性,使得我们可以给类的常量和引用成员进行初始化成为可能。 
我们修改上面的程序,得到如下代码:
#include <iostream>
using namespace std;

class Teacher  
{   
    char *director; 
public: 
    Teacher(char *temp)  
    {  
  cout << "class Teacher:";
  
        director = new char[10];    
        strcpy(director, temp);  
    }  
    ~Teacher()
 {
  cout << "释放堆区director内存空间\n";  
        delete[] director;  
        cin.get();  
 }  
    char* GetMember()
 {  
     return director;  
 }  
 void Show() 
 {
  cout << "director = " << director << endl; 
 }
};  
  
class Student  
{  
 int number;  
    int score;  
    Teacher teacher;
    int &pk;  
    const int ps;  
public:  
    Student(char* temp, int &k): teacher(temp), pk(k), ps(10)  
    {  
  cout << "class Student:";
        number = 1;  
        score = 100;  
    }  
    
    ~Student()  
    {
  cout << "释放class Student 内存空间\n"; 
  cin.get();  
 }
    void Show() 
 {  
     cout << "\nteacher = " << teacher.GetMember() << endl;
  cout << "number = " << number << endl;
  cout << "score = " << score << endl; 
  cout << "pk = " << pk << endl;  
  cout << "ps = " << ps << endl;     
 }  
};  
  
int main()  
{  
 char *name = "王大力";  
    int b = 99;  
    
    Student a(name, b);  
    a.Show();  
    
    getchar();
 return 0;
}   
程序将正确运行并输出:
class Teacher:: class Student: teacher = 王大力
number = 1
score = 100
pk = 99
ps = 10
释放class Student 内存空间
释放堆区director内存空间
  
改变之处最重要的在这里Student(char* temp, int &k): teacher(temp), pk(k), ps(10)  
调用的时候我们使用 
Student a(name, b);  
我们将b的地址传递给了int &k这个引用,使得Student类的引用成员pk和常量成员ps进行了成功的初始化。 
 但是细心的人会发现,我们在这里使用的初始化方式并不是在构造函数内进行的,而是在外部进行初始化的。的确,在冒号后和在构造函数括号内的效果是一样的,但和teacher(temp)所不同的是,pk(pk)的括号不是调用函数的意思,而是赋值的意思,我想有些读者可能不清楚新标准的c++对变量的初始化是允许使用括号方式的,int a=10和int a(10)的等价的,但冒号后是不允许使用=方式只允许()括号方式,所以这里只能使用pk(pk)而不能是pk=pk了。