C/C++编程:成员初始化列表器列表

永远在使用对象之前先将它初始化

一个编程原则是:永远在使用对象之前先将它初始化

内置类型:手动

  • 对于内置类型,建议必须手动完成初始化
int x = 0;
const char *text = "A c-stype string";

double d;
std::cin >> d;  // 以读取input stream的方式完成初始化

类类型:成员初始化列表

  • 对于类类型,在构造函数中对每一个成员进行初始化。 注意:注意是初始化而不是赋值
    • 初始化类的成员有两种方式:
      • 使用初始化列表
      • 在构造函数体内进行赋值操作
    • 一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。
  • 许多类中共有多个构造函数,每个构造函数都有自己的成员初始化列表。多个成员初始化列表的存在就会导致不受欢迎的重复和无聊的工作,这时候可以使用委托构造函数

实现:什么是初始化列表

这里是赋值

class Entry{
public:
	// 不推荐的做法
    Entry(int num, const std::string & address, const std::string & name){  // 先调用无参构造函数,然后调用赋值运算符
        m_num = num;   
        m_address = address; // 不是初始化而是赋值
        m_name = name;
    }
private:
    int m_num;
    std::string m_address;
    std::string m_name;
};

这里是初始化列表

class Entry{
public:
	// 推荐
    Entry(int num, const std::string & address, const std::string & name)
        : m_num(num),
          m_address(address),
          m_name(name){   // 初始化列表调用的是拷贝构造函数
        }
private:
    int m_num;
    std::string m_address;
    std::string m_name;
};

其专有名词叫做成员初始化列表成员初始化列表器列表是以:开头,后跟一系列以,分隔的初始化字段

如果不想给成员变量任何参数呢?

也推荐使用成员初始化列表,只要指定nothing作为初始化实参即可:

 Entry()
        : m_num(0),   // 记得将m_num显示初始化为0
          m_address(), // 调用m_address的默认构造函数
          m_name(){
        }

原因:为什么推荐用初始化列表而不是第一版呢?

原因

  • 第二版的效率更高:对于类类型来说,使用初始化列表少了一次调用默认构造函数的过程
  • 为了代码一致性
  • 如下情况下必须用初始化列表
    • 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
    • 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写到初始化列表里面
    • 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。

详细解释

(1)效率

  • 构造函数的执行可以分为两个阶段,初始化阶段和计算阶段

    • 初始化阶段
      • 所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中
    • 计算阶段:
      • 一般用于执行函数体内的赋值操作。
  • 举个例子,在本例第一个版本中,如果构造一个对象,将会自行如下操作:

    • 先调用m_address、m_name这些成员的默认构造函数初始化成员
    • 然后进入Entry构造函数本体,在这里面,又会重新对m_name 、m_address进行一次拷贝赋值
  • 对于第二个版本来说,如果构造一个对象,将会自行如下操作:

    • 先调用m_address、m_name这些成员的拷贝构造函数初始化成员变量
    • 然后进入Entry构造函数本体,在这里面,是空的,什么也不干。

可见,对于类类型来说,使用初始化列表少了一次调用默认构造函数的过程,第二版的效率更高

(2)代码一致性

  • 对于nums来说,m_num是内置类型,我们无需关心它是什么时候赋值的,因为它初始化和赋值的成本相同。但是为了一致性最好也通过成员初值列表来初始化。

(3)有时候必须使用初始化列表

  • 当成员变量是const或者references,它们就一定需要初值,不能被赋值
  • 因此,为避免记住成员变量什么时候必须在成员列表中初始化,什么时候不需要,最简单的做法是:总是使用成员初始化列表

以local static对象替换non-local static对象

C++有着十分固定的成员初始化次序

  • 基类更早于派生类被初始化
  • 类的成员变量总是以声明次序被初始化

