Effective C++ 学习笔记——条款04:确定对象被使用前已先被初始化

Effective C++ 学习笔记——条款04:确定对象被使用前已先被初始化

C++语言是由多种“次语言”组成的(见条款01),因此有时定义的变量并不能如我们所想得进行初始化,即读取未初始化的值会导致不明确的行为。如:
C part of C++ 中的整型数组 int[],未初始化时其中可能包含非零初始化元素。
STL 中整型容器 std::vector,可以保证所有元素均被零初始化。

内置类型(built-in type)的初始化——C part of C++

C++的一部分基础数据类型继承于 C,因此不能保证该类型变量在定义时初始化为一定的数值。使用未初始化变量可能导致程序不正常工作。
未初始化变量如:

 int a;           //未初始化int
 double b;        //未初始化double
 char* text;      //未初始化字符指针

需改为:

 int a = 0;       //对 int 进行手动初始化
 const char* text = "A C-style string";   //对指针进行手动初始化
 double b;
 std::cin >> b;   //以读取 intput stream 的方式完成初始化

类的初始化(构造函数)

自定义类型参数的初始化依靠构造函数(constructors)完成,其原则为:确保每一个构造函数都将对象的每一个成员初始化
其难点主要在于区分 赋值(assignment)初始化(initialization)。
例如,构造一个通讯录class,其构造函数如下:

 class PhoneNumber { ... };
 class ABEntry {
 public:
      ABEntry(const std::string& name, const std::string& address, 
              const std::list<PhoneNumber>& phones);
 private:
      std::string theName;
      std::string theAddress;
      std::list<PhoneNumber> thePhones;
      int numTimesConsulted;
 };
 ABEntry::ABEntry(const std::string& name, const std::string& address, 
                  const std::list<PhoneNumber>& phones)
 {
      theName = name;          // 这些操作均是赋值,并不是初始化
      theAddress = address;    / /初始化发生在成员的默认构造函数被自动调用时
      thePhones = phones;      // 比进入构造函数本体时间早
      numTimesConsulted = 0;   // 该参数属于内置类型,与上述不同
 }

该方法本质是在默认构造函数完成后,再次对参数进行赋值,则默认构造函数的行为浪费了
正确构造函数书写方法,即使用成员初值列表(member initialization list)替代赋值操作

 ABEntry::ABEntry(const std::string& name, const std::string& address, 
                  const std::list<PhoneNumber>& phones)
      :theName(name),          // 这些操作均是初始化操作
       theAddress(address),
       thePhones(phones),
       numTimesConsulted(0)
 { }                           // 构造函数本体不需要做任何动作

该方法,初值列表中针对各个成员变量而设的实参,被拿去作为成员变量构造函数的实参
本例中 theName 以 name 为初值进行 copy构造,后两者同理,单调用一次拷贝构造函数效率更高
其中,numTimesConsulted 初始化和赋值成本相同,但推荐一直通过成员初值列表完成初始化
无参构造函数也可通过,指定无物作为初始化实参,如:

 ABEntry::ABEntry(const std::string& name, const std::string& address, 
                  const std::list<PhoneNumber>& phones)
      :theName(),              // 调用 theName、theAddress、thePhones 的默认构造函数
       theAddress(),
       thePhones(),
       numTimesConsulted(0)    // 内置 int 需要显示初始化为 0
 { }

如果成员变量是 const 或 references,就一定需要初值,不能被赋值。因此为避免错误发生,必须对所有成员使用成员初值列表完成初始化。
例如:

 const int a;                //报错,需要初始化
 int& b;                     //报错,需要初始化

 //现在对其进行初始化:
 const int a = 3;            //编译通过
 int  c = 3;
 int& b = c;                 //编译通过

在继承关系中,基类(base class)总是先被初始化。
在同一类中,成员数据的初始化顺序与其声明顺序是一致的,而不是初始化列表的顺序。
因此,为了代码一致性,要保证初始化列表的顺序与成员数据声明的顺序是一样的。

初始化非本地静态对象

在两个编译单元中,分别包含至少一个非本地静态对象,当这些对象发生互动时,它们的初始化顺序是不确定的,所以直接使用这些变量,就会给程序的运行带来风险。
编译单元(translation unit): 可以让编译器生成代码的基本单元,一般一个源代码文件就是一个编译单元。
非本地静态对象(non-local static object): 静态对象可以是在全局范围定义的变量,在名空间范围定义的变量,函数范围内定义为 static 的变量,类的范围内定义为 static 的变量,而除了函数中的静态对象是本地的,其他都是非本地的。
注意:静态对象存在于程序的开始到结束,所以它不是基于堆(heap)或者栈(stack)的。初始化的静态对象存在于 .data 中,未初始化的则存在于 .bss 中。
例如,现有以下服务器代码:

 class Server{...};
 extern Server server;                 //在全局范围声明外部对象server,供外部使用

客户端:

class Client{...};
Client::Client(...){
     number = server.number;
}

Client client;                       //在全局范围定义client对象,自动调用了Client类的构造函数

主要问题:定义对象 client 自动调用了 Client 类的构造函数,此时需要读取对象 server 的数据,但全局变量的不可控性让我们不能保证对象 server 在此时被读取时是初始化的。
试想如果还有对象 client1, client2 等等不同的用户读写,我们不能保证当前 server 的数据是我们想要的。
解决方法: 将全局变量变为本地静态变量。使用一个函数,只用来定义一个本地静态变量并返回它的引用。因为C++规定在本地范围(函数范围)内定义某静态对象时,当此函数被调用,该静态变量一定会被初始化。
解决方法如下:

 class Server{...};

 Server& server(){                         //将直接的声明改为一个函数
	 static Server server;
	 return server;
 }


 class Client{...};

 Client::client(){                        //客户端构造函数通过函数访问服务器数据
	 number = server().number;
 }

 Client& client(){                        //同样将客户端的声明改为一个函数
	 static Client client;
	 return client;
 }

总结

  1. 内置型对象进行手动初始化,C++代码不保证初始化参数;
  2. 构造函数使用成员初值列表,不要再构造函数内使用赋值操作。初值列表列出的成员变量,其排列次序应该和 class 中的声明次序相同;
  3. 为免除“跨编译单元的初始化次序”问题,请以 local static 对象替换 non-local static 对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值