Effective C++

[条款08:别让异常逃离析构函数]

问题:如果一个类的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?举个例子,假设使用过一个class负责数据库连接:

class DBConnection
{
public:
    static DBConnection create();   //此函数返回DBConnection对象
    void close();           //关闭联机;失败则抛出异常
};

一个较佳策略是创建一个用来管理DBConnection资源的DBConn类,DBConn类自己提供一个close函数,因而赋予客户一个机会得益处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已被关闭,若没有被关闭,则由DBConn的析构函数关闭它。这可防止遗失数据连接。然而如果DBConnection析构函数调用close失败,则可使用“强制结束程序”或“吞下异常”的方法:

class DBConn
{
public:
    DBConn();
    ~DBConn();
 
    void close();
private:
    DBConnection db;
    bool closed;
};
 
DBConn::DBConn()
{
}
 
DBConn::~DBConn()
{
    if(!closed)
    {
        try
        {
            db.close();          //关闭连接
        }
        catch(...)                //如果关闭动作失败
        {
            写日志,记下对close的调用失败;  //记录下来并结束程序
            ...                //或者吞下异常;
        }
    }
}
 
void DBConn::close()        //供客户使用的新函数
{
    db.close();
    closed = true;
}

如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数抛出异常及时危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
请牢记:
1、析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获异常,然后吞下它们或结束程序。
2、如果客户需要对某个操作函数进行运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行操作。

[条款09:绝不在构造和析构过程中调用virtual函数]

不该在构造函数和析构函数期间调用virtual函数,这一点是C++与jave/C#不同的地方之一。

假设有一个class继承体系,用来模拟股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。

正确的做法是在基类Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息给基类Transaction的构造函数,这样那个构造函数便可安全地调用non-virtual logTransaction。正确用法如下:

//绝不在构造和析构过程中调用virtual函数

//如有以下继承体系,希望每创建一个交易对象,都会有一笔日志记录
class Transaction
{
public:
    Transaction() 
    {
        logTransaction();
    };

    virtual void logTransaction() const = 0;
};

//继承类,需要实现logTransaction()
class BuyTransaction : public Transaction
{
public:
    virtual void logTransaction() const;
};

//继承类,需要实现logTransaction()
class SellTransaction : public Transaction
{
public:
    virtual void logTransaction() const;
};

//解决方案:logTransaction改为非虚,然后继承类构造函数传递必要的信息给基类构造函数
class Transaction
{
public:
    explicit Transaction(const std::string& logInfo)
    {
        logTransaction();
    }

    void logTransaction(const std::string& logInfo) const; //非虚
};

class BuyTransaction : public Transaction
{
public:
    BuyTransaction(parameters) 
        : Transaction(createLogString(parameters))
    {}

private:
    //静态的放置“初期未成熟的buytransaction对象内尚未初始化的成员变量”
    static std::string createLogString(parameters); 
};

注意示例BuyTransaction内的private static函数 createlogString的运用。比起在成员初值列内给予基类所需的数据,利用辅助函数创建一个值传递给基类的构造函数往往比较方便(也比较可读)。令此函数为static,也就不可能意外指向“初期未成熟的BuyTransaction对象内尚未初始化的成员变量”。这很重要,正是因为“那些成员变量处于未定义状态”,所以在基类构造和析构期间调用的virtual函数不可下降至派生类。
请牢记:
 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。

[条款10:令operator= 返回一个reference to *this]

关于赋值,可以写成连锁形式:

int x, y, z;
x = y = z = 15;    //赋值连锁形式

赋值采用右结合律,故上述赋值被解析为:

x = (y = (z = 15)); 

为了实现连锁赋值,赋值操作符必须返回一个reference引用指向操作符的左侧实参。
下面示例是为classes实现赋值操作符时应该遵循的协议:

class Widget
{
public:
    ...
    Widget& operator=(const Widget& rhs)    //返回类型是个reference,指向当前对象
    {
        ...
        return *this;   //返回左侧对象
    }
    ...
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:

class Widget
{
public:
    ...
    //这个协议适用于+=,-=,*=,等等
    Widget& operator+=(const Widget& rhs)   //返回类型是个reference,指向当前对象
    {
        ...
        return *this;   
    }
    Widget& operator=(int rhs)  //此函数也适用,即使此操作符的参数类型不符合协定
    {
        ...
        return *this;   
    }
    ...
};

这份协议被所有内置类型和标准程序库提供的类型如string,vector,complex,trl::shared_ptr或即将提供的类型共同遵守。
请牢记:
  令赋值(assignment)操作符返回一个reference to *this

条款11:在operator= 中处理“自我赋值”

“自我赋值”发生在对象被赋值给自己时:

class Widget
{
    ...
};
 
Widget w;
...
w = w;  //赋值给自己

operator=,不仅不具备“自我赋值安全性”,也不具备“异常安全性”。
让operator= 具备“异常安全性”往往自动获得“自我赋值安全性”的回报。因此越来越多的人对“自我赋值”的处理态度是不去管它,而把焦点放在实现“异常安全性”上。
确保代码不但“异常安全”而且“自我赋值安全”的一个替代方案是,使用所谓的copy and swap技术。此技术和“异常安全性”有密切关系,它是一个常见而够好的operator=撰写办法,其实现方式为:

class Widget
{
public:<br>    ...
    void swap(Widget& rhs);     //交换*this和任rhs的数据
    ...
};
 
