关于类的初始化,使用初始化列表和在构造函数中初始化好像没啥区别吧?

时间:2021-06-30 19:39:19
根据迷信书时的做法,比如C++ Premier,都提倡能在出屎在列表中出屎化就写在列表中,并且在MFC中,VC6不算,比如2008,如果新加一个变量,也会被自动用初始化列表初始化,VC6好像是构造函数,要是没记错。

但是最近开始按大牛的说法迷信编译器啥的。

发现,根本没区别吧。

比如说。


class Plane
{
public:
int id;
int ip;

Plane():id(15),ip(7)
{
id = 16;
ip = 8;
}
~Plane()
{

}
};

int main(int argc, int argv[])
{
Plane p1;

return 0;
}

初始化这段根本就是直接的嘛。


00411473  mov         eax,dword ptr [this] 
00411476  mov         dword ptr [eax],0Fh 
0041147C  mov         eax,dword ptr [this] 
0041147F  mov         dword ptr [eax+4],7 
    10:  id = 16;
00411486  mov         eax,dword ptr [this] 
00411489  mov         dword ptr [eax],10h 
    11:  ip = 8;
0041148F  mov         eax,dword ptr [this] 
00411492  mov         dword ptr [eax+4],8 
    12:  }


也就是说如果不用初始化列表初始化,也是会直接按顺序进入构造函数初始化的。

也就是说这个神马出屎化列表完全多余吧....

完全不明白id(15)这种写法会比在构造函数中id = 15更方便或是什么么?

6 个解决方案

#1


如果你的成员是一个类而不是内置类型或指针,就有区别了,初始化列表效率高

const类型也得用初始化列表

#2


effective c++说过这事,等下,我去找,复制来。

#3


条款12: 尽量使用初始化而不要在构造函数里赋值

看这样一个模板,它生成的类使得一个名字和一个t类型的对象的指针关联起来。

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  string name;
  t *ptr;
};

(因为有指针成员的对象在进行拷贝和赋值操作时可能会引起指针混乱(见条款11),namedptr也必须实现这些函数(见条款2))

在写namedptr构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表:

template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr  )
: name(initname), ptr(initptr)
{}

第二种方法是在构造函数体内赋值:

template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr)
{
  name = initname;
  ptr = initptr;
}

两种方法有重大的不同。

从纯实际应用的角度来看,有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。所以,如果想让namedptr<t>对象不能改变它的名字或指针成员,就必须遵循条款21的建议声明成员为const:

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  const string name;
  t * const ptr;
};

这个类的定义要求使用一个成员初始化列表,因为const成员只能被初始化,不能被赋值。

如果namedptr<t>对象包含一个现有名字的引用,情况会非常不同。但还是要在构造函数的初始化列表里对引用进行初始化。还可以对名字同时声明const和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读的对象。

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  const string& name;               // 必须通过成员初始化列表
                                    // 进行初始化

  t * const ptr;                    // 必须通过成员初始化列表
                                    // 进行初始化
};

然而前面最初的类模板不包含const和引用成员。即使这样,用成员初始化列表还是比在构造函数里赋值要好。这次的原因在于效率。当使用成员初始化列表时,只有一个string成员函数被调用。而在构造函数里赋值时,将有两个被调用。为了理解为什么,请看在声明namedptr<t>对象时都发生了些什么。

对象的创建分两步:
1. 数据成员初始化。(参见条款13)
2. 执行被调用构造函数体内的动作。

(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)

对namedptr类来说,这意味着string对象name的构造函数总是在程序执行到namedptr的构造函数体之前就已经被调用了。问题只在于:string的哪个构造函数会被调用?

这取决于namedptr类的成员初始化列表。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。

相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。

即使是一个很简单的string类型,不必要的函数调用也会造成很高的代价。随着类越来越大,越来越复杂,它们的构造函数也越来越大而复杂,那么对象创建的代价也越来越高。养成尽可能使用成员初始化列表的习惯,不但可以满足const和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会。

