条款四:确认对象使用前已经被初始化
读取未初始化的值会导致不明确的行为。
最佳的处理方法是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,我们必须手动完成初始化。
int x=0;
const char* c="hellowoeld";
double b;
cin>>b;
在内置类型以外的任何东西,初始化的责任应该有构造函数完成。
确保每一个构造函数都将其每一个成员变量初始化。
不要混淆赋值和初始化。
下列函数是在赋值,c++规定,对象的成员变量的初始化发生在进入构造函数本体之前。
class PhoneNumber
{ ...
};
class ABEntry
{
public:
PhoneNumber(const string name,const string address,const list<PthoneNumer> phone);
private:
string thename;
string theaddress;
list<PhoneNumber> thePhone;
};
PhoneNumber::PhoneNumber(const string name,const string address,const list<PthoneNumer> phone)
{
thename=name;
theaddress=address;
thePhone=phone;
}
正确的写法如下,使用成员初始列替换赋值操作。
PhoneNumber::PhoneNumber(const string name,const string address,const list<PthoneNumer> phone): thename(name),
theaddress(address),
thePhone(phone)
{
}
两个代码运行结果相同,但第二个通常效率更高。因为基于赋值的版本会先调用default构造函数为成员变量赋初值,然后在对其赋新值。对大多数类型而言,先调用default构造函数在调用copy构造函数效率是比直接调用copy构造函数效率低的。有些内置类型中,初始化和赋值的成本相同,但为了统一而言,一般都通过成员初始列来构造,即使你想要一个default初始化的参数,也可以用成员初始列来实现。
PhoneNumber::PhoneNumber():
thename(),
theaddress(),
thePhone()
{
}
上述代码即调用thname、theaddress等的default构造函数。
规定总是在初始列中列出所有成员变量,以免还得记住那些成员变量可以无需初值。
有些情况下成员变量属于内置类型,也一定得使用初始列,如const和引用必须要初始化,而不能被赋值。为了避免记住成员变量何时初始化、何时赋值,最简单的做法是:总是使用成员初始列。
许多class中有多个构造函数,多分成员初始列的存在就会导致不必要的重复,这情况下可以将那些初始化和赋值效率差不多的变量改用赋值操作,并封装至一个函数里,供所有构造函数调用。
c++有固定的初始化顺序,先给基类初始化,再给派生类初始化,而其成员变量是按照其声明的次序初始化,thename、theaddress、thePhone,即使他们再成员初始列中以不同次序出现。故声明是要检查其次序,以免有隐晦错误(如数组大小)
不同编译单元内定义non-local static对象的初始化顺序
所谓编译单元是指产出单一目标文件的那些源码,包括源码文件和其所包含的头文件。
static对象是指其寿命是从被构造出来到程序结束为止。该对象包括global对象、namespace内的对象、文件内、class内、函数内被声明为static的对象。
函数内的static对象称为local static对象,其他均为non-local static对象。
问题:如果某个编译单元内的non-local static对象的初始化动作调用了另一个单元的non-local static对象,它所用到的这个对象可能未初始化。
接下来有一个实例
class FileSystem
{public:
size_t numDisk();
};
extern FileSystem tfs;
class Directory
{
public:
Directory(params);
};
Directory::Directory(params)
{
size_t disks=tfs.numDisks();//使用tfs对象
};
Directory temper(params);//临时文件
除非再调用temper之前tfs已经被初始化,否则就会使用未初始化的tfs。可如何确保tfs会先于temper初始化呢?
答案是基本不能保证。
我们可以进行一个小小的设计:将每个non-local static对象搬到自己的专属函数中,这些函数返回一个引用指向它所含的对象,然后用户调用这些函数,而不直接涉及到对象。换句话说,non-local static对象被local static对象替代了。
上述代码更改如下
class FileSystem
{public:
size_t numDisk();
};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory
{
public:
Directory(params);
};
Directory::Directory(params)
{
size_t disks=tfs().numDisks();
};
Directory& temper()
{
static Directory td;
return td;
}
不同的是,使用tfs()和temper()而不再是tfs和temper,使用函数返回的引用,而不是直接使用对象。
- 为内置对象进行手工初始化,因为c++不保证初始化他们。
- 构造函数最好使用成员初值列,成员初值列中的变量应与声明时一致。
- 为免除“跨编译单元的初始化次序问题”,应用local static替换non-local static对象。
条款五:了解c++默默编写并调用哪些函数
如果一个空类,你没有声明,则编译器会为它声明一个拷贝构造函数、一个=重载操作符、一个析构函数。此外若没有声明任何构造函数,编译器还会声明一个默认构造函数。所有这些函数都是inline且public的。
class Empty{};
//等价于
class Empty{
public:
empty(){}
Empty(const Empty& rhs){}
~Empty(){}
Empty& operator=(const Empty& rhs){}
};
唯有这些函数被调用时,他们才会被编译器创建出来。
Empty e1;
Empty e2(e1);
e2=e1;
默认构造函数和析构函数会给编译器一个地方来放隐藏的代码,如基类的构造函数和析构函数。
编译器构造出的析构函数时非虚函数,除非其基类声明有虚函数。
如果声明一个构造函数,编译器则不再提供默认构造函数。如果自己声明的是无参或者有参构造、则编译器还是会给一个默认的拷贝构造和=运算符重载函数。
但一般只有代码合法且有意义、编译器才会构造等号运算符重载,否则编译器不会默认生成等号运算符重载。
例子如下
class NameObject
{
public:
NameObject(string& name,const int &value);
private:
string &m_name;
const int & m_value;
};
NameObject p("per",2);
NameObject s("old",36);
p=s;
//引用对象不能被改变,所以类不会为其构造等号运算符重载函数
还有一种情况是基类将其等号运算符重载函数设为private,派生类也不会为其创建运算符重载函数。
总结:
- 编译器可以为类创建默认构造函数、拷贝构造函数、析构函数、等号运算符重载函数
条款六:若不想使用编译器自动生成的函数,就该明确拒绝
一般如果我们不希望类支持某功能,不声明对应函数即可,然而拷贝构造函数和等号运算符重载函数当有人调用它们时,编译器会自动构造。为了阻止编译器自动构造,我们可以将其声明为private,且不予实现。
一般而言这个方法不绝对安全,因为成员函数和友元依旧可以访问。在c++11之后,我们可以将该两个函数删除.
class pthreadpool{
private:
CThreadPool(const CThreadPool &) = delete;
CThreadPool &operator=(const CThreadPool &) = delete;
};
effective c++中提出,可以将基类的拷贝构造和等号运算符重载声明为private,然后由我们继承。
总结:
为驳回编译器自动机能,我们可以将成员函数声明为private,或者利用基类,c++11以后,我们可以直接将其删除。