 Widget& Widget::operator=(const Widget& rhs)
 {
     Widget temp(rhs);  //为rhs数据制作一份复件(副本)
     swap(temp);        //将*this数据和上述复件的数据交换
     return *this;
 }

另外一种实现方式为:

Widget& Widget::operator=(Widget rhs)  //rhs是被传对象的一份复件(副本),注意此处是值传递 pass by value
{
    swap(rhs);     //将*this数据和复件的数据交换
    return *this;
}

上述实现方式因为:1、某类的copy assignment操作符可能被声明为“以by value方式接受实参”;2、以by value方式传递东西会造成一份复件/副本
此方式牺牲了清晰性,然而将拷贝动作从函数本体移至“函数参数构造阶段”却可令编译器有时生产更高效的代码

//在operator=中处理自我赋值

class Bitmap {};

//保存一个指针指向一块动态分配的bitmap
class Widget
{
public:
    //问题的发生:
    Widget& operator=(const Widget& rhs)
    {
        delete pb;
        //如果rhs与this指向同一块内存则错误
        pb = new Bitmap(*rhs.pb);
        return *this;
    }

    //解决方法1:证同测试,
    Widget& operator= (const Widget& rhs)
    {
        if (this == &rhs)
            return *this;
        delete pb;
        //这里可能出现异常问题,如果new发生异常(不论内存不足还是copy构造函数异常)
        //widget会持有一个指针指向一块被删除的bitmap.
        pb = new Bitmap(*rhs.pb);
        return *this;
    }

    //解决方法2:精心安排语句顺序来保证“异常安全”,防止解法1的问题
    Widget& operator=(const Widget& rhs)
    {
        //记住原先指针,再构造一个副本,然后再删除,即删除在构造之后
        Bitmap* porg = pb;
        pb = new Bitmap(*rhs.pb);
        delete porg;
        return *this;
    }

    
    //解法3:2的一个替代方案,即copy and swap技术
    void swap(Widget& rhs)
    {
        //交换数据
    }

    Widget& operator=(const Widget& rhs)
    {
        Widget temp(rhs);
        swap(temp);   //交换*this与temp的数据
        return *this;
    }

