Effective C++(2)---- 构造/析构/赋值运算

条款05:了解C++默默编写并调用了哪些函数

若写下

class Empty { };

当C++处理过它之后,编译器就会为它声明一个default构造函数,一个copy构造函数,一个copy assignment操作符和一个析构函数。这就好像写下这样的代码:

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

惟有这些函数被需要(被调用),它们才会被编译器创建出来。

Empty e1;                     //default构造函数
Empty e2(e1);                 //copy构造函数
e2 = e1;                      //copy assignment操作符

default构造函数和析构函数主要是给编译器一个地方来放置“藏身幕后”的代码,像是调用base classesnon-static成员变量的构造函数和析构函数。注意,编译器产出的析构函数是个non-virtualcopy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源于对象的每一个non-static成员变量拷贝到目标对象。

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

NameObject<int> no1("Smallest Prime Number", 2);
NameObject<int> no2(no1);

编译器生成的copy构造函数必须以no1.nameValue和no1.objectValue为初值设定no2.nameValue和no2.objectValue。nameValue的类型是string,调用string的copy构造函数。objectValue的类型是int,是个内置类型,所以会拷贝每一个bits来完成初始化。

若NameObject的定义如下,其中的nameValue是个reference to string,objectValue是个const T:

template<class T>
class NameObject {
public:
    NameObject(std::string& name, const T& value);
private:
    std::string &nameValue;             //是个reference
    const T objectVaule;                //是个const
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NameObject<int> p(newDog, 2);
NameObject<int> s(oldDog, 36);
p = s;                                // p的成员变量该发生什么事?

编译器拒绝编译赋值动作。

如果某个base classes将copy assignment操作符声明成private,编译器将拒绝为derived classes生成一个copy assignment操作符。

请记住:

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

用一个class描述待售房屋

class HomeForSale { ... };

每个售卖的房屋都是独一无二的,所以HomeForSale对象不能被复制。

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);          //企图拷贝h1,不应通过编译
h1 = h2;                     //企图拷贝h2,不应通过编译

如果不声明copy构造函数或copy assignment操作符,编译器可能会为你产出一份,支持copying;

如果声明了copy构造函数或copy assignment操作符,那就还是支持copying

所有编译器产出的函数都是public,我们可以把copy构造函数和copy assignment操作符声明成private。但是成员函数和友元函数还是可以调用private函数,解决办法就是“将成员函数声明为private而且故意不实现它们”。

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

请记住:

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。

条款07:为多态基类声明virtual析构函数

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

class AtomicClock : public TimeKeeper { ... };
class WaterClock : public TimeKeeper { ... };
class WristWatch : public TimeKeeper { ... };

// 使用工厂模式函数,返回一个指向derived class对象的base class指针
TimerKeeper* ptk = getTimeKeeper();
...
delete ptk;

C++明确指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁。解决问题办法很简单,给base class一个virtual的析构函数。

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

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

任何class只要带有virtual函数都几乎确定应该有个virtual析构函数。

如果class不含有virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。

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

欲实现出virtual函数,对象必须携带某些信息,主要用来控制运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(vitrual table pointer)指针指出。vptr指向一个由函数指针构成的数组,成为vtbl(vitrual table);每一个带有virtual函数的class都有一个对应的vtbl。当对象调用某一virtual函数,实际调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。

若Point class内含virtual函数,其对象的体积会增加。

std::string,STL中容器如vector,list,set等等都不带有virtual析构函数,不要继承一个标准容器或其他“带有non-virtual析构函数”的class。

纯虚函数(pure virtual),拥有纯虚函数的类是抽象类。

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

析构函数的运作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。 编译器会在AWON的derived classes的析构函数中创建一个对~AWON的调用动作,所以必须为这个函数提供一份定义,否则链接器会发出抱怨。

请记住:

带有多态性质的base classes应该声明一个virtual的析构函数。如果class带有任何的virtual函数,那么它就应该拥有一个virtual析构函数。

