一、未初始化的危害
- 使用未初始化的值会导致不明确的行为
二、内置类型的初始化
- 对于内置的数据类型(char、int、float、double等),在使用前必须进行初始化,如果不初始化在使用时会出错,并且如果使用了未初始化的内置类型变量,有的编译会出错
-
int a; //未初始化。危险
-
int x = 0; //初始化
-
const char* text = "Hello World";//初始化
-
double d;
-
std::cin >> d; //以读取输入流的方式完成初始化
三、构造函数
- 对于class来说,在使用对象之前,必须使用构造函数对成员变量进行初始化
- 但是需要注意赋值和初始化的区别
构造函数中的赋值操作(非初始化)
- 例如下面的构造函数中,对成员变量的都不是初始化,而是赋值操作
class PhontNumber {};
class ABEntry
{
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhontNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhontNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhontNumber>& phones)
{
//下面的都是赋值操作,而不是初始化
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
成员初始化列表(真正的成员变量初始化)
- 如果想要对成员进行初始化,应该是在构造函数的成员初始化列表对成员数据进行初始化
class PhontNumber {};
class ABEntry
{
public:
//成员初始化列表初始化:真正的初始化
ABEntry(const std::string& name, const std::string& address,
const std::list<PhontNumber>& phones)
:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
{}
private:
std::string theName;
std::string theAddress;
std::list<PhontNumber> thePhones;
int numTimesConsulted;
};
四、为什么成员初始化列表比普通赋值效率高
- 在构造函数内对数据成员赋值而言,效率更低,因为它们先需要调用class的默认构造函数对成员数据进行默认的初始化,然后用再调用各个成员的拷贝构造来进行复制。例如在上面的在构造函数内赋值的演示案例中,先调用ABEntry的默认构造函数对theName,theAddress,thePhones进行初始化,然后运行到构造函数内部的时候,再分别调用theName,theAddress,thePhones的拷贝构造函数进行拷贝
- 成员初始化列表:成员初始化列表是在执行构造函数体前运行的代码,因此它们是真正的对成员数据进行初始化,从而避免了默认的构造函数调用以及拷贝构造等操作
五、成员变量如果是内置类型
- 成员变量如果是内置类型,即使没有进行初始化/赋值操作,那么编译器会自动为内置类型进行初始化,例如int被赋值为0等
- 但是如果成员变量是const或引用,那么就必须进行初始化(不能在构造函数体内赋值,要在成员初始化列表中),而不是赋值
- 总结:因此不论成员变量为什么类型,最好都必须进行初始化
六、成员初始化顺序
- 成员初始化列表的初始化是有顺序的,越在前面的成员变量越先被初始化,在后面的成员变量后被初始化。(与成员变量定义的顺序无关)
- 例如,上面介绍的构造函数中,theName先初始化,再接着是theAddress,thePhones,numTimesConsulted依次被初始化
-
ABEntry(const std::string& name, const std::string& address,
-
const std::list<PhontNumber>& phones)
-
:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
-
{}
- 成员初始化顺序带来的问题,参阅本文另一篇文章:https://blog.csdn.net/qq_41453285/article/details/88866625
七、静态对象初始化问题
- 静态对象的生命周期从定义开始,直到程序结束为止
-
静态成员分为哪几类?
- 非局部的静态对象:全局静态对象、定义与namespace作用域内的对象、class的静态成员
- 局部的静态对象:函数内部的局部静态变量
- 本文主要探讨在不同编译单元之间定义“非局部的静态对象”的初始化顺序问题
- 编译单元:是指产出单一目标文件的源码,通常是一个源文件加上一个头文件的组合
演示案例
- 现在有一个FileSystem类,它类似于一个文件系统,供网络上的用户使用,其定义在FileSystem.h文件中。定义如下:
//FileSystem.h
class FileSystem
{
public:
std::size_t numDisks()const;
};
extern FileSystem tfs;
- 现在客户端程序在Directory.h中建立Directory类用来处理文件系统内的目录
//Directory.h
#include "FileSystem.h"
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs.numDisks(); //使用tfs对象
}
//现在客户在主程序中创建一个Directory对象,用来放置临时文件
Directory tempDir;
- 现在问题来了:如果要使用tempDir,必须确保tfs对象在这之前已经被初始化了,否则tempDir就会用到未初始化的tfs。但是tfs对象与tempDir对象是出于不同编译单元之间的对象,那怎样才能保证tfs在tempDir使用之前已经被初始化了呢?答案是没有这种方法可以确保
对于上述演示案例的解决办法
- 幸运的是,有一个小小的设计就可以确保上面的这个问题。经需要用到的非局部静态对象封装到一个函数内,对象在函数内部被声明为static,然后函数返回一个指向这个对象的引用。用户之后调用这些函数,而不是直接调用这些对象。此处非局部静态对象被局部对象替换了。这在设计模式中,是Singleton模式的一种常见手法
- 这种手法保证:函数内的局部静态对象会在“该函数被调用期间,第一次遇到该对象的定义时”时被初始化
- 更改FileSystem.h
//FileSystem.h
class FileSystem
{
public:
std::size_t numDisks()const;
};
FileSystem& tfs()
{
static FileSystem tfs;
return fs;
}
- 更改Directory.h
//Directory.h
#include "FileSystem.h"
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs.numDisks(); //使用tfs对象
}
//这个函数用来替换tempDir对象,它在Directory内部可能是static
Directory& tempDir()
{
static Directory td;
return td;
}
七、总结
- 为内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初始化列表,而不要再构造函数体内部做赋值操作(初始化和赋值不是同一概念)
- 为避免“跨编译单元之初始化次序”问题,请以局部静态对象替换非局部静态对象