    //3的另一个变型解法,依赖以下事实(1) 某类的拷贝赋值操作可能被声明为"by value"方式接受实参
    //(2)以by value方式传递东西会造成另一份副本
    Widget* operator=(Widget rhs)  //这里利用by value构造一个副本
    {
        swap(rhs);   //这里是将*this与副本数据互换,
        return *this;
    }
private:
    Bitmap* pb;  //指向一个从堆上分配的对象
};



请牢记:
  1、确保当前对象自我赋值时operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺心、以及copy-and-swap。
  2、确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

[条款12:复制对象时勿忘其每一个部分]

设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,即copy构造函数与copy assignment操作符。编译器会在必要的时候为类创建coping函数,并说明这些“编译器生成版”的行为:将被拷贝对象的所有成员变量都做一份拷贝。

任何时候,只要自己实现派生类的copying函数,则必须很小心的复制其基类成分。这些成分往往是private私有的,故无法直接访问它们,因此应该让派送类的coping函数调用相应的基类函数:

void logCall(const string& funcName);<br> class Customer
 {
 public:   
     Customer(const Customer& rhs);
     Customer& operator=(const Customer& rhs);
 private:
     string name;
     Date lastTranscation;
 };
class PriorityCustomer : public Customer
{
public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
     ...
private:
    int priority;
};
 
PriorityCustomer ::PriorityCustomer (const PriorityCustomer& rhs) 
    : Customer(rhs),   //调用基类的copy构造函数
    priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
 
PriorityCustomer& PriorityCustomer ::operator = (const PriorityCustomer& rhs) 
{
    logCall("PriorityCustomer copy assignment constructor");
    Customer::operator=(rhs);  //对基类Customer成分进行复制动作
    priority = rhs.priority;
    return *this;
}

当编写一个copying函数,确保1、复制所有local成员变量,2、调用所有基类的适当的copying函数。
注意两个错误用法:
1、令copy assignment操作符调用copy构造函数是错误的,因为在这就像试图构造一个已存在的对象。
2、令copy构造函数调用copy assignment操作符同样是错误的。构造函数用来出事后对象,而assignment操作符只实行与已初始化的对象身上。对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“z只对已初始化对象才有意义”的事意义。
消除copy构造函数与copy assignment操作符重复代码的做法是:建立一个新的成员函数给两者调用。这样的函数往往是private而且被命名为init。这个策略可以安全消除copy构造函数与copy assignment操作符之间的代码重复。
请牢记:
  1、copying 函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。
  2、不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

[条款13:以对象管理资源]

所谓资源就是,一旦使用了它,将来必须还给系统。C++最常使用的资源就是动态分配内存(如果分配了内存却不释放,会导致内存泄露),但内存只是必须要管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当不再使用它时,必须将它还给系统。

假设我们使用一个用来模拟投资行为(例如股票、债券等)的程序库,其中各式各样的投资类型继承自一个root class Investment:

//以对象管理资源

//基类
class Investment
{...}

//程序库通过一个工厂函数产生某个特定的inverstment对象,返回指针
Investment* createInvestment(); 

//这导致调用者需要对这个返回的指针进行删除
void f()
{
    Investment* pInv = createInvestment();
    //这里中间可能过早返回,或抛出异常,则都不会执行delete,也就会发生资源泄漏
    ...
    delete pInv;
}

//解决方法1:使用auto_ptr管理对象资源
void f()
{
    //这里是RAII,获得资源后立刻放进管理对象
    std::auto_ptr<Investment> pInv(createInvestment);

    //auto_ptr的复制行为
    std::auto_ptr<Investment> pInv2(pInv);  //pInv2指向对象,pInv=null
    pInv = pInv2;       //pInv指向对象,pInv2=null

}//离开函数的时候,调用auto_ptr的析构函数确保资源被释放

//解决方法2:使用tr1::shared_ptr管理对象资源
void f()
{
    //这里是RAII,获得资源后立刻放进管理对象
    std::tr1::shared_ptr<Investment> pInv(createInvestment);

    //shared_ptr的复制行为
    std::tr1::shared_ptr<Investment> pInv2(pInv);  //pInv2, pInv指向同一对象
    pInv = pInv2;       //pInv,pInv2指向同一对象

}//离开函数的时候,调用shared_ptr的析构函数确保pInv,pInv2被释放

//以下行为虽然可以通过编译器,但是会有资源泄漏,不应该使用
std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<std::string> api(new int[1024]);

请牢记:
1、为防止内存泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用RAII class分别是trl1::shared_ptr和auto_ptr。trl1::shared_ptr通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使被复制物指向null。trl1::shared_ptr在头文件<memory>中

[条款14:在资源管理类中小心copying行为]

请牢记:

1、复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

2、普遍常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也可能被实现。

//资源管理类的copying行为

class Lock
{
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm)
    {
        lock(mutexPtr); //构造函数锁住资源
    }

    ~Lock()
    {
        unlock(mutexPtr);  //析构函数释放资源
    }
};

//客户端的用法
Mutex m;
Lock ml(&m);  //锁定,执行关键区域内的操作
...

//如果进行拷贝,可以发生以下情况:
Lock ml1(&m);
Lock ml2(ml1);

//情况1:禁止复制,参考条款6的做法,
//1声明为私有,不定义,如果复制发生链接错误
//2建一个不能拷贝的基类,并私有继承于它,这将错误移到编译期
class Lock : private Uncopyable
{
};

//情况2:引用计数法,例如shared_ptr的行为,但当资源变为0时,它的默认行为是
//删除所指物,而如果我们只是释放锁定,这种情况下可以利用shared_ptr中可以指定“删除器”
//的一个函数或函数对象,当引用计数为0时,调用此函数,这个参数对它的构造函数是可有可无的第二个参数
class Lock
{
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) //unlock即为指定的删除器
    {
        lock(mutexPtr.get());
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr; //用shared_ptr管理这个对象,并且指定删除器,自定义行为
};
//而且以上不再需要析够函数,因为析构函数会在引用计数为0时自动调用shared_ptr的删除器

//情况3:复制底部资源,进行深度拷贝,例如string类,

//情况4:转移底部所有权,如auto_ptr的行为,资源所有权从被复制物转移到目标物

[条款15:在资源管理类张提供对原始资源的访问]

请牢记:

1、APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。

2、对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

//资源管理类中提供对原始资源的访问

class Font
{
public:
    explicit Font(FontHandle fh) : f(fh)
    {}

    //可以提供两种访问原始资源的方式:
    //1 显示转换函数,优点是安全,缺点是每次调用都需要访问该函数
    FontHandle get() const
    {
        return f;
    }

    //2 隐式转换,重载转换操作符,优点不需显示调用,会隐式执行,缺点:容易出错
    operator FontHandle() const
    {
        return f;
    }

    ~Font()
    {
        releaseFont(f);
    }
private:
    FontHandle f;  //管理的原始字体资源
};

Font f1(getFont());
FontHandle f2 = f1;  //本来是想拷贝Font对象,却隐式转换f1为FontHandle,然后进行了复制

[条款16:成对使用new和delete时要使用相同的形式]

请牢记:

如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。 new[] 对应 delete[]

如歌在new表达式中不适用[],一定不要在相应的delete表达式中使用[]。 new 对应 delete

