读书笔记_Effective C++_构造,析构,赋值

条款五

class Empty { };

这样的一个类,当C++处理过后,编译器会为它声明一个copy构造函数、一个copy assignment操作符、一个析构函数和一个default构造函数,所有这些函数都是public且inline。

class Empty {
public:
    Empty() { ... }
    Empty(const Empty& rhs) { ... }
    ~Empty() { ... }

    Empty& operator=(const Empty& rhs) { ... }
};

default构造函数和析构函数主要是编译器用来放置调用base classes和non-static成员变量的构造函数和析构函数等,编译器产出的析构函数是个non-static,除非这个class的base class自身声明有virtual析构函数。

copy构造函数和copy assignment操作符,编译器只是将来源对象的每一个non-static成员变量拷贝到目标对象。考虑这样一个类:

template<typename T>
class NamedObject {
public:
    NamedObject(const char* name, const T& value);
    NamedObject(const std::& name, const T& value);
    ...
private:
    std::string nameValue;
    T objectValue;
}

NamedObject<int> no1("doubi", 2);
NamedObject<int> no2(no1);

编译器生成的copy构造函数会以no1.nameValue和no1.objectValue为初值设定no2.nameValue和no2.objectValue。nameValue的类型是string,而标准的string有copy构造函数,所以no2.nameValue的初始化方式是调用string的copy构造函数并以no1.nameValue为实参,另一个成员NameObject::objectValue的类型是int,是内置类型,直接copy内存中的每个bits完成初始化。

copy assignment操作符的生成方式和copy构造函数的生成方式基本一致,但如果生成的代码是不合法的,编译器会拒绝为class生成operator=。例如成员变量是reference或const,或者base class将copy assignment操作符声明为private。

条款六

通常不希望class支持某一项特定的功能,只要不声明对应的函数即可,但这个策略对copy构造函数和copy assignment操作符却不起作用,因为编译器会默认声明。那么你要怎么让这些函数失效呢?

所有的编译器产出的函数都是public,为阻止这些函数被创建出来,需要自行声明它们,可以声明为private。一般而言这种做法并不是绝对安全的,因为member函数和friend函数还是可以调用你的private函数。当然你可以不去定义它们,如果某些人不慎调用了任何一个,就会获得一个链接错误(linkage error),这一点C++的iostream程序库就是这么做的。

class HomeForSale {
public:
    ...
private:
    ...
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
};

将连接期错误转移到编译期是可能的(而且是好事,越早侦测出错误越好),设计一个专门阻止copying动作的base class类:

class Uncopyable {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale : private Uncopyable  {
};

这样当member函数和friend函数尝试copy HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment操作符,编译器会尝试调用base class,因为private所以会被拒绝。

Uncopyable class的实现和运用很微妙,不一定要以public继承它,Uncopyable的析构也不一定得是virtual。

条款七

class TimeKeeper {
public:
    TimeKeeper();
    ~TimeKeeper();
    ...
};
class AtomicClock : public TimeKeeper { ... };
class WaterClock : public TimeKeeper { ... };
class WristWatch : public TimeKeeper { ... };

TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;

getTimeKeeper返回一个derived class对象,而那个对象却经由一个base class指针被删除,而目前的base class有个non-virtual析构函数。当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没被销毁,容易造成资源泄漏、败坏之数据结构、在调试器上浪费时间

解决这个问题的方法就是给base class一个virtual析构函数。

class TimeKeeper {
public:
    TimeKeeper();
    virtual ~TimeKeeper();
    ...
};

考虑下面这个class:

class Point {
public:
    Point(int xCoord, int yCoord);
    ~Point();
private: 
    int x,y;
};

任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数,带有virtual函数通常表明该class被设计为一个virtual函数。因为virtual函数实现使得对象必须携带某些信息来决定哪一个virtual函数应该被调用。这份信息通常是由vptr指针指出,vptr指向一个由函数指针构成的数组,称为vtbl

因为virtual函数的缘故,对象的体积就会增加,32-bit计算机体积结构中将占用64bits(存放2个ints)至96bits(2个ints加上vptr);在64-bit计算机体系结构中可能占用64~128bits,因为指针在这样的计算机结构中占64bits。

class AWOV {
public:
    virtual ~AWOV() = 0;
};

AWOV::~AWOV() { }

对于一个拥有pure virtual析构函数的抽象类,必须为析构函数提供一个定义式,不然连接器会报错。

条款八

class Widget {
public:
    ...
    ~Widget() { ... };
};

void doSomething()
{
    std::vector<Widget> v;
    ...
}

当vector v被销毁,它需要销毁其内含的所有Widgets,如果析构函数中抛出异常,当两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。这个时候如果需要保证资源回收呢?

class DBConnection {
public:
    ...
    static DBConnection create();