换句话说,通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。另外,它简化了对类的维护(见条款m32),因为如果一个数据成员以后被修改成了必须使用成员初始化列表的某种数据类型,那么,什么也不用变。

但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。例如,这里有个类可以用来说明这种情形:

class manydatambrs {
public:
  // 缺省构造函数
  manydatambrs();

  // 拷贝构造函数
  manydatambrs(const manydatambrs& x);

private:
  int a, b, c, d, e, f, g, h;
  double i, j, k, l, m;
};

假如想把所有的int初始化为1而所有的double初始化为0,那么用成员初始化列表就要这样写:

manydatambrs::manydatambrs()
: a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0),
  j(0), k(0), l(0), m(0)
{ ... }

manydatambrs::manydatambrs(const manydatambrs& x)
: a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0),
  j(0), k(0), l(0), m(0)
{ ... }

这不仅仅是一项讨厌而枯燥的工作,而且从短期来说它很容易出错,从长期来说很难维护。

然而你可以利用固定数据类型的(非const, 非引用)对象其初始化和赋值没有操作上的不同的特点,安全地将成员初始化列表用一个对普通的初始化函数的调用来代替。

class manydatambrs {
public:
  // 缺省构造函数
  manydatambrs();

  // 拷贝构造函数
  manydatambrs(const manydatambrs& x);

private:
  int a, b, c, d, e, f, g, h;
  double i, j, k, l, m;

  void init();        // 用于初始化数据成员
                      
};

void manydatambrs::init()
{
  a = b = c = d = e = f = g = h = 1;
  i = j = k = l = m = 0;
}

manydatambrs::manydatambrs()
{
  init();

  ...

}

manydatambrs::manydatambrs(const manydatambrs& x)
{
  init();

  ...

}

因为初始化函数只是类的一个实现细节,所以当然要把它声明为private成员。

请注意static类成员永远也不会在类的构造函数初始化。静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去“初始化”它们没有任何意义。至少这会影响效率:既然是“初始化”,那为什么要去做多次?而且,静态类成员的初始化和非静态类成员有很大的不同,这专门有一个条款m47来说明。

#4


原来如此,看来还得多补几本书,然后挨个儿例子反汇编......

#5


构造函数分两个阶段进行:
1.初始化阶段(初始化列表部分)
2.计算阶段(函数体部分)
无论是否提供初始化列表,构造函数都会先进行第一个阶段,在进行第二个阶段。
因此,如果类数据成员是没有默认构造函数的类类型,若不在初始化列表中显示指定初始化式就会出错。
同样地,由于const类型和引用类型不能被赋值,只能被初始化,他们作为类成员只能在初始化列表中初始化。

#6


初始化和赋值不是一回事.

#1


如果你的成员是一个类而不是内置类型或指针,就有区别了,初始化列表效率高

const类型也得用初始化列表

#2


effective c++说过这事,等下,我去找,复制来。

#3


条款12: 尽量使用初始化而不要在构造函数里赋值

看这样一个模板,它生成的类使得一个名字和一个t类型的对象的指针关联起来。

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  string name;
  t *ptr;
};

(因为有指针成员的对象在进行拷贝和赋值操作时可能会引起指针混乱(见条款11),namedptr也必须实现这些函数(见条款2))

在写namedptr构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表:

template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr  )
: name(initname), ptr(initptr)
{}

第二种方法是在构造函数体内赋值:

template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr)
{
  name = initname;
  ptr = initptr;
}

两种方法有重大的不同。

从纯实际应用的角度来看,有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。所以,如果想让namedptr<t>对象不能改变它的名字或指针成员,就必须遵循条款21的建议声明成员为const:

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  const string name;
  t * const ptr;
};

这个类的定义要求使用一个成员初始化列表,因为const成员只能被初始化,不能被赋值。

如果namedptr<t>对象包含一个现有名字的引用,情况会非常不同。但还是要在构造函数的初始化列表里对引用进行初始化。还可以对名字同时声明const和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读的对象。