当使用new时(即通过new动态生成一个对象),有两件事发生:第一,内存被分配出来(通过名为operator new 的函数);第二,针对此内存会有一个(或更多)构造函数被调用。

当使用delete时,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete 的函数)。delete的最大问题在于:即将被删除的内存之内有究竟存有多少对象?(即将被删除的那个指针,所指的是单一对象或对象数组?)此问题的答案决定了又多少个析构函数必须被调用起来。

单一对象的内存布局不同于对象数组的内存布局:数组所用的内存包括“数组大小”记录,以便delete制定需要调用多少次析构函数。单一对象的内存则没有这笔记录。

delete[]认定指针指向一个数组,多次调用析构函数。因此切记 new和delete时要采取相同形式。

typedef std::string AddressLines[4];  //定义了一个AddressLines类型,执行string [4]

std::string* pal = new AddressLines; //这里分配的是数组,相当于new string[4]

delete pal;  //错误,但是可能会发生
delete [] pal; //正确的形式,但与以上new不对称

[条款17:以独立语句将newed对象置入智能指针]

请牢记:

以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被跑出来,有可能导致难以察觉的资源泄露。

请牢记:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被跑出来,有可能导致难以察觉的资源泄露。

假设有个函数用来处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

int priority();    //处理程序优先权的函数<br>
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);//该函数在动态分配所得的Widget上进行某些带有优先权的处理。
调用:
processWidget(new Widget, priority());     //编译不过!该构造函数是explicit 无法隐式转换为shared_ptr

因此可以写成:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());  //可以编译通过,但是...可能泄露资源。

后果:一旦发生异常,可能资源泄露
原因:
在调用processWidget之前,编译器必须创建代码,执行三步:
(1)调用prority()
(2)执行"new Widget"
(3)调用 tr1"shared_ptr构造函数
但是c++调用顺序跟java和c#不同,不是以特定顺序完成。priority函数的调用有可能在第一、第二或者第三执行。当在第二位执行的情况下:
(1)执行"new Widget"
(2)调用prority()
(3)调用 tr1"shared_ptr构造函数
若调用prority时发生异常,则"new Widget"返回的指针将会遗失,这样会引发资源泄露。
解决方案:使用分离语句,分别写出(1)创建Widget,(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:

std::tr1::shared_ptr<Widget> pw(new Widget);    //在单独语句内以智能指针存储newed所得对象<br>
processWidget(pw, priority());    // 这个动作不会造成泄露
//以独立语句将newed对象置入智能指针
//有以下函数
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

//以下调用形式,不会通过编译,因为tr1::shared_ptr的构造函数是explicit函数
//虽然它接受一个widget的指针,但是不能进行隐式转换,
processWidget(new Widget, priority());

//所以如果使用以下强制转化,可以通过,但这可能有隐式的资源泄漏,在new与调用shared_ptr构造函数之间
//执行priority,如果它发生异常,则资源泄漏
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

//解决方案:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

因为编译器对于”跨越语句的各项操作“没有重新排列的自由(只有在语句内编译器才拥有那个自由度)。

[33:避免掩盖继承而来的名称]

//避免掩盖继承而来的名称

//如以下类
class Base
{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int);

    virtual void mf2();
    
    void mf3();
    void mf3(double);
};

class Derived : public Base
{
public:
    virtual void mf1();   //这里会将基类的mf1,mf1(int)都隐藏掉,遮盖了名称
    void mf3(); //这里也如mf1一样,将基类的两个mf3遮盖
    void mf4();
};

Derived d;
int x;

d.mf1();    //Derived::mf1
d.mf1(x);   //错误,因为继承类掩盖了基类的mf1(int)
d.mf2();    //Derived::mf2
d.mf3();    //Derived::mf3
d.mf3(x);   //错误,掩盖了基类的mf3(int)

//为了避免名称被掩盖,1 public继承下可以使用using声明式
class Base
{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int);

    virtual void mf2();
    
    void mf3();
    void mf3(double);
};

class Derived : public Base
{
public:
    using Base::mf1;  //让base类名为mf1和mf3的所有东西(函数了变量了)
    using Base::mf3;  //在Derived作用域内都可见(并且是public)
    virtual void mf1(); 
    void mf3();
    void mf4();
};
d.mf1();    //Derived::mf1
d.mf1(x);   //没问题,Base::mf1(int)
d.mf2();    //Derived::mf2
d.mf3();    //Derived::mf3
d.mf3(x);   //没问题,Base::mf3(int)

//为了避免名称被掩盖,2 使用转交函数继承一个函数
class Base
{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int);
    ...
};

class Derived : private Base
{
public:
    virtual void mf1()
    {
        Base::mf1();   //转交函数,暗自成为inline
    }
};
d.mf1();    //Derived::mf1
d.mf1(x);   //错误,名称被掩盖

[34:非纯虚函数的缺省实现提供]

//非纯虚函数的缺省实现提供

