Cpp_Basic---15/N(初始化列表)

2017-01-12 09:53:42来源:oschina作者:OSer_Merlin人点击

第七城市

老生常谈的问题了, 总结一下.


修订历史:

初稿(初始化机制的出现: 语言机制? 效率? & 一些注意)---2017/1/11
修订1(深入挖掘初始化列表; 建议)----2017/1/11晚 Why?

初始化列表的出现, 到底是由于语言的限制, 还是为了效率有意为之.


语言机制:

因为, 从使用上看, 初始化 != 赋值, 这就导致了如果成员变量是const, 引用时只能采用初始化,如果数据成员是对象时, 在初始化本类对象或者非对象数据成员前, 就要初始化完成成员对象, 所以也不能放在构造器函数体中, 故而只能放在初始化列表中.


总结如下, 只能使用初始化列表而不能在构造器函数体中的情况:

需要初始化的数据成员是对象的情况(类成员没有默认构造函数)
需要初始化const修饰的类成员
需要初始化引用成员数据
继承中, 基类的默认构造函数没有提供或者被shadow, 需要显示调用基类构造器(派生类必须在其初始化列表中调用基类的构造函数, 默认的可以自动调用, 一旦默认的没有了, 就要在初始化列表中手工调用)

案例:


class Person {
public:
  Person() { } //default constructor
  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;
};

上面的代码, 很显然, 编译报错: 对const变量进行赋值, 坚决不允许. (此外注意, 所有实现, 请到类定义的外面, 不要搞的"很不专业的样子")


Person(string name, string phone, string addr)
:m_name(name), m_phone(phone), m_addr(addr){ }

从这个const的例子可以看到, 由于语言中const的限制, 导致了只能使用初始化列表, 而不能采用函数体赋值的方式. 然而事实的另一方面是.....

效率:

"没有使用初始化列表, 是先创建好对象(此时它的数据成员内存已经分配, 但是没有初始化), 之后通过赋值进行修改"; 这种方式的效率是比不上,"在创建对象, 分配数据成员内存的时候, 就进行初始化"的. (从内存寻址去理解) 你想想赋值效率高, 还是初始化效率高就知道了.


详细内容见下面.

注意:(初始化次序, 虚基类问题)1. 数据成员被初始化的顺序与构造函数初始化列表中的次序无关,而是与成员的定义顺序一致。
2. 在虚基类问题中, 末端子类可能需要在初始化列表中手动调用顶层父类的构造器(不信你把顶层父类的默认构造器给shadow掉试试看就知道了, 见下面的demo)

例如:


class Base{
public:
Base(int i){
_ii = i;
}
private:
int _ii;
};
class B: virtual public Base{
};
class C: virtual public Base{
};
class A: public B,public C{
};

此时, 因为虚基类Base的默认构造函数没有了, 所以必须在初始化列表中显示的调用一下它的有参构造器. class B, C, A 都需要, 不然都会报错, 代码如下:


#include
using namespace std;class Base{
public:
Base(int i){
_ii = i;
}
private:
int _ii;
};
class B: virtual public Base{
public:
B(int i):Base(i){}
};
class C: virtual public Base{
public:
C(int i):Base(i){}
};
class A: public B,public C{
public:
A():B(1),C(1),Base(1){}
};
深入挖掘初始化列表
初始化列表的效率问题:
结论:对于基本类型(内建类型), 用不用初始化列表, 其实效率上都是一样(初始化和赋值);
对于对象成员数据成员, 使用初始化列表的效率, 要比构造体里的赋值效率要高. 解释:

试想一下, 对于继承中子类是如何初始化父类的对象的, 案例如下:


class Base{
public:
Base(){
cout << "Base" << endl;
}
};
class Sub: public Base{
public:
Sub():Base(){
cout << "Sub" << endl;
}
};int main(void)
{
Sub sb;
return 0;
}

其中,


Sub():Base(){
cout << "Sub" << endl;
}

如果没有初始化列表显示调用的话, 系统也会自动调用(但是父类的默认构造函数没有或者被shadow了, 那你就要在初始化列表中手动调用了), 但是并不能在构造体中进行调用.


明白了这个例子, 再来看下面, 当成员变量是类的时候, 看看在初始化列表和函数的构造体的区别:(初始化和赋值的区别)


#include
#include
using namespace std;class MString{
public:
MString();
MString(string str);
void operator=(const string str);
private:
char _str[128];
};
MString::MString(){
cout << "MString's default constructor has been called." << endl;
}
MString::MString(string str){
cout << "MString's constructor has been called." << endl;
memset(_str, 0, sizeof(_str));
strcpy(_str, str.c_str());
}
void MString::operator=(const string str){
cout << "MString's operator=() has been called." << endl;
memset(_str, 0, sizeof(_str));
strcpy(_str, str.c_str());
}
class A{
public:
A(int age, string name);
inline void print(){
cout << _age << endl;
}private:
int _age;
MString _name;
};
A::A(int age, string name){
cout << "A constructor assignment start..." << endl;
_age = 2;
string another = "wizard";
_name = another;
}int main(void)
{
string name = "merlin";
A a(1, name);
a.print();
return 0;
}

运行结果如下:


MString's default constructor has been called.
A constructor assignment start...
MString's operator=() has been called.
2

看到啦, 也就是说, 在构造A对象之前, 肯定会构造其成员变量MString _name, 并且一定会在初始化列表中调用其默认的构造器----"当用户定义类型的数据成员没有构造器列在成员初始化表中的时候,编译器会自动为其调用默认构造器"(验证的话,你把Mstring的默认构造器注释掉, 然后在A构造器的初始化列表中手动调用就知道了) 也就是说, 对于对象类型的成员, 本身就会在初始化列表中进行一遍初始化, 这个时候不进行初始化, 而是移到A的构造体在进行赋值修改,这样做效率不就是很低么? 为什么不在初始化列表中构造对象成员的时候, 即调用对象成员的构造函数的时候进行初始化呢?


由此可见, 对于对象成员, 初始化列表确实函数体里进行赋值效率要高.; 对于基本(内建)类型的成员, 用不用初始化列表, 真的无所谓.(基本类型又没有构造函数, 可以在初始化列表中被调用)初始化与赋值的开销是完全相同的.除了出于语言的限制, const和引用, 一般也将基本类型的初始化, 写在初始化列表中, 保持统一的同时, 也利于问题的排查.


始终使用初始化列表?

C++的规则严谨的同时, 其实也很死板, 很烦人, 比方说那些你不手动调用,它就调用默认的, 等规则.


始终使用初始化列表真的不是一件很好的事儿, 这就是你可能经常看到手工实现的init()函数, 这类函数通常是在类的内部私有的函数, 功能很简单就是为了初始化成员数据而存在, 特别是, 如果该类多继承(有多个基类), 有多个构造器(每个构造器都有自己的初始化列表), 有多个属性(成员变量), 那么此时, 把其中不需要一定在初始化列表中完成的成分拿出来, 抽取出来, 真的再好不过了.


先这样, 有问题, 在修改.

merlin


2017/1/11

第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台