全文参考自Effective C++, Scott Meyers
程序来自本人
https://github.com/hxz1998/ccl

1. 让自己习惯C++

C++高效编程守则视状况而变化,取决于使用C++的哪一部分。

C++四大块:

  • C
  • Object-Oriented C++:面向对象
  • Template C++:泛型编程,模板元编程
  • STL:容器,迭代器,算法,函数对象

2. 尽量以const,enum,inline替换 #define

  • 对于单纯常量,最好使用 const 或者 enums 来替换 #define
  • 对于形似函数的宏(macros),最好使用 inline 函数来替换 #define

有一种方法,可以获得宏带来的效率,以及一般函数带来的可预料行为以及类型安全性(Type Safety),例如:

template<typename T>inline T callWithMax(const T &a, const T &b) {    return a > b ? a : b;}

int main() {    int a = callWithMax<int>(1, 2);    cout <endl;}

3. 尽可能使用 const

  • 声明为 const 可以让编译器帮助检查错误。
  • const 可以施加于任何作用域内的对象、函数参数、函数返回类型、成员函数。
  • 编译器强制实施bitwise constness,但编写程序时,应该使用“概念上的常量性”conceptual constness
  • constnon-const 成员函数有实质等价的实现时,要用 non-const 版本去调用 const 版本,这样可减少代码重复。

const 如果出现在 * 左边,那么表示被指物是常量;如果在 * 右边,那么表示指针是常量;如果出现在两边,那么表示指针和被指物都是常量。例如:

int main() {    int a = 0, b = 1;    int const *p1 = &a;    int *const p2 = &a;

    *p1 = 2;    // 不行!因为 p1 指向的内容是常量    *p2 = 2;    // 可以,p2 自身是常量,p2 只能指向a,但是 a 中的内容可以变    p1 = &b;    // 可以,p1 指向另一个内容,并声称这个内容不可变    p2 = &b;    // 不可以,p2 自身是常量,不能指向其他东西了}

试着习惯这样的写法:

void f1(const int *i); // 指向一个不能修改内容的 ivoid f2(int const *i); // 一猫猫一样

这俩写法效果是一样的,都是指向一个内容不可变的数据(指针本身可以再修改指向的对象)。

对于第三点,一个很好的例子如下:

class Text {private:    std::string text;public:    const char &operator[](std::size_t pos) const {        return text[pos];    }

    char &operator[](std::size_t pos) {        return const_cast<char &> (         // 使用 const_cast 去掉 const 声明                static_cast<const Text &>   // 使用 static_cast 把 *this 转换成 const 对象                (*this)[pos]);              // 使用 (*this)[] 方法返回(这个时候是 const 结果,经过 const_cast 去掉 const 声明    }};

4. 确定对象被使用前已被初始化

  • 为内置对象进行手工初始化,因为C++并不能保证完全初始化好它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。
  • 初始列列出的成员变量,其排列顺序要和它们在类声明中的一致。
  • 为免除“跨编译单元初始化次序”问题,使用 local static 对象来代替 non-local static 对象。

如果成员变量是 const 的或者 references 的,那么它们一定需要有初值,不能被赋值。例如:

class X {    const int val;    int &re_val;public:    X(int val_, int &re_val_) : val(val_), re_val(re_val_) {}};

基类总是比派生类要先初始化好,例如:

class X {    const int val;    int &re_val;public:    X(int val_, int &re_val_) : val(val_), re_val(re_val_) {        cout <"X initialization..." <endl;    }};

class Y : public X {public:    Y(int val, int &re_val) : X(val, re_val) {        cout <"Y initialization..." <endl;    };};

int main() {    int re_v = 1;    Y y(1, re_v);    // >: X initialization...    //    Y initialization...}

由于定义于不同编译单元内的 non-local static 对象的初始化顺序并未明确定义,因此会出现这样情况:

  • 定义在 File1.hh 中一个静态全局变量 tfs
  • File2.cc 中使用 tfs

那么如果 File1.hh 中的 tfs 还没初始化好呢,File2.cc 中就想使用了,那么就会出现大问题!例如下面这个例子:

// File1.hh

#include using namespace std;class FileSystem {public:    size_t numDisks() const { return 0; }};extern FileSystem tfs;

// File2.cc

#include #include "File1.hh"using namespace std;class Directory {    size_t disks;public:    Directory() { disks = tfs.numDisks(); }    size_t getDisks() const { return disks; }};int main() {    Directory directory;    cout <}

这个时候编译器就会报错:

CMakeFiles\local_static.dir/objects.a(File2.cc.obj):File2.cc:(.rdata$.refptr.tfs[.refptr.tfs]+0x0): undefined reference to `tfs'

很明显,在 local_static 目录中没有找到该引用,因此报错了,那么该怎样做呢?

使用方法(类似于工厂方法)来获取这个值,而不是依赖编译器初始化

例如:

// File1.hh

class FileSystem {public:    size_t numDisks() const { return 0; }};FileSystem &getFS() {    static FileSystem tfs;    return tfs;}

// File2.ccclass Directory {    size_t disks;public:    Directory() { disks = getFS().numDisks(); }    size_t getDisks() const { return disks; }};int main() {    Directory directory;    cout <}

这样一来,就不用担心了☺。

不过,这样还是有另外一个问题,例如多线程环境下还是有不确定情况,处理这种麻烦情况的做法之一是:在单线程启动阶段,手动调用一遍所有的 reference-returning 方法。这样可以消除与初始化有关的“竞速形式(race conditions)”

5. 了解C++默默编写并调用哪些函数?

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

首先,开门见山地说,C++默认编写了默认构造函数、默认析构函数、拷贝构造函数,以及拷贝赋值函数,而且它们默认都是 inline 的。当然,这些函数的默认创建在一定时期是失效的,例如:

  • 默认构造函数:当提供了一个构造函数后,编译器不再为类提供默认构造函数,而且默认。
  • 默认析构函数:当提供了一个析构函数后,编译器就不再提供默认析构函数,默认析构函数是 non-virtual 的。
  • 拷贝构造函数:只要没提供,而且满足可拷贝构造的条件,那么就提供,否则不提供。
  • 拷贝赋值函数:只要没提供,而且满足可拷贝复制的条件,那么就提供,否则不提供。

上面说了两个条件,那么具体是什么条件呢?

5.1 可拷贝构造&可拷贝赋值?

先来看一下满足这俩条件的例子:

template<typename T>class NamedObject {private:    T objectValue;    string name;public:    NamedObject(string n, T val) : name(n), objectValue(val) {}    NamedObject(const NamedObject &rhs) {        objectValue = rhs.objectValue;        name = rhs.name + " copy ";    }friend ostream &operator<const NamedObject &rhs) {        os <" " <        return os;    }    NamedObject &operator=(const NamedObject &rhs) {        objectValue = rhs.objectValue;        name = rhs.name + " = ";return *this;    }};int main() {string newDog = "newDog";string oldDog = "oldDog";NamedObject<int> od(oldDog, 1);NamedObject<int> nd(newDog, 2);cout <" : " <endl;  // >: 2 newDog : 1 oldDog    nd = od;    cout <// >: 1 oldDog =}

那么此时,即便自己不提供拷贝构造以及拷贝赋值构造操作符,编译器也会对成员变量进行递归的拷贝赋值过来。但是在遇到成员变量是 const 或者 reference 类型时,编译器就两手一摊,无能为力了(具体可参考Effective C++, 3th, P37)。

例如下面的例子:

template<typename T>class NamedObject {private:    const T objectValue;    string& name;public:    // 其他函数都一样    NamedObject(const NamedObject &rhs) {        objectValue = rhs.objectValue;        name = rhs.name + " copy ";    }    NamedObject &operator=(const NamedObject &rhs) {        objectValue = rhs.objectValue; // 不能对一个 const 对象赋值!        name = rhs.name + " = ";return *this;    }};int main() {string newDog = "newDog";string oldDog = "oldDog";NamedObject<int> od(oldDog, 1);NamedObject<int> nd(newDog, 2);    nd = od;    // >: error: use of deleted function 'NamedObject& NamedObject::operator=(const NamedObject&)'}

当然,这只是编译器不再提供了而已,用户自己还是可以设计如何去复制拷贝以及构造拷贝的,这完全取决于自己怎么处理成员变量。

除此之外,如果基类把拷贝构造函数设置成了 private 那么在派生类中也是没办法操作的。

6. 若不想使用编译器自动生成的函数,那该明确拒绝

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。或者使用继承 Uncopyable 这样的基类。

如果不想让一个类支持拷贝构造或者赋值构造,那么我们可以将函数声明但不实现,例如这样子:

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

当然,对于每一个想实现这个功能的类都能去单独这样声明,不过,还可以使用继承方法去实现,例如:

class Uncopyable {protected:    Uncopyable() = default;private:    Uncopyable(const Uncopyable &);    Uncopyable &operator=(const Uncopyable &);};class SubClass : public Uncopyable {    // 默认不允许拷贝构造和赋值运算符};int main() {    SubClass s1, s2;    s1 = s2;    // error!}

7. 为多态基类声明virtual析构函数

  • 带多态性质的基类应该声明一个 virtual 析构函数。
  • 如果类带有任何 virtual 函数,那么它就应该拥有一个 virtual 析构函数。
  • 如果类的设计目的不是用来做基类的,那么就不应该声明 virtual 析构函数。

当使用基类指针指向派生类对象时,没有问题,但是要是想把这个基类指针给删掉,这时候问题就来了,例如下面这个例子:

class BaseClass {private:    char *name;public:    BaseClass(int size) {        name = new char[size];        for (int i = 0; i 'a';    } // 基类的析构函数    ~BaseClass() { delete name; }};

class DeriveClass : public BaseClass {private:    char *count;public:    DeriveClass(int size) : BaseClass(size) {        count = new char[size];        for (int i = 0; i 'b';    }    // 派生类的析构函数    ~DeriveClass() { delete count; }};int main() {    // 多态用法,基类指针指向派生类对象,没毛病    BaseClass *obj = new DeriveClass(16);    // 删除基类指针,出现了问题!    delete obj;    return 0;}

上面的程序乍一看看不出个毛病来,现在对 BaseClass *obj = new DeriveClass(16); 设置断点,进行单步调试,可以观察到构造函数过程是:

new DeriveClass(16)    |BaseClass(16)    |DeriveClass(16)    |    end

这个顺序完全正确,先构造基类再构造派生类嘛,执行完后,内存状态是这样的:

可以得知操作系统给这两个对象中的成员分配内存到了 name : 0x1061980count : 0x10619c0

那么执行 delete obj; 时,顺序是这样的:

delete obj    |~BaseClass()    |    end

从上面可以看出来,竟然只执行了基类的析构函数,而没有执行派生类的析构函数,那么这时的内存表示是怎么样的?见下图:

由此可见,在不经意间,就造成了内存泄漏问题,那么该如何解决这个问题呢?

很简单,只需要把基类的析构函数声明为 virtual 就可以了,这样强制去执行子类的析构函数。

不过,这样还是有两种结果,例如下面是一种结果:

class BaseClass {    // 其他都一样    virtual ~BaseClass() { delete name; }};class DeriveClass : public BaseClass {    // 其他都一样    ~DeriveClass() override { delete count; }};

这个时候,是先执行的派生类析构函数,再执行基类析构函数。

另一种结果是:

class BaseClass {    virtual ~BaseClass() { delete name; }};class DeriveClass : public BaseClass {    // 删掉了自己的析构函数};

这个情况下,才是先执行基类析构函数,再执行派生类析构函数。

8. 别让异常逃离析构函数

  • 析构函数绝对不要抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。
  • 如果接口使用者需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而不是在析构函数中)执行操作。

即便C++允许析构函数抛出异常,但是最好不要这样做。当然,吞掉异常也是有争议的,比如“草率地结束程序”可能会带来更严重的问题,或者“不明确的行为带来的风险”可能会带来不安全的问题等等,具体问题具体分析是比较好的。

但是,通常可以提供一个让用户在析构函数前控制异常的机会,例如使用“双重保险”来尽最大化确保问题得到解决。

9. 绝不在构造函数和析构函数过程中调用virtual函数

在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至派生类。

不管怎样,都不应该在构造函数和析构函数内部去调用一个 virtual 函数,因为这样的操作是不可预估的,带来意想不到的结果。为什么这样?因为在基类中,构造函数执行阶段或者析构函数执行阶段只能看到基类的内容,所以在派生类中实现的程序,是不可用的。下面这句话直白且有效的指出了问题的所在:

在基类(base-class)构造期间,virtual 函数不是 virtual 函数。

也正是因为这样一个“对象在 derived class 构造函数开始执行前,不会成为一个 derived class 对象”的规则,所以最好在构造期间对 virtual 函数视而不见。

那么如何科学有效地去解决这个问题?当然是在基类中把需要在构造函数内执行地函数设置成非 virtual 函数。

总之就是,在基类构造和析构期间调用的 virtual 函数不可下降至派生类。

10. 令 operator= 返回一个 reference to *this

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

为什么这样做呢?是因为可以实现类似于这样的程序:

a = b = c = 10;

因此,我们在编写类的 operator= 操作符时,可以写成:

class BaseClass {public:    BaseClass &operator=(const BaseClass &rhs) {        // 随便干点什么        return *this; // 关键在于这里    }};

当然啦,也可以不做返回,不过既然这是一个好的实践,那么没有确切的理由不去做,最好就去做。

11. 在 operator= 中处理“自我赋值”

  • 确保当对象自我赋值时,operator= 有可预估的行为。其中需要注意的包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序以及拷贝交换。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

简而言之,就是需要考虑操作符两边是否是同一个对象,因为如果是同一个对象,会出现类似下面的问题:

class BaseClass {private:    char *data;public:    BaseClass &operator=(const BaseClass &rhs) {        delete data;        data = rhs.data;        return *this;    }};int main() {    BaseClass baseClass;    baseClass = baseClass;}

自己给自己赋值,没毛病,但是在运算符函数的 delete data 却带来了问题,因为它删除掉了自己的内存空间,却在下面那行 data = rhs.data 又想用了,而这时系统已经收回了这块空间,这样一来操作系统肯定是不干的,所以程序就报错了。

那么该如何解决呢?这样:

class BaseClass {private:    char *data;public:    BaseClass &operator=(const BaseClass &rhs) {        // 多一个检查是否是自己的操作就可以了,也称证同测试        if (&rhs == this) return *this;        delete data;        data = rhs.data;        return *this;    }};

12. 复制对象时勿忘其每一个成分

  • 拷贝函数应该确保复制了“对象内的所有成员变量”以及“所有的 base class 成员”。
  • 不要尝试以某个拷贝函数去实现另一个拷贝函数,应该将两者共同的部分抽取到一个新的函数中去完成,然后由两个拷贝函数共用。

一般而言,如果自己不声明拷贝构造函数和拷贝赋值操作符,那么编译器会帮自己生成的,但是!重点来了!如果选择了自己去声明定义,那么麻烦事就来了(因为即便可能出错编译器也不会告诉你)。

尤其是一个类派生自基类的时候,就需要小心谨慎地去处理基类的对象,然而有些是 private 的,因此复制起来比较麻烦,这个时候可以使用这样的方式来解决问题:

  • 对于拷贝构造函数,在初始化列表中显式地去调用基类的拷贝构造函数,然后在子类的拷贝构造函数内部处理好自己的问题。
  • 对于赋值拷贝操作符,在合适的位置显式调用基类的 operator=() 函数。

具体例子见下面:

class BaseClass {private:    string name;public:    BaseClass() = default;    BaseClass(int sz, char c) { name = string(sz, c); }    BaseClass(const BaseClass &rhs) : name(rhs.name) {}    BaseClass &operator=(const BaseClass &rhs) {        if (this == &rhs) return *this;        this->name = rhs.name;        return *this;    }    friend ostream &operator<const BaseClass &rhs) {        os <        return os;    }};class DeriveClass : public BaseClass {private:    int age;public:    DeriveClass(int a, int sz, char c) : BaseClass(sz, c), age(a) {}    // 必须要调用基类的拷贝构造函数,否则不会拷贝构造完全    DeriveClass(const DeriveClass &rhs) : age(rhs.age), BaseClass(rhs) {}    DeriveClass &operator=(const DeriveClass &rhs) {        if (&rhs == this) return *this;        age = rhs.age;        // 如果不调用下面这句,将会出现没有拷贝基类 name 值的问题!        BaseClass::operator=(rhs);        return *this;    }    friend ostream &operator<const DeriveClass &rhs) {        os <"\t" <        return os;    }};int main() {    DeriveClass d1(18, 3, '1');    DeriveClass d2(20, 5, '2');    DeriveClass d3(d1);    d1 = d2;    cout <endl <endl <    /**     * 正常输出:     * 22222    20     * 22222    20     * 111      18     */    /**     * 如果按照前两点建议,那么出现这样的情况概不负责;     * 111      20     * 22222    20     *          18     */}

总而言之,一旦选择了自己去完成拷贝构造函数复制拷贝操作符,那么就别怪编译器不厚道了,需要自己去谨慎操作。


软考之后终于可以静下心来看看书了?(开)?(心)

effective c++_【阅读笔记】Effective C++()相关推荐

  1. 《Effective Java》阅读笔记(二)

    最近在看<Effective Java>这本书,顺便就记录一些笔记,记录一下书中的一些知识点以及对知识点的总结.一般情况会记录所有的知识点,但是知识点太过简单或者无归纳点总结的就不做详细记 ...

  2. 《Effective Java》阅读笔记

    第2章 第1条:用静态工厂方法代替构造器 [1] 辅助理解静态工厂的文章,关于 Java 的静态工厂方法,看这一篇就够了! [2] 单例模式:Hi,我们再来聊一聊Java的单例吧 第2条:遇到多个构造 ...

  3. 阅读笔记-Effective Capacity Analysis of STAR-RIS-Assisted NOMA Networks

    STAR-RIS:即同时折射和反射可重构智能表面(simulta-neously transmitting and reflecting reconfigurable intelligent sur- ...

  4. 《Effective Java》阅读笔记7 避免使用终结方法

    1.序 本条的意思是,让你尽量不要在你的类中覆盖finalize方法,然后在在里面写一些释放你的类中资源的语句. 1.1为什么要避免覆盖并使用finalize方法? (1)finalize方法不能保证 ...

  5. 大道至简_阅读笔记02

    接下来是三四五章的阅读: 给我印象最深的就是第五章所说的在项目开发的过程中,难免遇到不少的问题,甚至是失败,但这并不代表什么,可能是我们在某些环节有一点漏洞,只要我们将其打上补丁,什么问题都能解决,但 ...

  6. verilog设置24进制计数器_阅读笔记:《Verilog HDL入门》第3章 Verilog语言要素

    3.1标识符 1.Verilog中的Identifier是由任意字母.数字.下划线和$符号组成的,第一个字符必须是字母或者下划线.区分大小写. 2.Escaped Identifier是为了解决简单标 ...

  7. 软件构架实践_阅读笔记01(1-3)

    之前的学期,我们学习了软件工程概论和软件需求分析,而下个学期即将学习软件体系架构.如课程安排的一样,如大众的观点一致:需求在架构之前.即传统的思想:在知道了系统的需求,就可以为此系统构建构架.而紧接着 ...

  8. 迁移学习_迁移学习简明手册(王晋东)_阅读笔记5-6

    5.迁移学习的基本方法 基于样本迁移: 根据一定的权重生成规则,增加源域中跟目标域样本相似度高的样本的权重. 增加狗类别样本的权重 基于特征迁移: 通常假设源域和目标域间有一些交叉的特征,通过特征变换 ...

  9. 《这些道理没有人告诉过你》_阅读笔记

    1/选择决定结果 在某种程度上,择校决定着一个孩子是否能学有所成,择业决定着一个人的事业是否 成功,择偶决定着一个人的生活是否美满.而学有所成.事业成功.生活美满,恰恰是 芸芸众生所追求的三大人生目标 ...

  10. 迁移学习_迁移学习简明手册(王晋东)_阅读笔记7-8

    7.第二类方法:特征选择 特征选择法的基本假设是: 源域和目标域中均含有一部分公共的特征,在这部分公共的特征上,源领域和目标领域的数据分布是一致的.因此,此类方法的目标就是,通过机器学习方法,选择出这 ...

最新文章

  1. 一文详解为什么Serverless比其他软件开发方法更具优势
  2. 如何在SharePoint Server中整合其他应用系统?
  3. 成功解决Please use the NLTK Downloader to obtain the resource:
  4. Mybatis学习--Mapper.xml映射文件
  5. 【Redis】4.Redis数据存储listsetsorted_set
  6. 信息学奥赛一本通 1025:保留12位小数的浮点数 | OpenJudge NOI 1.1 05
  7. python--split方法
  8. python去除列表中的重复元素,简单易理解,超详细解答,步骤分析
  9. tmb100 刷linux,天猫魔盒TMB100C短接刷机教程
  10. python写词法分析器_python实现词法分析器
  11. 软件测试的测试代码,软件测试(示例代码)
  12. 数论 —— 逆元与同余式定理
  13. 岛屿数量问题(C实现)
  14. 互联网医疗的千姿百态:火热、亏损、巨头亲赖
  15. 初始3D打印机(Hori 3D Z600)
  16. 【CTF WriteUp】2023数字中国创新大赛网络数据安全赛道决赛WP(1)
  17. 阿里云函数计算(fc)使用体验
  18. 最新ThinkPHP微信独立精彩互换抢红包系统源码开源版
  19. 海康NCG联网网关设备通过国标接入到EasyCVR视频图像智能分析平台注册失败问题排查
  20. google检索技巧-从菜鸟到黑客

热门文章

  1. iphone屏幕录制_无需第三方APP,苹果iPhone手机屏幕录制的方法
  2. lightroom 闪退_UP加速器闪退怎么办 UP加速器闪退解决方法
  3. MATLAB入门级知识
  4. 牛式 Prime Cryptarithm
  5. 谁拿了最多奖学金pascal程序
  6. AtCoder AGC024F Simple Subsequence Problem (字符串、DP)
  7. HDU 6741 MUV LUV UNLIMITED (博弈论)
  8. python文本关键词匹配_NLP利剑篇之模式匹配
  9. 关于Rabbitmq的routingkey的作用
  10. 【Unity3D与23种设计模式】模板方法模式(Template Method)