class Airport {...};
class Airplane
{
public:
    virtual void fly(const Airport& destination);
    ...
};

void Airplane::fly(const Airport& destination)
{
    缺省代码,将飞机飞向指定的目的地
}

class ModelA : public Airplane {...};
class ModelB : public Airplane {...};
//以上继承体系,A,B中fly的相同行为可以写入基类的fly,如果两者飞行方式相同,则不需重新定义fly,可以直接继承基类的实现。
//如果有区别可以调用基类的,不同的行为可以再在这个函数中写出来
//如果新增一个飞机C,且飞行方式不一样,为了防止忘记重新定义fly函数,即
//基类实现“提供缺省实现给derived classes”,但除非它们明确提出才能调用缺省实现,一种方法是切断“virtual”函数接口和“实现接口”之间的链接,如下
class Ariplane
{
public:
    //声明为纯虚的保证子类必须被重写
    virtual void fly(const Airplane& destination=0)=0;
    ...
protected: //定义为protected是因为只有继承类需要此函数,外部不应调用,也不关心
    //将默认行为移至另外一个函数,使得子类必须显示调用该函数才能继承默认行为
    void defaultFly(const Airport& destination);    
};

void Airplane::defaultFly(const Airport& destination)
{
    提供缺省行为
}

class ModelA : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        defaultFly(destination);
    }
};


class ModelB : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        defaultFly(destination);
    }
};

//这样新增的moduleC不可能意外继承fly的实现版本了,必须自己提供,且如果不继承
//默认行为则要自己写出来
class ModelC : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        //不继承默认行为,自己实现
    }
};

//但以上方法有人认为不应该以不同的方法分别提供接口和实现继承,则可以利用
//纯虚函数必须在derived classes中重新声明,但他们也可拥有自己的实现这个事实
//给基类的纯虚fly函数提供一个缺省实现
class Ariplane
{
public:
    //声明为纯虚的保证子类必须被重写
    virtual void fly(const Airplane& destination=0)=0;
    ...
};

void Airplane::fly(const Airport& destination)
{
    提供缺省行为
}

class ModelA : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        Airplane::fly(destination);
    }
};


class ModelB : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        Airplane::fly(destination);
    }
};

//这样新增的moduleC不可能意外继承fly的实现版本了,必须自己提供,且如果不继承
//默认行为则要自己写出来
class ModelC : public Airplane
{
public:
    virtual void fly(const Airport& destination)
    {
        //不继承默认行为,自己实现
    }
};

[35:virtual函数替代方案,例子]

//virtual函数替代方案,例子

//游戏人物的基类,会有不同的继承类
class GameCharacter
{
public:
    //返回人物健康指数,不是纯虚,说明是有一个计算健康指数的缺省算法
    virtual int healthValue() const;  
};

//方案1:NVI手法
class GameCharacter
{
public:
    int healthValue() const
    {
        //可以做一些事前工作,如包括锁定互斥器,制造运转日志记录,验证classu
        //约束条件,验证函数先决条件等等
        ...
        int retVal = doHealthValue();
        ...
        //调用结束后还可以做一些事后工作,包括互斥器接触锁定,验证函数的事后
        //条件,再次验证类约束条件
        return retVal;
    }
private:
    virtual int doHealthValue() const   //继承类可以重定义它
    {
        ...//缺省算法,计算健康指数
    }
};

//2 成员函数指针实现stategy模式
class GameCharactor;  //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
    //定义函数类型
    typedef int (*HealthCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
        : HealthFunc(hcf)
    {}

    int healthValue() const
    {
        return healthFunc(*this);
    }
private:
    HealthCalcFunc heathFunc;
};
//GameCharacter可提供一个成员函数setHealthCalculator,
//用来替换当前的健康指数计算函数,则函数可以在运行时变更

//同一人物类型的不同实体可以有不同的健康计算函数
class EvilBadGuy : public GameCharacter
{
public:
    explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc)
        : GameCharacter(hcf)
    {...}
};

//不同的健康指数计算函数
int loseHealthQuickly(const GameCharacter&); 
int loseHealthSlowly(const GameCharacter&);

//相同类型的人物搭配不同的健康计算方式
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);

//方案3:tr1::function函数实现strategy模式
class GameCharactor;  //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
    //代表函数是接受一个reference指向const GameCharacter,并返回int,而且这个
    //函数对象可以持有任何与此前面式兼容的可调用物,即参数可被隐式转换为
    //const GameCharacter&,返回值可被隐式转换为int,这个也是与方案2类的唯一
    //区别,不是持有一个指针,而是持有一个tr1::function对象,相当于指向函
    //数的泛化指针
    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
        : HealthFunc(hcf)
    {}

    int healthValue() const
    {
        return healthFunc(*this);
    }
private:
    HealthCalcFunc heathFunc;
};
//健康计算函数:返回是是non-int,但是可以隐式转换为int,可调用
short calcHealth(const GameCharacter&);
//函数对象
struct HealthCalculator
{
    int operator() (const GameCharacter&) const
    {...}
};

