C/C++ 通过初始化列表和构造函数内赋值初始化成员变量的区别

时间:2022-09-09 20:05:03

一般我们进行成员变量初始化用两种方法

第一种是通过在构造函数内赋值

class Point
{
public:
 Point(){ _x = 0; _y = 0;};
 Point( int x, int y ){ _x = 0; _y = 0; }
private:
 int _x, _y;
};

第二种是使用初始化列表

  class Point
{
public:
 Point():_x(0),_y(0){};
 Point( int x, int y ):_x(x),_y(y){}
private:
 int _x, _y;
};

 

这两种用法是有区别的

一、在有些情况下,必须使用初始化列表。特别是const和引用数据成员被初始化时。

class Point
{
// 这个类的定义就要求使用初始化成员列表,因为const成员只能被初始化,不能被赋值
public:
 Point():_x(0),_y(0){}; 
 Point( int x, int y ):_x(x),_y(y){}
 //Point(){ _x = 0; _y = 0;}
 //Point( int x, int y ){ _x = 0; _y = 0; }
private:
 const int _x, _y;
};

 

二、是从效率方面来说的,对于内置类型或复合类型,差异不会太大,但对于非内置数据类型,差异还是很明显的

如我们再给Point类添加一个新的string类型的成员变量

class Point
{
 const int _x, _y;
 string _name;
};

 

构造函数内赋值进行初始化

Point( int x, int y, string name ){ _x = 0; _y = 0; _name = name; }

 

_name = name 这个表达式会调用string类的缺省构造函数一次,再调用Operator=函数进行赋值一次。所以需调用两次函数:一次构造,一次赋值

 

用初始化列表进行初始化

Point( int x, int y, string name ):_x(x),_y(y), _name(name){}  

_name会通过拷贝构造函数仅以一个函数调用的代码完成初始化

即使是一个很简单的string类型,不必要的函数调用也会造成很高的代价。随着类越来越大,越来越复杂,它们的构造函数也越来越大而复杂,那么对象创建的代价也越来越高,所以一般情况下建议使用初始化列表进行初始化,不但可以满足const和引用成员的初始化要求,还可以避免低效的初始化数据成员。

详细出处参考:http://www.itqun.net/content-detail/105800_2.html

————————————————————————————————————————————————————————

C++构造函数中初始化成员__参数列表初始化成员(必须用原因:对象成员的初始化,const修饰的成员的初始化,引用成员的初始化,子类调用父类的构造函数初始化父类成员)__参数列表在构造函数执行之前执行,参数列表中执行的是初始化(所有的成员,无论是否出现在参数列表中,都会有初始化),参数列表的执行顺序与类中成员的声明顺序,与类的继承顺序相一致__构造函数中执行的一般是赋值_多重继承,虚继承构造函数的参数初始化列表的区别  

类对象的构造顺序是这样的:
1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员;
2.进入构造函数后在构造函数中执行一般赋值与计算。

使用初始化列表有两个原因:
原因1.必须这样做:

《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:

一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
二、需要初始化const修饰的类成员;
三、需要初始化引用成员数据;

即:

例一、数据成员是对象,切对象只有含参数的构造函数;
        如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。
class Test
{
public:
         Test(int x,int y,int z);
private:
         int a;
         int b;
         int c;
};
class MyTest
{
public:
        MyTest():test(1,2,3){}        //初始化,初始化列表在构造函数执行前执行(这个可以测试,对同一个变量在初始化列表和构造函数中分别初始化,首先执行参数列表,后在函数体内赋值,后者会覆盖前者)。
private:
        Test test;            //声明
};

        因为Test有了显示的带参数的构造函数,那么他是无法依靠编译器生成无参构造函数的,所以没有三个int型数据,就无法创建Test的对象。
        Test类对象是MyTest的成员,想要初始化这个对象test,那就只能用成员初始化列表,没有其他办法将参数传递给Test类构造函数