classes的设计目的如果不是作为base classe使用,或不是为了具备多态性,就不应该声明virtual析构函数。

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

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的构造函数被调用,但首先Transaction构造函数一定会更早被调用;Transaction构造函数的最后一行调用了virtual函数logTransaction,调用的是Transaction内的版本,而不是BuyTransaction内的版本。base class的构造期间virtual函数是不会下降到derived class阶层。同样道理也适用于析构函数。

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

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

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

    }
private:
    static std::string createLogString(parameters);
};

可以由derived classes将必要的信息向上传递给至base class的构造函数。BuyTransaction内的private static函数作用,此函数是static函数,就不可能意外指向“初期未成熟之BuyTransaction对象内尚未初始化的成员变量”。 

 请记住:

在构造和析构期间不要调用virtual函数,因为这类调用从不下降到derived class。

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

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

int x,y,z;
x = y = z = 15;

赋值采用的是右结合,所以上述赋值被解析为:

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

15先赋值给z,然后其结果(更新后的z)再赋值给y,然后其结果(更新后的y)再赋值给x。

为了实现连续赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。

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

    Widget& operator+=(const Widget& rhs) {
        ...
        return *this;
    }
};

请记住:

令赋值操作符返回一个reference to *this。 

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

class Widget { ... }
Widget w;
w = w;

 看起来有点愚蠢,但它合法。

a[i] = a[j];   //当i和j有相同值时
*px = *py;     //当px和py指向相同的东西
class Bitmap { ... };

class Widget {
private:
    Bitmap* pb;
};

Widget& Widget::operator=(const Widget& rhs) {
    delete pb;                            //删除原来的pb
    pb = new Bitmap(*rhs.pb);             //令pb指向*pb的一个副本
    return *this;
}

 增加“证同测试”

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

当new Bitmap抛出异常时,pb保持原状。即使没有证同测试,这段代码还是能够处理自我赋值,虽然不是效率最高,但是它行的通。

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrg = pb;                    //记住原来的pb
    pb = new Bitmap(*rhs.pb);             //令pb指向*pb的一个副本
    delete pOrg;                          //删除原来的pb
    return *this;
}

 一个常见而够好的operator=撰写方法如下:

class Widgt {
...
void swap(Widget& rhs);     //交互*this和rhs的数据
...
};

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

另外一个变奏,是用以by value方式接受实参

Widget& Widget::operator=(Widget rhs) {   //rhs是被传递对象的一个副本
    swap(rhs);                            //将*this的数据和副本的数据交换
    return *this;
}

请记住:

确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

确认任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12:赋值对象时勿忘其每一个成分

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;
}

 当新增一个Date成员后:

class Date { ... };

class Customer {
public:
...
private:
    std::string name;
    Date lastTransaction;
};

这时既有的copying函数执行的是局部拷贝:它们的确复制了name,但是没有复制新增的lastTransaction。编译器不会提醒你。如果为class增加了一个成员变量,必须同时修改copying函数。

一旦发生继承,可能会造成一个潜藏危机。

class PriorityCustomer : public Customer {
public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority) {
    logCall("PriorityCustomer copy constructor");
}

PriorityCustomer& operator=::PriorityCustomer(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer copy constructor");
    priority = rhs.priority;
    return *this;
}

 PriorityCustomer的copying函数看起来好像是复制了PriorityCustomer内的每样东西,再看一眼,它们复制了PriorityCustomer声明的成员变量,对继承自Customer的成员变量却未被复制。

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : 
    Customer(rhs), priority(rhs.priority) {
    logCall("PriorityCustomer copy constructor");
}

PriorityCustomer& operator=::PriorityCustomer(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer copy constructor");
    Customer::operator=(rhs);
    priority = rhs.priority;
    return *this;
}

 请记住:

Copying函数应用有确保复制“对象内的所有成员变量”及“所有base class成分”;

不要尝试以某个copying函数调用另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数公用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值