template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  const string& name;               // 必须通过成员初始化列表
                                    // 进行初始化

  t * const ptr;                    // 必须通过成员初始化列表
                                    // 进行初始化
};

然而前面最初的类模板不包含const和引用成员。即使这样,用成员初始化列表还是比在构造函数里赋值要好。这次的原因在于效率。当使用成员初始化列表时,只有一个string成员函数被调用。而在构造函数里赋值时,将有两个被调用。为了理解为什么,请看在声明namedptr<t>对象时都发生了些什么。

对象的创建分两步:
1. 数据成员初始化。(参见条款13)
2. 执行被调用构造函数体内的动作。

(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)

对namedptr类来说,这意味着string对象name的构造函数总是在程序执行到namedptr的构造函数体之前就已经被调用了。问题只在于:string的哪个构造函数会被调用?

这取决于namedptr类的成员初始化列表。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。

相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。

即使是一个很简单的string类型,不必要的函数调用也会造成很高的代价。随着类越来越大,越来越复杂,它们的构造函数也越来越大而复杂,那么对象创建的代价也越来越高。养成尽可能使用成员初始化列表的习惯,不但可以满足const和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会。

换句话说,通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。另外,它简化了对类的维护(见条款m32),因为如果一个数据成员以后被修改成了必须使用成员初始化列表的某种数据类型,那么,什么也不用变。

但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。例如,这里有个类可以用来说明这种情形:

class manydatambrs {
public:
  // 缺省构造函数
  manydatambrs();

  // 拷贝构造函数
  manydatambrs(const manydatambrs& x);

private:
  int a, b, c, d, e, f, g, h;
  double i, j, k, l, m;
};

假如想把所有的int初始化为1而所有的double初始化为0,那么用成员初始化列表就要这样写:

manydatambrs::manydatambrs()
: a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0),
  j(0), k(0), l(0), m(0)
{ ... }

manydatambrs::manydatambrs(const manydatambrs& x)
: a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0),
  j(0), k(0), l(0), m(0)
{ ... }

这不仅仅是一项讨厌而枯燥的工作,而且从短期来说它很容易出错,从长期来说很难维护。

然而你可以利用固定数据类型的(非const, 非引用)对象其初始化和赋值没有操作上的不同的特点,安全地将成员初始化列表用一个对普通的初始化函数的调用来代替。

class manydatambrs {
public:
  // 缺省构造函数
  manydatambrs();

  // 拷贝构造函数
  manydatambrs(const manydatambrs& x);

private:
  int a, b, c, d, e, f, g, h;
  double i, j, k, l, m;

  void init();        // 用于初始化数据成员
                      
};

void manydatambrs::init()
{
  a = b = c = d = e = f = g = h = 1;
  i = j = k = l = m = 0;
}

manydatambrs::manydatambrs()
{
  init();

  ...

}

manydatambrs::manydatambrs(const manydatambrs& x)
{
  init();

  ...

}

因为初始化函数只是类的一个实现细节,所以当然要把它声明为private成员。

请注意static类成员永远也不会在类的构造函数初始化。静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去“初始化”它们没有任何意义。至少这会影响效率:既然是“初始化”,那为什么要去做多次?而且,静态类成员的初始化和非静态类成员有很大的不同,这专门有一个条款m47来说明。

#4


原来如此,看来还得多补几本书,然后挨个儿例子反汇编......

#5


构造函数分两个阶段进行:
1.初始化阶段(初始化列表部分)
2.计算阶段(函数体部分)
无论是否提供初始化列表,构造函数都会先进行第一个阶段,在进行第二个阶段。
因此,如果类数据成员是没有默认构造函数的类类型,若不在初始化列表中显示指定初始化式就会出错。
同样地,由于const类型和引用类型不能被赋值,只能被初始化,他们作为类成员只能在初始化列表中初始化。

#6


初始化和赋值不是一回事.