Effective C++ 读书笔记

手头上有一本 Scott Meyes 的 Effective C++(3rd Edition),虽然中文的出版时间是感人的2011年(也就是说C++11的那些新特性都没讨论了),但看网上的一些评论,此书还是值得一读的。(PS:作者针对C++11和14的新特性有本新书 Effective Modern C++。。

Day 1:

Item 1: View c++ as a federation of languages

我果然还是喜欢在开头说一点总结性的东西。这里对条款1做一点记录。。

C++是一个多重范型编程语言,一个简单理解C++的方法是把C++视为多个次语言(sublanguage)组成的联邦,次语言之间的守则可能会不一致。次语言共有4个:C,面向对象(Object-Oriented C++),模版 (Template C++),STL。

一个例子:对内置类型(C-like)而言,pass-by-value通常比pass-by-reference高效;对用户自定义的对象,由于构造和析构函数的存在,pass-by-reference-to-const往往更好;而在传递STL迭代器(iterator)和函数对象(function object)时,pass-by-value则更好,因为它们都是基于C指针实现的。

Item 2: Prefer consts, enums and inlines to #define

#define只是预处理阶段的文本替换,我们要尽量利用编译器提供更多信息。比较常用的就是const代替#define的常量,用inline函数代替#define的宏函数。这两个平时都用的很多了。enum只是在下面很特定的例子中有用:

// 1.h
class A {
public:
    static const int x = 5;
    int y[x];
};

这里希望声明一个常量x,它以类成员形式出现,而且所有对象只保存它的一个副本。于是就有了这么一个static const的成员变量,但是旧编译器并不允许你在static变量声明时赋值,#define又无法体现x的作用域,这时就可以使用enum:

// new_1.h
class A {
public:
    enum { x = 5 };
    int y[x];
};

很无语是吧。。是个挺过时的故事。值得一说的是,你如果想要取1.h中x的地址(其实也是挺奇怪的,相当于取常量地址),是需要重新声明一下这个成员变量的:

// 1.cpp
#include "1.h"
#include<iostream>

const int A::x;  // already set value to 5, if not, assign a value here

int main() {
    A a;
    std::cout << a.x << std::endl;  // ok with or without the redeclaration
    std::cout << &(a.x) << std::endl;  // must redeclare A::x
    return 0;
}

Day 2:

Item 3: Use const whenever possible

const是一个神奇的关键字,它是C++语法的一部分,却允许你对程序的语义做出限制。应该尽可能在程序中使用const关键字,这样编译器能够帮你做更多。

class Rational {
    const Rational operator * (const Rational &lhs, const Rational &rhs) {...}
};

int main() {
    Rational a, b, c;
    // should be "==", but will pass compilation if "const" missed
    if((a * b) = c) {...}  
}

const在指针上有两种语义:1)指针本身不能指向其他地址;2)指针所指地址的值不能变动。分别对应:

char greeting[] = "hello";
char* const p1 = greeting;
const char* p2 = greeting;

Item 1中提到了,STL的迭代器是基于指针的。在STL中这两类迭代器分别对应:

std::vector<int> vec;
const std::vector<int>::iterator iter1 = vec.begin();
std::vector<int>::const_iterator iter2 = vec.begin();

const在C++中如此重要的另一个因素是在用户自定义类型上,pass-by-reference-to-const比pass-by-value高效。因此,应当尽可能多用const关键词修饰成员函数,已方便在const对象上的操作。
关于一个成员函数是否能用const关键字,编译器关心的是bitwise constness,即函数内部没有对成员变量的赋值;而程序员更应当关心程序事实运行中的logical constness,即有些变量可能会被修改,但需要保持const的变量事实上是不会变的,两个例子:

// a program to "beat" bitwise constness
// we only apply const function on const object, but the string value changed

#include<iostream>
#include<cstring>

