《Effective C++》学习笔记(3)

2 构造/析构/赋值运算(续)

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

class Transaction {                            //所有交易的base class
public:
   Transaction();
    virtual void logTransaction() const = 0;     //做出一份因类型不同而不同的日志记录(log entry)
    ...
};
Transaction::Transaction()                     //base class构造函数之实现
{
    ...
   logTransaction();                           //最后动作是志记这笔交易
}
class BuyTransaction: public Transaction {     //derived class
public:
   virtual void logTransaction() const;        //志记(log)此型交易
   ...
};
class SellTransaction: public Transaction {    //derived class
public:
   virtual void logTransaction() const;        //志记(log)此型交易
   ...
};

如果执行BuyTransaction b;,其执行过程为:
会有一个BuyTransaction构造函数被调用,但首先会调用Transaction构造函数(因为派生类对象内的基类成分会在派生类自身成分被构造之前先构造妥当)。Transaction构造函数的最后一行调用virtual函数logTransaction被调用的logTransaction是Transaction(基类)内的版本,不是BuyTransaction内的版本。
换句话就是:在base class构造函数执行期间,virtual函数不是virtual函数。

  • 在初始化派生类对象时,基类的构造函数的执行要早于派生类的构造函数。即,派生类对象内的基类成分会在派生类自身成分被构造前先构造妥当。

  • 基类构造期间,virtual函数绝不会下降到派生类阶层。

  • 当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?

  • 唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。后一种情况通常不会引发任何编译器和连接器的问题,但它也是错误的。

  • 解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。

// 在class Transaction内将logTransaction函数改为non-virtual,
// 然后要求派生类构造函数传递必要信息给Transaction构造函数,
// 而后那个构造函数便可安全地调用non-virtual logTransaction。
class Transaction {
 public:
   explicit Transaction(const std::string& logInfo);
   void logTransaction(const std::string& logInfo) const; //如今是个non-virtual函数
   ...
 };
 Transaction::Transaction(const std::string& logInfo)
 {
   ...
   logTransaction(logInfo);                              //如今是个non-virtual调用
 }
 class BuyTransaction: public Transaction {
 public:
   BuyTransaction( parameters ) : Transaction(createLogString( parameters ))//将log信息传给base class构造函数
   { ... }
   ...
 private:
   static std::string createLogString( parameters );
 };

** note: **
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。


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

对于赋值操作符,可以进行连锁赋值:

int x, y, z;
x = y = z = 15;    // 等价于 x=(y=(z=15)); 赋值采用右结合律

为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。

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

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

所有内置类型和标准程序库提供的类型如string,vector,complex,tr1::shared_ptr或即将提供的类型(条款54)共同遵守。

** note: **
令赋值(assignment)操作符返回一个reference to *this。


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

// 自我赋值的例子
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指向内存对象进行delete
    pb = new Bitmap(*rhs.pb);                // 如果*this == rhs,那么这里new出错
    return *this;       
}

如果operator=函数内的rhs和*this(赋值的目的端)是同一个对象,将会导致*this对象里的pb指针指向一个已被删除的对象.

  • 传统做法是藉由operator=最前面的一个“证同测试”达到“自我赋值”的检验目的:
Widget& Widget::operator=(const Widget& rhs)     
{     
    if (this == &rhs)  
        return *this;            // 证同测试,如果是自我赋值,就不做任何事

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

虽然增加证同测试后,可以达到自我赋值的安全性,但不具备异常安全性。如果new过程发生异常(不论是因为分配时内存不足或因为Bitmap的copy构造函数抛出异常),Widget最终持有一个指针指向一块被删除了的Bitmap。这样的指针有害,我们无法安全地删除它们,甚至无法安全地读取它们.

  • 通过合理安排语句,可以实现异常安全的代码。
 Widget& Widget::operator=(const Widget& rhs)
{
    Bitmap *pOrig = pb;                // 记住原先的pb     
    pb = new Bitmap(*rhs.pb);      // 令pb指向*pb的一个副本    
    delete pOrig;                           // 删除原先的pb    
    return *this;  //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。     
} 

如果"new Bitmap"抛出异常,pb会保持原状。即使没有证同测试,上述代码还是能够处理自我赋值.

  • 还有一种方法copy and swap技术(条款29)可以实现operator=函数的异常安全和自我赋值安全。
class Widget{
    ...
    void swap(Widget& rhs);    // 交换*this和rhs的数据,详见条款29
    ...
};
Widget& Widget::operator=(const Widget& rhs)
{
    Widget temp(rhs);      //为rhs数据制作一份复件
    swap(temp);            //将*this数据和上述复件的数据交换
    return *this;
}

上述代码中,创建临时变量temp,作为rhs数据的一份复件。由于swap函数会交换*this和参数对象的数据,如果直接将rhs作为参数,则会改变rhs对象的值,与operator=的操作不符。

** note: **

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

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

copying函数:copy构造函数和copy assignment操作符。

  • 编译器在必要时会为我们的classes创建copying函数,以将被拷对象的所有成员变量都做一份拷贝。但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们也应确保对“每一个”成员进行拷贝,否则拷贝不完全时编译器也不会提示你。
void logCall(const std::string& funcName); // make a log entry
class Customer {
public:
    ...
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
    ...
private:
    std::string name;
};
// copy构造函数
Customer::Customer(const Customer& rhs)
    : name(rhs.name) // copy rhs’s data
{
    logCall("Customer copy constructor");
}
// copy assignment操作符
Customer& Customer::operator=(const Customer& rhs)
{
    logCall("Customer copy assignment operator");
    name = rhs.name; // copy rhs’s data
    return *this; 
}
  • 如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。

  • 在派生类的构造函数,copy构造函数和copy assignment操作符中应当显示调用基类相对应的函数,否则编译器可能会调用基类的default构造函数(必定有一个否则无法通过编译)对基类成分进行缺省的初始化。

// 派生类
class PriorityCustomer: public Customer { // a derived class
public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
    ...
private:
    int priority;
};
// copy构造函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs), // 调用base class的copy构造函数
priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
// copy assignment操作符
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); // 对base class成分进行赋值操作
    priority = rhs.priority;
    return *this;
}
  • 当你编写一个copying函数,请确保:
    1. 复制所有local成员变量;
    2. 调用所有base classes内的适当的copying函数。
  • 我们不该令拷贝赋值操作符调用拷贝构造函数,也不该令拷贝构造函数调用拷贝赋值操作符。如果copy构造函数和copy assignment操作符有相近的代码,消除代码重复的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。

** note: **

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

推荐阅读更多精彩内容

  • 再读高效c++,颇有收获,现将高效c++中的经典分享如下,希望对你有所帮助。 1、尽量以const \enum\i...
    橙小汁阅读 1,204评论 0 1
  • 接着上节 condition_varible ,本节主要介绍future的内容,练习代码地址。本文参考http:/...
    jorion阅读 14,750评论 1 5
  • 1. 让自己习惯C++ 条款01:视C++为一个语言联邦 为了更好的理解C++,我们将C++分解为四个主要次语言:...
    Mr希灵阅读 2,782评论 0 13
  • C++ 面向对象编程 博客园地址:http://www.cnblogs.com/xiongxuanwen/p/42...
    先之阅读 683评论 0 1
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,504评论 1 51