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

时间:2023-02-22 19:00:43
已经有个构造函数负责初始化,为什么还需要构造函数初始化表呢?
在以下三种情况下需要使用初始化成员列表:
一,需要初始化的数据成员是对象的情况;
二,需要初始化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了。

C++中为什么构造函数初始化列表的更多相关文章

  1. c&plus;&plus;中的构造函数初始化列表

    三种情况下,必须在构造函数初始化列表中初始化成员: 1.const成员 2.引用成员 3.没有默认构造函数的成员

  2. C&plus;&plus;构造函数初始化列表与构造函数中的赋值的区别

    C++类中成员变量的初始化有两种方式:构造函数初始化列表和构造函数体内赋值. 一.内部数据类型(char,int……指针等) class Animal { public: Animal(int wei ...

  3. &lbrack;c&plus;&plus;基本语法&rsqb;——构造函数初始化列表

    c++构造函数初始化成员变量列表: #pragma once class Node { public: int data; // 权值 Node *parent; // 父节点 Node *left; ...

  4. C&plus;&plus;类构造函数初始化列表

    C++类构造函数初始化列表 构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式.例如: class CExample {public:     ...

  5. C&plus;&plus;构造函数初始化列表与赋值

    C++中类的初始化操作一般有四个部分组成: 1.构造函数初始化列表 2.构造函数体内赋值 3.类外部初始化 4.类声明时直接赋值 对于内部数据类型(char,int,float...),构造函数初始化 ...

  6. 10&period;C&plus;&plus;-构造函数初始化列表、类const成员、对象构造顺序、析构函数

    首先回忆下,以前学的const 单独使用const修饰变量时,是定义的常量,比如:const int i=1; 使用volatile const修饰变量时,定义的是只读变量 使用const & ...

  7. C&plus;&plus;类构造函数初始化列表&lpar;转&rpar;

    构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式.例如: { public:     int a;     float b;     //构 ...

  8. const成员或者引用成员必须使用构造函数初始化列表的方式

    #include<iostream.h> class A { const int a; int b; }; void main() { A obja; }编译出现如下错误:error C2 ...

  9. c&plus;&plus;构造函数成员初始化中赋值和初始化列表两种方式的区别

    先总结下: 由于类成员初始化总在构造函数执行之前 1)从必要性: a. 成员是类或结构,且构造函数带参数:成员初始化时无法调用缺省(无参)构造函数 b. 成员是常量或引用:成员无法赋值,只能被初始化 ...

随机推荐

  1. javascript中创建对象的几种方式

    1. 使用Object构造函数来创建一个对象,下面代码创建了一个person对象,并用两种方式打印出了Name的值. var person = new Object(); person.name=&q ...

  2. 降kipmi0的CPU

    echo 100 >/sys/module/ipmi_si/parameters/kipmid_max_busy_us

  3. UVa1025 &lpar;DAG上的dp&rpar;

    这是紫书上的第一个dp哈. 1.状态定义:dp[i][j]---->到时刻i的时候(出发的时候时刻为0,约定时间为时刻time),从j号车站开往N号车站,在车站等待的最少的时间. 2.这个人当前 ...

  4. 多个ip以逗号分隔

    /^(((?:(?:1[0-9][0-9]\.)|(?:2[0-4][0-9]\.)|(?:25[0-5]\.)|(?:[1-9][0-9]\.)|(?:[0-9]\.)){3}(?:(?:1[0-9 ...

  5. 力软开发力软框架力软敏捷开发learun

    版本:6.1.6.2 演示地址:www.fishcmonkey.com 联系QQ:6539471

  6. OSPF协议总结

    总结: 1.ospf协议报文不会泛洪扩散,而是逐级路由器处理后,再从所有ospf启用端口发送出去,也就是说,只能从邻居接收到ospf报文,报文的源ip是邻居的ip地址,目的ip是组播ip. 2.开启o ...

  7. &lbrack;转&rsqb;jsPlumb插件做一个模仿viso的可拖拉流程图

    原贴:https://www.cnblogs.com/sggx/p/3836432.html 前言 这是我第一次写博客,心情还是有点小小的激动!这次主要分享的是用jsPlumb,做一个可以给用户自定义 ...

  8. Python 递归函数 详解

    Python 递归函数 详解   在函数内调用当前函数本身的函数就是递归函数   下面是一个递归函数的实例: 第一次接触递归函数的人,都会被它调用本身而搞得晕头转向,而且看上面的函数调用,得到的结果会 ...

  9. cocos2d-x getVisibleOrigin

    getVisibleSize:表示获得视口(可视区域)的大小,如果DesignResolutionSize跟屏幕尺寸一样大,则getVisibleSize等于getWinSize.getVisible ...

  10. K组翻转链表 &&num;183&semi; Reverse Nodes in k-Group

    [抄题]: 给你一个链表以及一个k,将这个链表从头指针开始每k个翻转一下.链表元素个数不是k的倍数,最后剩余的不用翻转. [思维问题]: [一句话思路]: // reverse head->n1 ...