条款04:确定对象使用前已被初始化
对象成员变量的初始化发生在进入函数构造本体之前,在函数体内成员变量不是被初始化而是被赋值
class A{
public:
A(const string &);
private:
string s;
};
A::A(const string ¶)
{
s=para; //这些是赋值而不是初始化
}
此版本首先调用default构造函数为s设初值,然后立刻赋予它们新值。default构造函数的一切因此浪费了,从而有第二个版本:
A::A(const string ¶)
:s(para) //此时是初始化
{ }
本例中s以para为初值调用copy构造函数。
对大多数类而言,比起先调用default构造函数然后在调用copy assignment操作符,单次调用copy构造函数更加高效。虽然对于许多内置类型,如int,初始化和赋值的成本相同,但为了一致性,也采用在初始化列表中进行初始化。
C++有着非常严格的初始化顺序:base class更早于其derived class被初始化,而class成员变量总是以其声明次序被初始化。在初始化列表中初始化时,最好依照声明次序进行,以避免一些错误,如:初始化arry时,需要指定大小,因此代表大小的那个成员必须先有初始值。
最后需要之一的一点是:不同编译单元内定义的non_local static对象的初始化次序
non_local static:static对象就是其寿命从被构造出来直到程序结束为止,这种对象包括global对象、定义于namespace作用域内的对象、在class、函数、以及file作用域内被声明为static的对象。函数内的static对象是称为local static对象。
编译单元(translation unit)是指产出单一目标文件(singel object file)的源代码。不同编译单元意味着至少两个源码文件,每个至少含一个non_local static对象。
真正的问题是:某个编译单元内的某个non_local static的初始化使用到了另一个编译单元内未初始化的non_local static对象
class FileSystem{
public:
std::size_t numDisk()const;
};
extern FileSystem tfs;
class Directory{
public:
Directory(params);
};
Directory::Directory(params)
{
std::size_t disks=tfs.numDisk();
}
Directory tempDir(params);
从上例中可以看出:除非tfs在tempDir之前被初始化,否则会用到未被初始化的tfs。为解决这个问题,可以将每个non_local static对象搬到属于自己的专属函数之内,这些函数返回一个reference指向它所含的对象,即:non_local static对象被local static对象替换掉了。
class FileSystem{};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{};
Directory::Directory()
{
std::size_t disks=tfs().numDisk();
}
Directory& tempDir()
{
static Directory td;
return td;
}
这么修改之后可以保证在首次调用该对象定义式时,对象被初始化,唯一不同的是使用tfs( )和tempDir( )而不再是tfs和tempDir,即:使用函数返回的指向static对象的reference而不是static自身。
请记住:
为内置类型对象进行手工初始化,因为C++不保证初始化它们
构造函数最好使用初始化列表,而不要在构造函数体内使用赋值操作。初始列表列出的成员变量,其排列次序因该和它们在class中的声明次序相同
为免除跨编译单元的初始化次序问题,请以local static替换non_local static对象
条款05:了解C++默默编写并调用哪些函数
如果自己没有声明,编译器会自动为类声明一个copy构造函数、一个copy assignment操作符和一个析构函数。并且只有当这些函数被需要时才会被编译器创造出来。 编译器所产生的析构函数是non_virtual,除非class的base class自身声明含有virtual析构函数(这种情况下此函数的虚属性来自base class)。
当自身声明了一个构造函数之后,编译器便不会再为其创建default构造函数。
编译器自动生成的copy assignment操作符,只有当代码合法并且有机会证明它有意义时,便一起才会自动构造。
template<class T> class NameObject{
public:
NameObject(std::string& name,const T &value);
private:
std::string &nameValue;
const T objectValue;
};
//当进行如下操作时
std::string new("Persephone");
std::string old("Satch");
NameObject<int> p(new,2);
NameObject<int> s(old,36);
p=s; //此时会发生什么?
C++并不允许reference改指向不同的对象,便对这个难题,编译器会拒绝为其创建copy assignment操作符,必须自己定义。
最后,如果某个base class将其copy assignment声明为private,编译器也将拒绝为其derived class生成copy assignment操作符。因为编译器为其derived class所生的copy assignment操作符想象中可以处理base class,但其无权调用(见条款7和12)。
请记住:
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
当不想使用编译器默认构造的函数时,一种方法是将其函数声明放入private之中,而且不实现它们(防止类自生成员函数调用它们)。另一种方法是为其构造一个base class来防止copying动作,此base class也是将其函数放入private中,在derived class中就不必再声明其相应的函数。当需要调用这些函数时,编译器为其构造函数,这些函数会尝试调用其base class的对应函数,而编译器会拒绝,由于其为private,所以编译器会拒绝为其自动生成函数。
请注意:
为驳回编译器自动提供的机能,可将其成员函数声明为private,并且不予实现,定义base class也是应用此种方法
条款07:为多态基类声明virtual析构函数
当base class的析构函数为non_virtual,而base class的指针实际指向的对象为derived class时,当我们delete 这个指针时,会产生“局部销毁”的现象,造成资源泄露。解决办法就是将其base class的析构函数定义为virtual析构函数。
任何class只要带有virtual函数几乎一定会有一个virtual析构函数,当一个class不含virtual函数,通常表示他并不愿意作为一个base class。
通常来说,将一个类的成员函数声明为virtual似乎是一个比较周全的做法,但这实际上会有额外的开销。以一个含有两个int类型的成员的point类为例子,若int占用32bits,一个point对象可以被当做64bits的量,传给其他语言,如C或者FORTRAN撰写的函数,但当其析构函数为virtual时,发生了变化。
欲实现virtual函数,对象必须携带一些信息,主要用来决定在运行期间决定执行哪一个virtual函数。这通常是由一个vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,成为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl。
因此含有virtual函数的class对象体积一般会膨胀50%到100%!point对象不能塞入64bits缓存器,不再具有移植性。
只有当一个class内至少含有一个virtual函数,才为它声明virtual析构函数
当需要构造抽象class时,可为这个class声明一个pure virtual析构函数:
class AWOV{
public:
virtual ~AWOV()=0; //声明为pure virtual析构函数
};
需要为pure virtual析构函数提供一份定义:从最底层开始调用析构函数时,最后需要创建一个对~AWOV的调用
AWOV::~AWOV()
{ }
给base class一个virtual析构函数,这个规则只适用于polymorphic(带多态性质的)base class身上。但并非所有的base class都是多态用途。例如string和STL容器都不被设计作为base classes使用,更别提多态了。
请记住:
polymorphic(带多态性质的)base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
class的设计目的如果不是为了base class的使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数
条款08:别让异常逃离析构函数
class Widget{
public:
~Widget() //假设这里可能会突出异常
{}
};
void doSomething()
{
std::vector<Widget> V;
} //v在这里被自动销毁
当vector v被销毁,它会销毁其内所含的所有Widgets,假设有10个Widgets,而在析构第一个元素期间,有个异常抛出,其他九个Widgets,还是应该销毁,第二个Widget析构函数又抛出异常,现在有两个同时作用的异常,这对C++来说太多了。在两个异常同时存在的情况,程序若不是结束执行就是导致不明确的行为。
如果析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该如何应对?有两个办法可以避免这一个问题,其析构函数可以:
- 如果抛出异常就结束程序。通常通过调用abort完成:
DBConn::~DBConn()
{
try{db.close();}
catch(...){
制作转运记录,记下对close的调用失败; //详见effective C++ P46
std::abort();
}
}
若果遭遇一个析构期间的异常后无法继续执行,强迫结束程序是一个合理选项。
- 吞下因调用close而发生的异常:
DBConn::~DBConn()
{
try{db.close();}
catch(...){
制作转运记录,记下对close的调用失败; //详见effective C++ P46
}
}
一般而言,将异常吞掉是个坏主意,因为它压制了某些失败动作的重要信息。
这些办法都没什么吸引力,问题在于两者都无法对“导致close抛出异常”的情况做出反应。更好的办法是在析构函数之外定义一个普通函数执行这个操作,以便用户做出反应。
请记住:
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非析构函数中)执行该操作。
条款09:决不在构造和析构过程中调用virtual函数
class Transaction{
public:
Transaction();
virtual void logTransaction()const=0; //做出一份因类型不同而不同的日志记录
};
Transaction::Transaction()
{
logTransaction();
}
class BuyTransaction:public Transaction{
public:
virtual void logTransaction()const;
};
BuyTransaction b;
BuyTransaction构造函数被调用,但首先Transaction构造函数先被调用,其构造函数最后一行调用virtual函数logTransaction,这时候调用的是base class的内的版本,而不是derived class。base class构造期间virtual函数绝对不会下降到derived class阶层。合理的解释是,当base class构造函数正执行起来打算初始化derived class对象内base class成分时,该对象是base class,而此时derived class专属成分尚未被初始化,随意面对它们最安全的做法是视他们不存在。
同理适用于析构函数,一旦derived class析构函数开始执行,执行到base class析构函数时,derived class专属对象呈现未初始化状态,该对象就被视为base class对象。
上述实例中,Transaction构造函数直接调用一个virtual函数,编译器能够识别出来,并给出一个警告信息。即使没有警告信息,这个问题在编译之前也会变得显而易见因为logTransaction函数在Transaction内是一个pure virtual。除非被定义,不然无法连接。但是,有些时候,这样的侦测不会这样轻松。
class Transaction{
public:
Transaction()
{ init();} //调用non_virtual
virtual void logTransaction()const=0;
private:
void init()
{
logTransaction(); //这里调用virtual
}
};
这段代码同上述示例,但他不会被编译器和连接器检测出来,而当我们建立一个derived class对象时,会调用一个base class的错误版本的logTransaction。解决办法是将logTransaction函数改为non_virtual,然后将derived class的信息传递给Transaction的构造函数。
请记住:
在构造和析构期间不要调用virtual函数,因为这类调用不会下降到derived class(相对于当前执行构造和析构函数的那一层)
条款10:令operator=返回一个reference to *this
这个条款主要是为了能够使用赋值的连锁形式:x=y=z=10,这个协议同样适用+=、-=、*=等等
请记住:
令复制操作符(assignment)返回一个reference to *this