class GameLevel
{
public:
    //成员函数,用于计算健康值,non-int返回类型
    float health(const GameCharacter&) const;
};

//不同的人物类型,构造函数相同
class EvilBadGuy : public GameCharacter
{};
class EyeCanyCharacter : public GameCharacter
{};

EvilBadGuy ebg1(calHealth);  //人物1,使用函数计算
EyeCandyCharacter ecc1(HealthCalculator()); //人物2,使用函数对象计算
//使用类的成员函数计算
GameLevel currentLevel;
EvilBadGuy ebg2(str::tr1::bind(&GameLevel::health, currentLevel, _1));

//方案4:古典的Strategy模式
class GameCharacter;
class HealthCalcGunc
{
public:
    ...
    virtual int calc(const GameCharacter& gc) const
    {}
};

class SlowHealthLoser : public HealthCalcFunc
{};
class FastHealthLoser : public HealthCalcFunc
{};

HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
    explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
        : pHealthCalc(phcf)
    {}
    int healthValue() const
    {
        return pHealthCalc->calc(*this);
    }
private:
    HealthCalcFunc* pHealthCalc;
};
//不同的人物类型,构造函数相同
class EvilBadGuy : public GameCharacter
{};
class EyeCanyCharacter : public GameCharacter
{};

[37:不要重写定义继承而来的缺省参数值]

//不要重写定义继承而来的缺省参数值

//以下有缺省参数值调用时可不指定参数
class Shape
{
public:
    enum ShapeColor {Red, Green, Blue};
    virtual void draw(ShapeColor color = Red) const = 0;
};

class Rectangle : public Shape
{
public:
    //这里赋予了不同的参数值,很糟糕,虽可以不带参数调用,却会造成不同
    virtual void draw(ShapeColor color = Green) const;
};

class Circle : public Shape
{
public:
    //客户端如果调用下面这个函数,有两种情况:1 以对象调用,需指名参数值,
    //因为对象调用是静态绑定并不会从基类继承缺省参数值 2 以指针或reference
    //调用,可以不指定参数值,因为动态绑定下这个函数会从其基类继承缺省参数值
    virtual void draw(ShapeColor color) const;
};

//以下静态类型都是Shape*
Shape* ps;                //动态:未确定,未指向任何对象
Shape* pc = new Circle;   //动态类型是Circle, 
Shape* pr = new Rectangle;//动态类型是Rectangle

//动态类型在程序执行过程中可以改变
ps = pc;
ps = pr;

pc->draw(Shape::Red)  //Circle::Draw(Shape::Red)
pr->draw(Shape::Red)  //Rectange::Draw(Shape::Red)

//因为Reactangle的draw有缺省参数值,所以这个调用正确,但是因为pr的静态值是
//Shape*,而缺省参数是静态调用的,所以这个的默认参数不是Rectangle中的Green,
//而是Shape中draw的Red,结果造成这个函数基类与继承类各出一半力,奇怪的行为
pr->draw();

//NVI手法实现缺省参数值,防止虚函数这种表现出异常的行为
class Shape
{
public:
    enum ShapeColor {Red, Green, Blue};
    //非虚调用一个虚函数完成人物,提供缺省参数
    void draw(ShapeColor color = Red) const = 0
    {
        doDraw(color);
    }
private:
    //继承类需提供实现
    virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle : public Shape
{
public:
    //这里不指定缺省参数值
    virtual void doDraw(ShapeColor color) const;
};

[39:明智而审慎的使用private继承]

//明智而审慎的使用private继承

class Timer
{
public:
    explicit Timer(int tickFrequency);
    //定时器每滴答一次,函数就自动被调用
    virtual void onTick() const;       
};

//Widget不是Timer,Timer只是帮助实现它,所以私有继承,函数onTick在Widget变成
//私有的,且把它放在私有域中,如果public则有可能调用阿
class Widget : private Timer
{
private:
    virtual void onTick() const; //查看Widget的数据等等, 
};

//使用复合实现以上方法
class Widget
{
private:
    //使用一个私有的嵌套类,而这个类共有继承Timer,并实现onTick方法
    class WidgetTimer : public Timer
    {
    public:
        virtual void onTick() const;
    };
    WidgetTimer timer;   //在包含一个嵌套类对象,则可以调用了呀
};

//空类例子
class Empty {};   //C++插入一个char成为一个字节的大小

//以下这个类不再是一个int大小,还包括empty的1字节,如果还有对齐,那就会有
//3个padding了,多一个int了,sizeof(HoldAnInt) > sizeof(int)
class HoldsAnInt
{
private:
    int x;
    Empty e;
};

//如果继承一个空类,则有空白基类最优化的情况发生EBO
//以下empty不占空间,sizeof(HoldAnInt) == sizeof(int)
class HoldsAnInt : private Empty
{
private:
    int x;
};

[42:嵌套从属类型]

//typename的双重意义

//嵌套从属类型
template <typename C>
print2nd(const C& container)
{
    //以下是一个嵌套从属名称,因为C是模板参数,并且const_iterator被嵌套于
    //这个C中,所以下面需要使用typename,表明它是一个类型
    typename C::const_iterator* x;
}

//typename只被用来验明嵌套从属类型名称,其他名称不该有它存在
template <typename C>   //允许使用typename与class,此中情况下等同
void f(const C& container,  //正常类型,不允许使用typename
        typename C::iterator iter);  //嵌套类型,需要使用typename

//特殊情况,typename不可以出现在基类列表内的嵌套从属类型名称之前,也不可在
//成员初始化列表中作为base class修饰符
template <typename T>
class Derived : public Base<T>::Nested //基类列表中不允许
{
public:
    explicit Derived(int x)
        : Base<T>::Nested(x)   //成员初始化列表中不允许
    {
        typename Base<T>::Nested temp;  //嵌套从属类型加typename
    }
};

//STL中typename
template <typename IterT>
void workWithIterator(IterT iter)
{
    //为嵌套从属类型使用一个类型声明符,value_type就代表这个类型
    typedef typename std::iterator_traits<IterT>::value_type value_type;
    value_type temp(*iter);
}

[49:了解new-handler的行为]

//了解new-handler的行为

//set_new_handler函数
namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();  //异常声明,表示该函数不抛出任何异常
}