例二、对象引用或者cosnt修饰的数据成员
        另一种情况是这样的:类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的

即:

class Test

{

     priate:

          const int a;               //const成员声明

      public:

          Test():a(10){}           //初始化

};

class Test

{

    private:

         int &a;                        //声明

    public:

         Test(int a):a(a){}        //初始化

}

例三、子类初始化父类的私有成员,需要在(并且也只能在)参数初始化列表中显示调用父类的构造函数,如下:

 

 
 

class Test {  private:   int a;   int b;   int c;  public:   Test(int a,int b,int c)   {    this->a = a;    this->b = b;    this->c = c;   }   int getA(){return a;}   int getB(){return b;}   int getC(){return c;} };

class MyTest:public Test {  private:   int d;  public:   MyTest(int a,int b,int c,int d):Test(a,b,c)   //MyTest(int a,int b,int c,int d)   {       //Test(a,b,c);           //构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用    this->d = d;   }   int getD(){return d;} };

 

int main(int argc,char *argv[]) {  MyTest mytest(1,2,3,4);  printf("a=%d,b=%d,c=%d,d=%d\n",    mytest.getA(),mytest.getB(),mytest.getC(),mytest.getD());  return 0; }

 
多重继承,虚继承构造函数的参数初始化列表的区别:
代码如下:
 
 
 
 
注意多重继承子类的构造函数
#include   <stdio.h> class   CTop { private : int  a ; public : int  getA () { return  a ; } CTop ( int  a ) { this -> a   =  a ; } }; class   CLeft : public CTop { private : int  b ; public : int  getL () { return  b ; } CLeft ( int  a , int  b ): CTop ( a ) { this -> =  b ; } }; class   CRight : public CTop { private : int  c ; public : int  getR () { return  c ; } CRight ( int  a , int  c ): CTop ( a ) { this -> =  c ; } }; class   Test : public   CLeft , public   CRight { private : int  d ; public : int  getT () { return  d ; } Test ( int  a , int  b , int  c , int  d ): CLeft(a,b),CRight(a,c) { this -> =  d ; } }; int  main ( int  argc , char   * argv []) { Test  obj ( 1 , 2 , 3 , 4 ); printf ( "obj.a=%d,obj.b=%d,obj.c=%d,obj.d=%d\n" , obj . CLeft :: getA (), obj . getL (), obj . getR (), obj . getT ()); //getA有歧义,要用类名来做区分。 return   0 ; }
 
 
 
 
 
 

注意虚继承子类的构造函数

#include <stdio.h> class CTop { private : int a ; public : int getA () { return a ; } CTop ( int a ) { this -> a = a ; } }; class CLeft : virtual public CTop
{ private : int b ; public : int getL () { return b ; } CLeft ( int a , int b ): CTop ( a ) { this -> b = b ; } }; class CRight : virtual public CTop { private : int c ; public : int getR () { return c ; } CRight ( int a , int c ): CTop ( a ) { this -> c = c ; } }; class Test : public CLeft,public CRight { private : int d ; public : int getT () { return d ; } Test ( int a , int b , int c , int d ): CLeft(a,b),CRight(a,c), CTop(a) { this -> d = d ; } }; int main ( int argc , char * argv []) { Test obj ( 1 , 2 , 3 , 4 ); printf ( "obj.a=%d,obj.b=%d,obj.c=%d,obj.d=%d\n" , obj . getA (), obj . getL (), obj . getR (), obj . getT ()); //因为采用虚基类,虚继承机制保证了a只有一份,所以不存在歧义。 return 0 ; }
 
 

 

原因2.效率要求这样做:
       类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。

注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在类中声明时的顺序是一致的,与列表的先后顺序无关所以要特别注意,保证两者顺序一致才能真正保证其效率和准确性

为了说明清楚,假设有这样一个类:
class foo
{
      private :
          int a, b;
};
1、foo(){}和foo(int i = 0){}都被认为是默认构造函数,因为后者是默认参数。两者不能同时出现。
2、构造函数列表的初始化方式不是按照列表的的顺序,而是按照变量声明的顺序。比如foo里面,a在b之前,那么会先构造a再构造b。所以无论foo():a(b + 1), b(2){}还是foo():b(2),a(b+1){}都不会让a得到期望的值。

3、构造函数列表能够对const成员初始化。比如foo里面有一个int const c;则foo(int x) : c(x){}可以让c值赋成x。

不过需要注意的是,c必须在每个构造函数(如果有多个)都有值。
4、在继承里面,只有初始化列表可以构造父类的private成员(通过显示调用父类的构造函数)。比如说:
       class child : public foo

{
       };
       foo里面的构造函数是这样写的:

foo (int x)

{

       a = x;

}.
       而在child里面写child(int x){ foo(x); }是通过不了编译的。

只有把子类构造函数写作child (int x) : foo(x){}才可以。

——————————————————————————————————————————————————————————————————
关于复合类型:

C++中,什么是复合类型?

刚开始,还以为自定义的类是复合类型。查了C++ primer才知道复合类型不是类。

在C++中类型大致可以分为三种

一、内置类型

如int, char, float, unsigned等。内置类型是最基本的类型。

二、复合类型

复合类型:使用其它类型定义的类型。有三种复合类型:引用,指针,数组。

三、类类型

就是类。比如string以及自己定义的类。


数组、结 构和指针式c++3种复合类型。

数组,可以在一个数据对象中存储多个同种类型的值。通过使用索引或下标,可以访问数组中的各个元素。

结构,可以将多个不同类型的值存储在同一个数据对象中,可以使用成员关系操作符(.来访问其中的成员。使用结构的第一步是创建结构模板,它定义了结构存储了哪些成员。模板的名称将成为新类型的标识符,然后就可以声明这种类型的结构变量。

共用体,可以存储一个值,但是这个值可以是不同的类型,成员名指出了使用的模式。

指针,是被设计用来存储地址的变量的。我们说,指针指向它存储的地址。指针声明指出了指针指向的对象的类型。对指针应用解除引用操作符,将得到指针指向的位置中的值

字符串,是以空字符为结尾的一系列字符。字符串可用引号括起来的字符串常量表示,其中隐式包含了结构的空字符。可以将字符串存储在char数组中,可以用被初始化为指向字符串的 char指针表示字符串。函数strlen()返回字符串的长度,其中不包括空字符。函数strcpy()将字符串从一个位置复制到另一个位置。在使用这些函数时,应包含头文件cstringstring.h.

头文件string支持 C++string类提供了另一种对用户更为友好的字符串处理方法。具体地说,string对象将根据要存储的字符串自动调整其大小,用户可以使用赋值操作符来复制字符串。

New操作符允许在程序运行时为数据对象请求内存。该操作符返回其内存的地址,可以将这个地址赋给一个指针,程序将只能使用该指针来访问内存。如果数据对象是简单变量,则可以使用解除引用操作符(*)来获得其值;如果数据对象是数组,则可以像使用数组名那样来使用指针来访问元素;如果数据对象是结构,则可以用指针解除引用操作符(->) 来访问其成员。

指针和数 组紧密相关。如果ar是数组名,则表达式ar[i]被解释为*(ar+i),其中数组名被解释为数组第一个元素的地址。这样,数组名的作用和指针相同。反过来,可以使用数组表示法,通过指针名来访问new分配的数组中的元素。

操作符newdelete  允许显式控制何时给数据对象分配内存,何时将内存归给内存池。自动变量是在函数中声明的变量,而静态变量时在函数外部或者使用关键字static声明的变 量,这两种变量都不太灵活。自动变量在程序执行到其所属的代码块(通常是函数定义)时产生,在离开该代码块时终止。静态变量在整个程序周期内部都存在。