    void close();
};

为了确保客户不忘记在DBConnection对象身上调用close(),通常的做法就是创建一个用来管理DBConnection资源的class,并在析构函数中调用close。

class DBConn {
public:
    ...
    ~DBConn()
    {
        db.close();
    }
private:
    DBConnection db;
};

这种做法如果close调用失败还是会在析构中出现问题,抛出难以驾驭的问题。

两种方法可以解决这个问题:
(1)如果close抛出异常就结束程序:

DBConn::~DBConn()
{
    try { db.close(); }
    catch (...) {
        std::abort();
    }
}

(2)吞下因调用close而发生的异常:

DBConn::~DBConn()
{
    try { db.close(); }
    catch (...) {

    }
}

但这两种方法都无法真正地解决问题。一个比较好的策略就是重新设计DBConn接口,例如DBConn自己提供一个close函数,追踪DBConnection是否已经关闭,并在答案为否的情况下由析构函数关闭,可以防止遗失数据库连接。然而如果DBConnection析构函数调用close失败,还是需要“结束程序”和“吞下异常”。

由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会,否则没机会响应。

条款九

看下面的代码:

class Transaction {
public:
    Transaction();
    virtual void logTransaction() const = 0;
    ...
};

Transaction::Transaction() {
    ...
    logTransaction;
}

class BuyTransaction : public Transaction { public:
    virtual void logTransaction() const;
    ...
};

class SellTransaction : public Transaction {
public:
    virtual void logTransaction() const;
    ...
};

BuyTransaction b;

当执行BuyTransaction b的时候,首先Transaction的构造函数会被调用,构造函数的最后一行调用了virtual的logTransaction。这个时候调用logTransaction是Transaction的版本,而不是BuyTransaction内的版本,因为在base class构造期间virtual函数不会下降到derived classes阶层

在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析到base class,如果要使用运行期类型信息(例如dynamic_cast和typeid),也会把对象视为base class类型。同样的道理也适用于析构函数,进入到base class析构函数后对象就成为一个base class对象

所以上述的例子在运行的时候就会出现连接器报错,pure virtual是没有定义的。那要如何实现这样的机制?每次Transaction继承体系上有对象被创建,就会调用正确的logTransaction函数?

(1)将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要的信息给Transaction构造函数:

class Transaction {
public:
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;
    ...
};

Transaction::Transaction(const std::string& logInfo)
{
    ...
    logTransaction(logInfo);
}

class BuyTransaction : public Transaction {
public:
    BuyTransaction (parameters) : Transaction(createLogString(parameters)) { ... }
    ...
private:
    static std::string createLogString(parameters);
};

因为无法使用virtual函数从base classes向下调用,在构造期间,可以将derived classes将必要的信息向上传递给base class构造函数。

这里有个需要注意的小技巧:BuyTransaction内的private static函数createLogString,比起在初始化列表中直接传递数据,利用辅助函数往往更加方便,最好令这个函数为static,这样就不会意外地指向BuyTransaction中未初始化的成员变量

条款十

赋值可以写出连锁的形式:

x = y = z = 15;

因为赋值是采用右结合律的,所以赋值操作符必须要返回一个reference指向操作符的左侧实参。

class Widget {
public:
    ...
    Widget& operator=(const Widget* rhs) {
        ...
        return *this;
    }
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,如+=,-=,*=等。

条款十一

自我赋值在对象或变量存在别名的情况下很有可能会出现,假设你建立一个class用来保存一个指针指向一块动态分配的位图:

class Bitmap { ... };
class Widget {
    ...
private:
    Bitmap* pb;
};

Widget& Widget::operator=(const Widget& rhs) {
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

这里的自我赋值问题是this如果和rhs是同一个对象,delete删除的就是当前对象的bitmap,想要阻止这种错误,传统的做法是借由operator=最前面的一个“认同测试”达到“自我赋值”的检验目的

Widget* Widget::operator=(const Widget& rhs) {
    if (this == &rhs) return *this;

    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

这样的做法确实可以解决自我赋值的问题,但是正如上一个条款所说,并不具备异常安全性。如果new Bitmap抛出异常,Widget持有的指针指向了一块删除的Bitmap。很多时候其实只要注意语句的顺序就可以解决异常安全的问题。

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;
    pb = new Bitmap(*rhs.pb)
    delete pOrig;
    return *this;
}

现在如果“new Bitmap”抛出异常,pb依然不变,即使没有认同测试这里copy了一份bitmap,删除了原先的bitmap,效率上是降低的。然而如果将认同测试加入到函数起始处同样需要成本,因为通常来说“自我赋值”发生的机率非常低,认同测试使得代码变大,并导入了一个新的控制流分支,降低了执行速度,prefetching、caching和pipelining等指令的效率都会因此降低。

另一种方案是使用copy and swap技术,这也是operator=常见的一种撰写手法。

class Widget {
    ...
    void swap(Widget& rhs);
    ...
};
Widget& Widget::operator=(const Widget& rhs) {
    Widget temp(rhs);
    swap(temp);
    return *this;
}

这个方法的另一种实现方式基于的事实是:
(1)某class的copy assignment操作符可能被声明为“以by value方式接受实参”;
(2)以by value方式传递东西会造成一份副本。

Widget& Widget::operator=(Widget rhs) {
    swap(rhs):
    return *this;
}

这种做法用它巧妙的修补而牺牲了清晰性,而且将copying的动作从函数的本体移至函数参数构造阶段可以令编译器生成更加高效的代码。

条款十二

一个设计良好的面向对象系统会将对象的内部封装起来,只留下两个函数负责对象拷贝,那便是copy构造函数和copy assignment操作符,编译器会在适当的时候为classes创建copying函数,并将被拷贝的对象的所有成员变量都做一份拷贝。如果声明自己的copying函数,那么当实现代码中出现错误的时候,编译器是不会告诉你的。

考虑这样一个class:

void logCall(const std::string& funcName);
class Customer {
public:
    ...
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
    ...
private:
    std::string name;
};

Customer::Customer(const Customer& rhs) : name(rhs.name){
    logCall("Customer copy constructor");
}

Customer& Customer::operator=(const Customer& rhs) {
    logCall("Customer copy assignment operator");
    name = rhs.name;
    return *this;
}

这种做法带来的问题就是,每次有新的成员变量加入就必须同时修改copying函数,而且这个时候编译器是不会报错的。而且一旦发生了继承,更是容易出现这样的情况。如果是重写继承类的copying函数就需要确保调用了基类的copying函数。

当编写一个copying函数时要确保:
(1)复制所有的local成员变量;
(2)调用所有的base classes内的适当的copying函数。

很多时候自己的构造一个类的时候要思考清除类扩展的复杂度:
(1)添加成员变量、函数;
(2)继承多态;
(3)运算符操作;
(4)多线程。

如果发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个private且通常被命名为init的函数给两者调用。

…终于结束了第二章….

你可能感兴趣的:(C++,读书笔记)