//set_new_handler用法
//以下是operator new无法分配内存时,该被调用的函数
void outOfMem()
{
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}

int main()
{
    std::set_new_handler(outOfMem); //安装内存处理函数
    int *pBigDataArray = new int [100000000L];  //当无法分配时,调用outOfMem;
    ...
}


//提供类专属之new-handler,
class Widget
{
public:
    //客户端需要提供一个new-handler函数专门是该类使用的,
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc); //只抛出bad_alloc异常
private:
    static std::new_handler currentHandler;  //指向当前类使用的new-handler,保存它是为了恢复
};

std::new_handler Widget::currentHandler = 0; //初始化0

//客户端需要提供一个new-handler函数专门是该类使用的,该函数获得这个指针并存储起来,然后返回先前存储的指针。
//这也正式标准的set_new_handler的行为
static std::new_handler Widget::set_new_handler(std::new_handler p) throw();
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

//来管理Widget的new-handler,保证可以正常恢复原来的handler
class NewHandlerHolder
{
public:
    explicit NewHandlerHolder(std::new_handler nh) //取得目前的new-handler
        : handler(nh)
    {}

    ~NewHandlerHolder()
    {
        std::set_new_handler(handler);  //释放它,其实就是还原了handler
    }
private:
    std::new_handler handler;  //记录下来

    NewHandlerHolder(const NewHandlerHolder&);  //阻止拷贝
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

//实现了以上类,则就可以实现Widget's operator new,实现很简单
void *Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
    //安装Widget的new-handler,并且保存原来的handler,如果第一次调用,原来的是null
    NewHandlerHolder h(std::set_new_handler(currentHandler));

    return ::operator new(size);  //分配内存或抛出异常,回复global new-handler(调用NewHandlerHolder的析构函数恢复)       
}


//客户端可能的调用:
void outOfMem();  //为类声明的handler
Widget::set_new_handler(outOfMem); //设置outOfMem为Widget内存分配失败时调用的函数

Widget *pw1 = new Widget;  //如果内存分配失败,调用outOfMem

std::string* ps = new std::string;  //如果内存分配失败,调用global new-handler,如果有的话

Widget::set_new_handler(0);  //设置Widget专属的为NULL
Wdiget* pw2 = new Widget;    //如果内存分配失败,立刻抛出异常

//适用于每个类的方案
template <typename T>
class NewHandlerSupport
{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
    static std::new_handler currentHandler;
};

template <typename T>
std::new_handler
NewHandlerSuppport<T>::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    
    return ::operator new(size);
}

//以下将每一个currentHandler初始化为NULL
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

//只要Widget继承自NewHandlerSupprt<Widget>就可以实现为该类提供一个专属的handler的功能了
class Widget : public NewHandlerSupport<Widget>
{
    ...
};

//nothrow new
class Widget {...};
Widget* pw1 = new Widget;  //正常的,分配失败返回bad_alloc
if (pw1 == 0) ... //测试失败

Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,返回0
if (pw2 == 0) ... //测试可能成功. 


[50:定制型operator new]

//定制型operator new

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    size_t realSize = size + 2 * sizeof(int);  //增加大小,使其可以塞入两个signatures

    void* pMem = malloc(realSize);
    if (!pMem)
        throw bad_alloc();

    //将signature写入内存的最前段落与最后段落
    *(static_cast<int*>(pMem)) = signature;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;

    //返回指针,指向位于第一个signature之后的内存位置
    return static_cast<Byte*>(pMem) + sizeof(int);
}

[51:编写new和delete时需固守常规]

//编写new和delete时需固守常规

