Effective C++学习笔记(第二章)

条款05:了解C++默默编写并调用的哪些函数
  • 如果写了一个空的类,C++编译器会为这个类自当产生四个函数,并且这些函数都是inline的:
    (1)默认构造函数,不带参数;
    (2)析构函数;
    (3)拷贝构造函数;
    (4)赋值运算符;
    实际上在现代C++(C++11之后)还会产生一个移动构造函数
  • 开发者如果实现了一个类的构造函数(带参数),则编译器就不会为这个类产生默认构造函数了。
  • 编译器在子类产生默认赋值运算符的时候也会考虑到父类中成员变量的const属性,如果父类中有const成员,则编译器不会为子类产生默认的赋值运算符,如下:
class B {
private:
  const int value = 100;
};
class D : public B {
};
// 编译器不会自动为B产生赋值运算符!
条款06:若不想用编译器自动生成的函数,就该明确拒绝

如果在声明一个类后,没有声明拷贝构造函数或者赋值运算符的情况下,按照上一条规则,编译器是会为你自动生成对应的函数的。(注意:这里的自动是指你的代码里产生了这样的调用的时候
但是,实际场景下,往往存在不能拷贝构造或者赋值运算的情况(这两种操作没有实际意义),就需要将这两个函数定义成不能被使用。

  • 方法1:把拷贝构造函数或者赋值运算符定义为类的private成员
class A {
public:
private:
  A(const A& other); // 不定义它
  A& operator=(const A& other);  // 不定义它
};

外部代码肯定不能使用这两个函数了,并且A类内部成员函数使用的时候会在链接的时候报错,因为我们没有定义他们。

  • 方法2:私有继承(实现继承而非接口继承)一个uncopyable的类,这个方法比较巧妙
class UnCopy {
public:
  UnCopy() {}
  ~UnCopy() {}
private:
  UnCopy(const UnCopy&);
  UnCopy& operator=(const UnCopy&);
};
class MyClass : private UnCopy { // 这里如果用public继承也可以达到同样的效果
};

MyClass obj1, obj2;

obj1 = obj2;   // 错误,编译器自动为MyClass生成赋值运算符的时候会调用UnCopy的赋值运算符,而它在MyClass类中是不可见的。

这一条规则在现代C++(11之后),实现起来就非常简单了,使用delete关键字即可做到,如下所示:

class MyClass {
public:
  MyClass() {}
  ~MyClass() {}
  MyClass(const MyClass& other) = delete;
  MyClass& operator=(const MyClass& other) = delete;
};

MyClass obj1, obj2;
obj1= obj2; // 错误
条款07:多态基类声明virtual析构函数
  • 如果要声明一个支持多态特性的基类,其析构函数要加上virtual关键字,否则在销毁对象时会产生局部销毁的问题,如下:
class B {
public:
  B() {}
  ~B() {}
};
class D : public B {
};

B* p = new D();
delete p; // 将会产生局部销毁的问题,产生资源泄露
  • 如果基类不是作为多态场景来使用,其析构函数不要加virtual关键字,以便减少内存占用。
条款08:别让异常逃离析构函数

这条规则背后的原因是如果在类的析构函数中抛出异常,外部的代码很难捕捉到,考察以下代码:

class A {
  public:
    A() {}
    ~A() {
      throw 1;
    }
};
int main()
{
    try {
        A tmp;
    } catch (...) {
        std::cout << "Caught" << std::endl;
    }

    return 0;
}

上面这段代码在A的析构函数中抛出了异常,外面的代码虽然进行了捕捉,但是是捕捉不到的,程序会直接退出。

如果类的析构函数真的可能会出现抛出异常的情况,推荐的处理方法有两种:

  • 方法1:在析构函数内部进行异常处理,直接退出或者吞掉异常。
class A {
public:
  A() {}
  ~A() {
    try {
      DoSomething();    // 可能产生异常的函数
    } catch(...) {
      std::abort();  // 直接退出或者在这里吞掉异常
    }
  }
};
  • 方法2:在方法1的基础之上,把析构函数中可能产生异常的函数独立出来作为类的一个成员函数,由用户在资源销毁时显式调用。
class A {
public:
  A() {}
  ~A() {
    try {
      DoSomething();    // 可能产生异常的函数
    } catch(...) {
      std::abort();  // 直接退出或者在这里吞掉异常
    }
  }
  void DoSomething() {...}
};
条款09:不在构造、析构函数中调用virtual函数
  • 不在构造函数中调用virtual函数原因:当定义子类对象时,构造过程是从基类对象构造开始,然后再执行子类对象部分构造。如果在基类的构造函数中调用了一个virtual函数,这时候该virtual函数还是指向基类对应的函数,因为子类对象还没有初始化好,这样就没有达到多态的预期效果。
  • 不在析构函数中调用virtual函数原因:当销毁一个子类对象时,先销毁子类对象部分,然后再销毁基类部分。如果在基类的析构函数中调用了virtual函数,则会调用子类中对应的该函数,而这时候子类部分已经被释放了,成员变量处于未定义状态,如果这个virtual函数访问了这些成员变量,则会出现位定义的行为。
条款10:令operator=返回一个引用指向*this

这一条不是一个强制性的规范,而是大家都遵循的协议,比如你在类的定义中实现operator=的时候,最好是返回一个T&引用,如:

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

A a1, a2, a3;
a1 = a2 = a3; //用户可能这样写
条款11:operator=处理自我赋值

这条规则是接上一条规则的,如果在实现operator=的时候,如果针对自我赋值的情况没有处理好,会出现以下两种结果:

  • 错误1:类内对象指针变量指向了被销毁的对象
class T;
class A {
public:
  A& operator=(const A& other)
  {
    delete pObj;  // 销毁原有的T对象
    pObj = new T(*other.pObj);   // T的拷贝构造函数
    return *this;
  }
private:
  T* pObj;
};

很显然,如果是自我赋值的情况,上面的语句会将自己的T对象释放掉,显然后面就会出问题了。

  • 错误2:new对象时出现异常不安全问题
class T;
class A {
public:
  A& operator=(const A& other)
  {
    if (this == &other) return *this;  // 加入一个是否是自我赋值的判断
    delete pObj;  // 销毁原有的T对象
    pObj = new T(*other.pObj);   // T的拷贝构造函数,这个过程可能出现异常,如果出现异常,那么pObj就指向了一个已经销毁了的对象。
    return *this;
  }
private:
  T* pObj;
};
  • 正确的版本:能够处理自我赋值且异常安全的实现
class T;
class A {
public:
  A& operator=(const A& other)
  {
    T* pObjOrig = pObj;
    pObj = new T(*other.pObj);    
    delete pObjOrig;
    return *this;
  }
private:
  T* pObj;
};
条款12:复制对象时勿忘其每一部分

这里的复制对象是指类的拷贝构造函数(copying)和赋值运算符(copy assignment),前者用于一个类实例对象的定义,后者用于两个已经定义的类实例之间的赋值。

  • 子类的拷贝构造函数定义时,如果没有定义基类部分的构造,则默认调用基类的默认构造函数初始化基类部分,如下:
class B {
public:
  B() {}
  virtual ~B() {}
  B(const B& other) {}
};
class D : public B {
public:
  D(const D& other) {}   // 这里没有定义基类部分的初始化,则默认调用基类的默认构造函数,即B()进行初始化
};

按照上面的代码,当使用D的拷贝构造函数进行初始化一个D类实例对象时,对于基类部分可能没有达到预期的复制效果,一般建议如下方式进行子类的拷贝构造函数定义:

class B {
public:
  B() {}
  virtual ~B() {}
  B(const B& other) {}
};
class D : public B {
public:
  D(const D& other) : B(other) {}  // 调用B类的拷贝构造函数对基类部分进行初始化   
  • 子类的赋值运算符定义时,如果没有定义基类部分的赋值,则保持不变,如果要实现基类部分同时赋值,则按照如下方式编写:
class B {
public:
  B& operator=(const B& other) {}
};

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

推荐阅读更多精彩内容