class Text {
    char *content;
    public:
    // ...
    char& operator [] (std::size_t idx) const {
        return content[idx];
    }
};

int main() {
    char greeting[] = "hello";
    const Text text = Text(greeting);
    char *p = &text[0];
    *p = 'j';
    std::cout << text[0] << std::endl;
    return 0;
}

注意,这个例子并不是说编译器错了,事实上它的确满足bitwise constness原则,content指针的值并没有发生变化。但我们明显是希望content所指向的字符串也保持不变。事实上,这是使用指针的问题,在重载[]时返回const char&或者用string类型作为成员变量,编译器都能发现这段程序的错误行为。

// a snippet explaining "logical constness"
// the const function getLength() updates two variables
// and they are declared as mutable

class Text {
    char *content;
    mutable std::size_t length;
    mutable bool lengthIsValid;
    public:
    // ...
    std::size_t getLength() const {
        if(!lengthIsValid) {
            length = strlen(content);
            lengthIsValid = true;
        }
        return length;
    }
};

最后,很多时候我们会为const对象实现一个const成员函数,为非const对象实现一个非const成员函数。他们行为几乎完全一样,如上述重载[]前要做一系列合法性检查。那么为了减少代码冗余,一个常见的操作是用非const函数调用const函数,这里展示一下C++风格的类型转换写法:

const char& operator [] (std::size_t idx) const {
    // many lines of code
    return content[idx];
}
char& operator [] (std::size_t idx) {
    return const_cast<char&>(static_cast<const Text&>(*this)[idx]);
}

Day 3:

Item 4: Make sure objects are initialized before they're used

C++中来自C语言的数据类型是不保证初始化的,因此请手动初始化所有的内置类型。

对于用户定义类型,请使用成员初值列(member initialization list)代替赋值操作,这是出于效率上的考虑:

// constructor in assignment style
A::A(const string &s, const std::list<int> l) {
    str = s; lst = l;
}
// a more efficient constructor
A::A(const string &s, const std::list<int> l): str(s), lst(l) {}

具体来说,对于A类的成员变量str和lst(它们都不是内置类型),它们的初始化时间在调用A构造函数之前,也就是说在上面的写法中,str和lst先用各自的默认构造函数初始化后,又进行了一次赋值;而在下面的写法中,它们就是用参数值初始化的。

对于跨文件的static变量,请用local-static对象替换non-local static对象。因为跨文件的static变量初始化顺序是不确定的,而有时我们必须指定特定顺序,举例而言:

// fs.h
class FileSystem {
public:
    // lines of code
    void foo() {}
};

FileSystem& fs() {
    static FileSystem fsObj;
    return fsObj;
}
// fs.cpp
tfs().foo();

调用全局变量就不太好了:

// fs.cpp
extern FileSystem fs;
fs.foo();

当有多个跨文件的全局变量时,上面写法中FileSystem对象的创建时间是可控的,下面则可能造成多个对象创建顺序的混乱。

Day 4:

Item 5: Know what functions C++ silently writes and calls

你写了一个空的类(只声明了一些成员变量),C++编译器会按需为你生成default构造函数,copy构造函数,析构函数以及重载赋值符号。而一旦你声明了对应的函数,C++就不会再默默为你做这些事。

class Empty {};
// the above code equals below
class Empty {
    Empty() { ... }
    Empty(const Empty &rhs) { ... }
    ~Empty() { ... }
    Empty& operator = (const Empty &rhs) { ... }
};

这些默认函数的行为都是naive的,对于基础类型,copy构造函数和赋值只做简单的bits拷贝。对于string这样定义了copy构造函数和赋值的类型,则会调用对应函数。
这种naive方法当然有失效的时候,比如你定义了一个const成员变量,或者你的成员变量是一个引用,那么很明显bits拷贝的方法是不行的,你必须手动初始化这两类变量。编译器会提示你显式地编写构造函数,copy赋值函数,重载赋值符。

Day 5:

