条款4-10

条款04:确定对象使用前已被初始化

        对象成员变量的初始化发生在进入函数构造本体之前,在函数体内成员变量不是被初始化而是被赋值

class A{
public:
A(const string &);
private:
string s;
};
A::A(const string &para)
{
s=para;                //这些是赋值而不是初始化
}

        此版本首先调用default构造函数为s设初值,然后立刻赋予它们新值。default构造函数的一切因此浪费了,从而有第二个版本:

A::A(const string &para)
: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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值