void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    if (size == 0)  //如果是0 byte, 则当成1byte
        size = 1;

    //循环尝试分配size bytes;
    while (true)
    {
        尝试分配size bytes;
        if (分配成功)
            return (一个指针,指向分配得来的内存);

        //分配失败,找出目前的new-handling函数,因为没有直接获得该值的函数,
        //所以使现在的handler=null,利用其返回值返回以前的handler;
        new_handler globalHandler = set_new_handler(0); 
        set_new_handler(globalHandler);  //安装

        //如果非空,则调用,失败则抛出异常
        if (globalHandler)
            (*globalHandler)();
        else
            throw std::bad_alloc();
    }
}

class Base
{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    static void* operator delete(void* rawMemory, std::size_t size) throw();
    ...
};

//假设类未声明operator new,则会继承基类的operator new
class Derived : public Base
{
    ...
};

Derived* p = new Derived; //调用基类的operator new,但基类专属的operator new并非用来对付上述这种大小的对象

//解决方案,operator new类版本
void* Base::operator new(std::size_t size) throw(sts::bad_alloc)
{
    if (size != sizeof(Base))   //如果大小错误,则调用标准的operator new,这里省略了对0的判断,因为sizeof(类)不会返回0
        return ::operator new(size);
    ...
}

//定制operator delete
void operator delete(void* rawMemory) throw()
{
    if (rawMemory == 0) //如果被删除的是个null指针,则什么都不做
        return;
    以下归还rawMemory内存
}

//operator delete类版本
void Base::operator delete(void* rawMemory, std::size_t size) throw()
{
    if (rawMemory == 0)  //如果为null,什么都不做
        return;
    if (size != sizeof(Base))  //如果大小错误,调用标准版
    {
        ::operator delete(rawMemory);
        return;
    }

    现在,归还rawMemory所指内存
    return;
}

[52:一般所指的placement new]

//一般所指的placement new
void* operator new(std::size_t, void* pMemory) throw();  

//防止构造函数期间的内存泄漏
class Widget
{
public:
    ...
    //一个placement new
    static void* operator new(std::size_t size, std::ostream& logStream)
        throw(std::bad_alloc);
    //提供placement delete,防止operator new成功,构造函数失败时造成的内存泄漏
    static void* operator delete(std::size_t size, std::ostream& logStream)
        throw();
    //operator new正常运行时调用的delete
    static void* operator delete(void* pMemory, std::size_t size)
        throw();
};

//以下语句如果引发Widget构造函数抛出异常,对应的placement delete会被调用,不会发生内存泄漏
Widget* pw = new (std::cerr) Widget;

//如果没有抛出异常,客户端有如下代码,则会调用正常的delete
delete pw;

//发生函数隐藏
class Base
{
public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream)
        throw (std::bad_alloc);   //会掩盖正常的global形式
    ...
};

Base* pb = new Base;   //错误!因为正常的operator new被掩盖
Base* pb = new (std::cerr) Base;   //正确,调用Base的placement new

//继承类掩盖全局与基类的new
class Derived : public Base
{
public:
    ...
    //重新声明正常形式的new
    static void* operator new(std::size_t size) throw (std::bad_alloc);
    ...
};

Derived* pb = new (std::clog) Derived; //错误!因为被掩盖了
Derived* pb = new Derived;  //正确,调用derived的new


//std中的operator new
void* operator new(std::size_t) throw(std::bad_alloc);  //normal new
void* operator new(std::size_t, void*) throw();         //placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); //nothrow new

//解决名称掩盖
//提供一个基类内含所有的正常形式的new和delete, 
class StandardNewDeleteForms
{
public:
    //normal new/delete
    static void* operator new(std::size_t size) throw(std::bad_alloc)
    {
        return ::operator new(size); //调用正常的new
    }
    static void* operator delete(void* pMemory) throw()
    {
        return ::operator delete(pMemory); //调用正常的delete
    }

    //placement new/delete
    void* operator new(std::size_t size, void* ptr) throw()
    {
        return ::operator new(size, ptr);
    }
    void* operator delete(void* pMemory, void* ptr) throw()
    {
        return ::operator delete(pMemory, ptr);
    }

    //nothrow new/delete
    void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
    {
        return ::operator new(size, nt)
    }
    void* operator delete(void* pMemory, const std::nothrow_t& nt) throw()
    {
        return ::operator new(pMemroy);
    }
};

//如果想以自定义形式扩充标准形式,则可利用继承机制及using声明式取得标准形式
class Widget : public StandardNewDeleteForms
{
public:
    using StandardNewDeleteForms::operator new;  //使这些形式可见
    using StandardNewDeleteForms::operator delete;

    //添加自定义的placement new/delete,这样就可以在这些函数中调用标准形式的了
    static void* operator new(std::size_t size, std::ostream& logStream)
        throw(std::bad_alloc);
    static void* operator delete(std::size_t size, std::ostream& logStream)
        throw();
};

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266