确定对象被使用前已先被初始化

2022-09-06 22:10  
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 对象。





小马过河啊
要好好学习呀!
 最新文章