一旦你已经很小心的将内置型成员变量明确的初始化了,而且也确保你的构造函数运用成员初始化列表初始化基类和成员变量,那就只剩下一件事: 不同编译单元中定义的no-local static对象的初始化次序:

  • 所谓static对象:

    • 其寿命从被构造出来直到程序结束为止,因此stack和heap-base对象都被排除。
    • 这种对象包括全局对象、定义于命名空间作用域内的对象、在类内、在函数内、在file作用域内被声明为static的对象
    • 函数内的静态对象被称为local static对象(因为它们对函数而言是local),其他的静态对象被称为no-local static对象。
    • 程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时自动调用
  • 所谓编译单元:

    • 指产生单一模板文件的那些源码
    • 基本上它是单一源码文件加上其所含有(#include)的头文件

现在的问题是:如果某个编译单元内的每个non-local static对象的初始化动作使用了另一个编译单元内的每个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对定义于不同编译单元内的non-local static对象的初始化次序无定义

为什么C++对定义于不同编译单元内的non-local static对象的初始化次序无定义? 原因:

  • 决定它们的初始化次序相当困难,非常困难,甚至无解。
  • 最常见的形式,也就是多个编译单元内的non-local static对象经由模版隐式具现化(implicit template instantiations)形成,不但不可能决定正确的初始化次序,甚至往往不值得寻找可决定正确次序的特殊情况。

那怎么解决呢?

  • 将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。
  • 这些函数返回一个引用指向它所含对象。
  • 然后用户调用这些函数,而不直接涉及这些对象

换句话说,non-local static对象被local static对象替换了。这是单例模式的一个常见实现手法。

这是手法的基础在于:

  • C++保证,函数内的local static对象会在该函数被调用期间首先遇到该对象的定义时被初始化
  • 所以如果你以函数调用(返回一个引用指向local static对象)替换直接访问non-local static对象
class FileSystem{
public:
    std::size_t  numDisk() const ;
};
FileSystem& tfs(){
    static FileSystem fs;
    return fs;
}

class Directory{
public:
    Directory(){
        std::size_t  disks = tfs().numDisk();
    }
};


Directory& tempDir(){
    static Directory td;
    return td;
}

问题: 怎么在在单线程环境中合理返回reference指向一个local static对象

成员变量的初始化顺序

成员变量的构造和析构时机

  • 类自身的构造函数在其函数体执行之前会先调用成员的构造列表
    • 成员的构造函数按照成员在类中声明的顺序调用,而不是按照成员在初始化器列表中出现的顺序
    • 为了避免混肴,最好按照成员的声明顺序指定初始化器
  • 在类自身的析构函数的函数体执行完毕后,会按照相反的顺序调用成员的析构函数

成员变量的初始化顺序

列表中的成员初始化器的初始化顺序与出现顺序无关,实际上,初始化的实际顺序如下:

  • 如果构造函数是最终派生类,则按基类声明的深度优先、从左到右的遍历中出现顺序(从左到右指的是基说明符列表中所呈现的),初始化各个虚基类
  • 然后,以在此类的基类说明符列表中出现的从左到右顺序,初始化各个直接基类
  • 然后,以类定义中的声明顺序,初始化各个非静态成员
  • 最后,指向构造函数体

(注意:如果初始化的顺序是由不同构造函数中的成员初始化器列表中的出现所控制,那么析构函数就无法确保销毁顺序是构造顺序的逆序了)

#include <fstream>
#include <string>
#include <mutex>
 
struct Base {
    int n;
};   
 
struct Class : public Base
{
    unsigned char x;
    unsigned char y;
    std::mutex m;
    std::lock_guard<std::mutex> lg;
    std::fstream f;
    std::string s;
 
    Class ( int x )
      : Base { 123 }, // 初始化基类
        x ( x ),      // x(成员)以 x(形参)初始化
        y { 0 },      // y 初始化为 0
        f{"test.cc", std::ios::app}, // 在 m 和 lg 初始化之后发生
        s(__func__),  //__func__ 可用,因为初始化器列表是构造函数的一部分
        lg ( m ),     // lg 使用已经初始化的 m
        m{}           // m 在 lg 前初始化,即使此出它最后出现
    {}                // 空复合语句
 
    Class ( double a )
      : y ( a+1 ),
        x ( y ), // x 将在 y 前初始化,其值不确定
        lg ( m )
    {} // 基类初始化器未出现于列表中,它被默认初始化(这不同于使用 Base(),那是值初始化)
 
    Class()
    try // 函数 try 块始于包含初始化器列表的函数体之前
      : Class( 0.0 ) // 委托构造函数
    {
        // ...
    }
    catch (...)
    {
        // 初始化中发生的异常
    }
};
 
int main() {
    Class c;
    Class c1(1);
    Class c2(0.1);
}

实验设计

初始化列表

实验:使用初始化列表少了一次调用默认构造函数的过程

下面的代码定义两个结构体,其中Test1有构造函数,拷贝构造函数及赋值运算符,为的是方便查看结果。Test2是个测试类,它以Test1的对象为成员,我们看一下Test2的构造函数是怎么样执行的。

#include <iostream>
using namespace std;
struct Test1
{
    Test1() // 无参构造函数
    {
        cout << "Construct Test1" << endl ;
    }

    Test1(const Test1& t1) // 拷贝构造函数
    {
        cout << "Copy constructor for Test1" << endl ;
        this->a = t1.a ;
    }

    Test1& operator = (const Test1& t1) // 赋值运算符
    {
        cout << "assignment for Test1" << endl ;
        this->a = t1.a ;
        return *this;
    }

    int a ;
};

struct Test2
{
    Test1 test1 ;
    Test2(Test1 &t1)  // 先调用无参构造函数,然后调用赋值运算符
    {
        test1 = t1 ;
    }
};
struct Test3
{
    Test1 test1 ;
    Test3(Test1 &t1): test1(t1 ){}   // 初始化列表调用的是拷贝构造函数
};
int main(){
    Test1 t1;   // // 无参构造函数
    printf("\n");
    Test2 t2(t1);
    printf("\n");
    Test3 t3(t1);
}

在这里插入图片描述

解释一下:

  • 第一行输出结果对应调用代码中第一行,构造一个Test1对象
  • 第二行输出对应Test2构造函数中的代码,用默认的构造函数初始化对象test1,这就是所谓的初始化阶段。
  • 第三行输出对应Test1的赋值运算符,对test1执行赋值操作,这就是所谓的计算阶段。

实验:没有默认构造函数的类,必须放在初始化列表中

using namespace std;
struct Test1
{
    Test1(int a):i(a){}
    int i ;
};

struct Test2
{
    Test1 test1 ;
    Test2(Test1 &t1)
    {
        test1 = t1 ;
    }
};

以上代码无法通过编译,因为Test2的构造函数中test1 = t1这一行实际上分成两步执行。

  1. 调用Test1的默认构造函数来初始化test1

  2. 调用Test1的赋值运算符给test1赋值

但是由于Test1没有默认的构造函数,所谓第一步无法执行,故而编译错误。正确的代码如下,使用初始化列表代替赋值操作。

struct Test2
{
    Test1 test1 ;
    Test2(Test1 &t1):test1(t1){}
}

实验:初始化列表与就地初始化

#include <iostream>

using namespace std;

struct Mem{
    Mem() {
        printf(" Mem default, num = %d\n", num);
    }
    Mem(int i) : num(i) {  // 初始化列表
        printf(" Mem default, num = %d\n", num);
    }
    int num = 2;  // 就地初始化
};

class Group{
public:
    Group(){
        printf("Group defautl, Val: %d\n", val);
    }
    Group(int i) : val('G'), a(i){  // G //> 71
        printf("Group defautl, Val: %d\n", val);
    }
    void NumofA(){
        printf("numer of A: %d\n", a.num);
    }
    void NumofB(){
        printf("numer of A: %d\b", b.num);
    }
private:
    char val{'g'};  // 103
    Mem a;
    Mem b{19};
};


int main(){
    Mem mem;  // 2
    Group group; //Mem a; --> Mem b{19}; -->  Group()
    group.NumofA();   // 2
    group.NumofB();   // 19

    Group group1(7);  // a(i) -->  b{19} --> Group(i)
    group1.NumofA();
    group1.NumofB();

}

在这里插入图片描述
从上面可以看出:

  • 默认先执行就地初始化
  • 初始化列表的效果会覆盖就地初始化

总结

综上:为了避免在对象初始化之前过早的使用它们,你需要做三件事。

  • 第一,手动初始化内置型内成员对象,因为C++不保证初始化它们
  • 第二:
    • 构造函数最好使用成员初始化列表,而不要在构造函数本体中使用赋值操作。
    • 初始化列表中列出的成员变量,其排列顺序应该和它们在类中的声明次序相同
  • 第三,在初始化次序不确定型,为避免跨编译单元的初始化次序问题,请以local static对象替换non-local static对象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值