Item 6: Explicitly disallow the use of compiler-generated functions you do not want

有些类不应当存在拷贝操作,让编译器帮助你的方法是将这个类的copy构造函数和赋值重载声明为private

class UnCopyable {
    UnCopyable(const UnCopyable&);  // parameter name can be skipped
    UnCopyable& operator = (const UnCopyable&);
    public:
    // lines of code
};

为了进一步防止成员函数和友元函数调用赋值,你需要使这两个private函数的函数体为空。这样如果你在成员函数或友元函数中使用了赋值,连接器(linker)会报错。

Item 7: Declare destructors virtual in polymorphic base classes

先看一份代码:

#include <iostream>

using namespace std;

class A {
    public:
    int x;
    virtual ~A() {
        cout << "Destructor A is called" << endl;
    }
};

class B: public A {
    public:
    int y;
    ~B() {
        cout << "Destructor B is called" << endl;
    }
};

class C {
    public:
    int x;
    ~C() {
        cout << "Destructor C is called" << endl;
    }
};

int main() {
    A *ptr = new B();
    delete ptr;
    cout << "size A: " << sizeof(A) << endl;
    cout << "size C: " << sizeof(C) << endl;
    return 0;
}

运行结果:

Compiler: GNU G++14 6.4.0
Ouput:
Destructor B is called
Destructor A is called
size A: 8
size C: 4

这份代码告诉我们两件事:
1)不要无端的将一个类的某些函数声明为virtual,这会使你的类体积变大,其原因是有虚函数的类会有一个虚指针(virtual table pointer),用于指向运行时实际调用的函数。
2)如果一个类是作为基类使用,并且会设计多态,那么一定要将构造函数声明为virtual,否则示例代码中只会调用A类的析构函数,从而B类在堆中为y申请的内存会发生泄漏。

Day 6:

Item 8: Prevent exceptions from leaving destructors

C++中当有多个异常被抛出时,程序不是结束执行就是导致不明确行为。而如果在析构函数中抛出异常,那么这样的情况非常容易发生,比如销毁一个vector!因此,绝对不应该让析构函数抛出异常,而是应该在析构函数中就对其捕捉并处理(考虑刚才提到的危害,在析构函数中发现异常后直接退出程序也是可以接受的)。
另一个做法是将可能抛出异常的部分代码独立出来作为一个成员函数,让用户手动去调用并处理可能带来的异常。类本身也可以设置一个变量记录进入析构函数时这个可能抛出异常的程序是否已经被调用。如果没有,那么捕获并处理掉这个异常,如果已经被调用,那么就可以摆脱异常处理这一烦恼了!这相当于是将部分风险责任分担给了用户从而获得的“双保险”做法。

Item 9: Never call virtual functions during constructor or destructor

虚函数的作用是在多态环境下调用正确类的函数。而在构造函数和析构函数中使用虚函数则会无法保证这一点,这可能会带来困惑。究其原因是一个派生类在调用自己的构造函数前,会先调用基类的构造函数,此时这个派生类对象的运行时类型(runtime type information)就会是基类!如果基类的构造函数中调用了虚函数,这个虚函数是没有办法指向派生类中的实现的!析构函数的道理类似。看完这段,不如猜一猜下面代码的输出吧!

#include <iostream>

class A {
    int x;
    public:
    A() {
        std::cout << "Construct A" << std::endl;
        foo();
    }
    virtual void foo() {
        std::cout << "A::foo() invocation" << std::endl;
    }
    ~A() {
        std::cout << "Destruct A" << std::endl;
        foo();
    }
};

class B: A {
    int y;
    public:
    B() {
        std::cout << "Construct B" << std::endl;
    }
    void foo() {
        std::cout << "B::foo() invocation" << std::endl;
    }
    ~B() {
        std::cout << "Destruct B" << std::endl;
    }
};

int main() {
    B b;
    return 0;
}

正确答案:

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