条款04:确定对象被使用前已被初始化
内置类型、STL
对于内置类型,像int、double等,其初始值都是垃圾值。书中建议:永远在使用对象之前先将它初始化。
而对于STL来说,C++已经为这些容器提供了构造函数,会自动地初始化,我们可以直接使用而可以不初始化。
自定义类
确保每一个构造函数都将对象的每一个成员初始化
我们还需要分清初始化与赋值。对于一个类,比如:
class A
{
private:
int x;
double y;
public:
A()
{
x = 0;
y = 0.0;
}
};
这是赋值。因为初始化发生在调用默认构造函数时,是在进入该构造函数之前,成员变量赋为了垃圾值,而上述方法只是进入构造函数之后新值覆盖旧值。而只有用如下这样的参数列表的形式,才是初始化。
class A
{
private:
int x;
double y;
public:
A() : x(0), y(0.0){}
};
就效率上来说,用参数列表的方法效率更高。第一种方法首先调用默认构造函数进行初始化,而后再调用一个构造函数将其赋予新值。而第二种方法直接将参数列表中的各个成员变量作为实参,构造函数本体不必有任何动作。
此外,如果当成员变量是const或reference,就必须初始化而不能被赋值。
不同编译单元内定义的non-local static 对象
static对象:生命期从被构造出来知道程序结束(main()结束后自动调用析构函数)。包括:global对象、定义于namespace作用域内的对象、class内、函数内、file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他的成为non-local static对象。
编译单元:产出单一目标文件的源码
这个话题讨论的情况是多个文件。
文件1中如下定义:
int x = 1;
文件2中使用该变量:
extern int x;
int y = x * x;
显而易见的是,文件2中使用的x必须依赖于文件1中的初始化,如果文件1的初始化在文件2之后,则文件2使用的就是垃圾值。产生这个问题的原因是:C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。因为决定它们的初始化次序相当困难。所以我们需要用巧妙的设计模式(singleton)避免这种问题:将每个non-local static对象搬到自己的专属函数内,并在该函数中声明为static。
所以我们可以将文件一修改成一个函数:
int& GetX()
{
static int x = 1;
return x;
}
在文件2中调用该函数:
int y = GetX() * GetX();
使用函数返回的“指向static对象”的引用,而不再使用static对象自身,这样在调用函数时已经保证x已经初始化了。
条款05:了解C++默默编写并调用哪些函数
一个空类,如果没有声明,编译器会为其声明:
默认构造函数
拷贝构造函数
析构函数
赋值运算符重载
他们都是public的,inline的。
对于构造函数和赋值运算符,需要特别分清:
有一个类A,实例化一个对象a,有三种初始化的方式:
①A a( b );
②A a = b;
③A a;
a = b;
方法①,一目了然,调用的是拷贝构造函数
而方法②,调用的是拷贝构造函数。方法③,调用的是赋值运算符。
虽然②和③都使用了"=",但是区别在于是否在使用"="使产生了新的对象。方法③中在"="之前已经产生了新的对象,所以调用的是赋值运算符。
当一个类的成员变量中有const类型和引用类型时,C++会拒绝生成赋值运算符的重载。因为在调用构造函数的时候,已经为他们初始化。如果我们试图调用赋值运算符的重载,会报如下错误:
所以我们需要自己为他们定义重载赋值运算符。
如果我们不用赋值运算符重载(方法③)的话,而是用拷贝构造函数(方法①或②),那么也是可以的。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
该条款要说明的问题是:不要让客户代码调用类的拷贝构造函数和赋值函数。但是上一条款说过,这两个函数如果已经定义,就能使用。即使没有定义,编译器也会默默地帮我们定义,所以看似用户代码无论如何都能调用。
这种问题的解决方法就是自己写拷贝构造函数和赋值函数,而且把它们写在private中,这样就解决了这个问题。
但是如果遇上友元函数或者友元类呢?它们可以访问private下的成员函数。所以我们需要再进一步的改动:将它们的声明放在一个基类中,一个专门为阻止调用这两个函数的基类。