Skip to content

xiaoqiangkx/effective_cpp_notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 

Repository files navigation

Effective C++ Notes

Contents

Introduction

Effective C++属于pratice实践类的书籍. 主要传授工程中碰到的问题以及积累的经验. 看这类书是帮你快速成长的重要手段.

正如Scott Meyers在前言中所说的:

学习程序语言语法是一回事; 学习如何以某种语言设计并实现高效程序则是另一回事. 掌握一般性的设计策略和带有具体细节的特定语法特性是我的忠告.

同时我也比较赞同刘未鹏大神所说, 学习一个新事物, 你要问三个问题.

1 它的本质是什么? 2 它的第一原则是什么? 3 它的知识结构是什么?

看完Effective后, 希望能够得到这三个答案.

第一部分: 让自己习惯C++

Item 1: C++是一个语言联邦

C++由C, Objected-Oriented C++, Template C++和STL四种次语言组成, 每种次语言都有其守则和范例, 守则都倾向简单, 直观易懂和容易记住. 你需要在四种次语言中进行切换.

举个例子, 传递参数使用pass-by-value还是pass-by-reference, C内置类型往往使用值传递, 包括指针; 而迭代器和函数对象基于C指针所以也使用值传递, 但是Object-Oriented C++中的自定义对象使用pass-by-reference-to-const, 从而避免频繁的调用构造函数和析构函数.

本章没有办法举个实际的例子.

回到前面的问题, 可以对问题3做出一部分解答. 这个应该是C++的知识结构了, 由C, OO C++, 模板以及STL四部分组成. 个人认为C语言为了精简而存在大量的语言缺陷.后面的每个条目中我会记下所有的陷阱问题.

Item 2-3: 尽量以const, enum, inline替换#define; 尽量使用const

编译错误问题

P13文中提到包含#define ASPECT_RATIO 1.653的代码中, 可能出现一个编译错误信息, 该错误信息包括1.653, 而不包含ASPECT_RATIO, 此外, 如果define语句在其它的头文件中, 将带来更多的麻烦. 现代编译器不太出现1.653, 往往只是说明变量类型而已, error: invalid conversion from ‘int’ to ‘int\*’, 同时gcc编译器选项-g中ASPECT_RATIO往往也不会被替换为1.653. 所以宏使用起来还是很方便的. 当然考虑到其它编译器可能效果不同, 最后还是使用const常量比较好.

define.cpp

C的缺陷1

常用字符串允许赋值为char*-based指针, 从而导致段错误

代码如下:

char *p = "test";
*p = 'A';

编译warning: warning: deprecated conversion from string constant to ‘char*’

运行error: Segmentation fault (core dumped)

缺陷改正方法: 使用const char* 来保存test信息. 如果p唯一指向此字符串, 使用const pointer即可.

const\_pointer.cpp

类的const static变量

类内部的const static变量仅仅是声明而已, 可以使用, 但是不同取地址以及赋值给引用. 作用类似于define. 只有在cpp文件中定义此变量后, 才会分配内存空间.

更多看详情

// *.h
class GamePlayer {
    private:
        static const int NumTurns = 5;  //声明式
        int scores[NumTurns];           //使用该常量
};

// *.cpp
const int GamePlayer::NumTurns;         //定义式

随想: C++的本质是什么? C++是一个编译语言, 编译器负责了变量声明以及变量定义, 定义时涉及静态内存分配, 甚至还有模板元编程. 那么哪些是在运行时才知道的呢?

const\_member.cpp

错误代码如下: undefined reference to Test::a'`

struct Test {
    static const int LEN = 5;
    int a[LEN];
};

// const int Test::LEN;

int main() {
    const int *p = &Test::LEN;
    return 0;
}

解释: 编译过程中所有的Test::LEN都编译为常量了, 没有没有内存分配. 对于PYTHON这种动态语言, 由于所有的标识符可以会被其它程序加载并改变, 所以没有这种常量类型.

注: 某些编译器不支持类内const static变量声明初始化, 此时使用enum替代即可.

inline函数替代宏函数

宏函数缺点: 存在副作用, ++/--等操作被复制, 导致运行多次.

#define f(a, b) ((a) > (b) ? (a) : (b))
int a = 2;
f(++a, 10); // a = 3
f(++a, 1);  // a = 4

函数返回自定义类型时尽量设置为const

作用:

帮助编译器侦测出错误信息

P18提到, operator*相乘应该返回const值, 避免被赋值, 由于内置类型的返回值被赋值是非法的.

问: compare函数应该返回const类型吗? 应该其值不应该被赋值

const\_return.cpp

函数const重载

  • 编译器使用bitwise constness(即不修改变量的每个bit), 而实际中使用logical constness, 使用mutable来实现修改

  • const重载可以保证const对象和非const对象的正确调用, 副作用是代码的重复性.

const\_function.cpp

Item 4: 确定对象被使用前已先被初始化

C parts of C++, 仅函数外内置类型会初始化, OO/STL parts of C++, 自定义类型会调用构造函数, 容器会调用元素的函数或者值初始化.

如果有多个构造函数, 可以将那些"赋值表现像初始化一样好"的成员变量改用赋值操作, 从而将这些赋值操作移到某个函数中去.

"成员初始化次序"固定, 先初始化base类, 然后是derived类, class的成员变量按声明顺序进行初始化.

重点: 定义于不同编译单元内的non-local static对象(不包括全局作用域static变量), 无法保证初始化顺序. 常用方法是使用单例singleton模式.使用全局函数的local static对象

local\_static.cpp

第二部分: 构造/析构/赋值运算

Item 5: 了解C++默认编写并调用哪些构造函数

问题: 何时编译器不会生成默认构造函数, 析构函数, 复制拷贝函数以及赋值函数?

当没有生成对象, 复制或者赋值时; 当base类的相应函数为私有时会报错, 当存在不可赋值的成员,如引用\ostream\const常量等值时, 系统报错; 当自定义了上述函数时, 编译器就不管了

问题: 何时编译器会去生成这些函数?

一旦创建了对象就会生成构造函数和析构函数, 使用初始化或者赋值就会生成复制或赋值函数.

silent\_function.cpp

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

通过Item5中提到了基类private复制和赋值构造方法实现拒绝复制

uncopyable.cpp

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

需求1: 当基类拥有一个或者多个virtual函数时, 即作为多态使用时, 一定要定义虚析构函数

方法: 使用valgrind来测试局部销毁, 参见partial\_delete.cpp, 顺便提到了对齐问题

需求2: 当一个基类需要定义为虚基类时, 没有虚函数可以设置为纯虚函数的话, 可以设置纯虚析构函数并且给出一个定义.

pure\_consturctor.cpp

Item 8: 别让异常逃离析构函数

情景: 如果有一个vector<Widget> v, 当它被析构时, 若某个Widget析构报错异常而程序没有终止, 此时大量资源没有析构, 程序行为未定义; 若一个Derived对象析构时出现错误, 报异常, 子类可能没有析构, 此时出现错误. 那么如何处理此类问题呢?

方法: 提供一个接口函数来执行有异常的代码, 并在析构函数中吞下它们或者终止程序.

exception\_destructor.cpp

Item 9: 决不在构造函数和析构过程中调用virtual函数

在base class构造期间, virtual函数不是virtual函数. 存在两种解释: 一种是Base类构造函数中, Derived类的成员还没有初始化, 所以调用派生类的虚函数不合适. 另一方面在Base类时, this即表示Base类.

具体见link\_function.cpp

如果希望在基类中做一些和派生类有关的事情, 通过传参给构造函数来实现.

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

没什么好说的, 随众吧, 为了支持连锁调用.

Item 11: 在operator=中处理"自我赋值"

当类中有指针类数据时, 自我赋值时可能删除自己, 导致无法正常赋值.

identity\_test.cpp

解决办法:

  1. 避免在构造函数中使用资源的副本, 而不是引用它.
  2. 使用"自我赋值安全性"和"异常安全性"都好的copy-swap或者手工写法

手工写法精巧实用, 喜欢这个

identity\_cure.cpp

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

一旦你自定义了copy复制和赋值函数以及构造函数, 你就承担了初始化, 赋值以及拷贝的重任, 当你添加了新的成员变量时, 你需要主动添加它们, 当然将所有的赋值操作放在一个私有的成员函数里面是极好的; 当我们有了继承类时, 主动调用这些自定义copy方法吧. 赋值构造函数甚至根本就不会调用子类赋值构造函数的哦.

看一下子类为调用父类operator=的情况:

assign_fail.cpp

第三部分: 资源管理

所谓资源管理就是, 一旦用了它, 将来必须还给系统.资源包括内存, fd, mutex, 图形界面中的字型和笔刷, 数据库连接以及网络socket.

忘记归还资源的几种可能: 忘记delete; 异常; 提前的return.

解决方法: 把资源放进对象内, 我们便可依赖C++的"析构函数自动调用机制"确保资源被释放. 从而将手动资源归还变成了自动化(诱人的三个字啊)

相对于不同的情形, 处理手段不同

  1. 单一区块或函数内的资源分配, 使用智能指针auto\_ptr. 例如std::auto_ptr\<Investment\> pInv(createInvenstment())

    • 获得资源后立即放到管理对象中 RAII(Resouce Acquisition Is Initialization), 不要被其它代码获取到这份资源, 如果需要获得这份资源只能通过拷贝
    • 管理对象利用析构函数确保资源被释放
    • 注: 防止多个auto\_ptr指向同一对象, 导致多次归还资源. 而多个auto\_ptr之间进行复制将导致右操作数置NULL, 从而使左操作数获得资源的唯一拥有权. STL要求元素具有赋值功能, 所以auto_ptr不可以使用
  2. 非环状引用下大区域范围使用智慧指针shared\_ptr

注意: 由于auto\_ptrshared\_ptr都使用delete而不是delete[], 所以不要出现下面的例子

std::auto_ptr<std::string> aps(new std::string[10]);

例子见smart\_ptr.cpp

Item 14: 在资源管理类中小心copying行为

当一个RAII对象被复制时, 会发生什么事?

  1. 禁止复制, 将拷贝构造函数和赋值构造函数声明为私有不实现.(也可以通过private 继承uncopyable基类)

  2. 底层使用shared_ptr来保存资源, 并且给智慧指针赋予自定义删除器

令: auto_ptr采用转移底部资源的拥有权, 深度拷贝使用复制底部资源

mutex.cpp例子

Item 15: 在资源管理类中提供对原始资源的访问

个人意见: 获得原始资源可能出现多次释放资源的错误, 所以避免隐式的转换. 使用get做显示转换即可.

smart\_get.cpp小例子

Item 16: 成对使用new和delete时要采用相同形式

delete[]的解释: 动态对象数组中保存了"数组大小"的记录.

不要对数组形式使用typedef动作.

    typedef std::string Address[4]
    std::string *pal = new Address;
    // delete pal;  // 容易误用此方法
    delete [] pal;

Item 17: 以独立语句将newed对象置入智能指针

以独立语句将newed对象放入智能指针中, 防止被异常打断, 从而资源泄露.

newed\_exception.cpp

第四部分: 设计与声明

Item 18: 让接口容易被正确使用, 不易被误用

设计接口的第一要务是考虑用户可能做出什么样的错误. 例如Date(int month, int day, int year)的代码可能输入错误次序或者传递无效的月份或天数; 在防范"不值得拥有的代码"上, 类型系统是你的主要同盟国.

如何避免接口错误? 让你的程序自身提供一致接口, 同时模仿并与优秀的代码接口保持一致, 例如STL容器.

任何接口如果要求客户必须记得做某些事情, 就是有着"不正确使用"的倾向, 因为客户可能会忘记做那件事.使用shared_ptr而不是普通pointer可以避免程序员忘记delete或者多次delete.

Item 19: 设计class犹如设计type

class设计即内置type类型设计

注: 本节是对Item20~Item25的总结.

Item 20: 宁以pass-by-reference-to-const替换pass-by-value

通过reference-to-const来避免拷贝函数和构造函数的开销. const保证实参不会被修改. reference的本质是指针值传递, 所以内置类型直接使用值传递pass-by-value即可.

缓存器: 内置类型可以被放进缓存器, 而某些包含内置类型的光杆对象却不可以.

可以合理假设返回value的只有内置类型, STL迭代器和函数对象.

Item 21: 必须返回对象时, 别妄想返回其reference

operator\*这种操作如果返回stack上数据将会造成段错误; 如果返回heap上数据, 将会造成内存泄露; 如果使用static变量将无法满足多次调用*的情况, 而且无法满足多线程的需求.

static\_return.cpp

Item 22: 将成员变量声明为private

成员变量的封装性与"成员变量的内容改变时所破坏的代码数量"成反比. 所以protected变量和private变量都不要使用可以保证封装性和访问数据的一致性.

Item 23: 宁以non-member, non-friend替换member函数

C++标准程序库的组装方式, 将一些public调用函数的工具类放在同一namespace但不同头文件中.从而降低依赖性.

对象内数据的封装性: 越少函数可以访问数据, 封装性就越低.

Item 24: 若所有参数皆需要类型转换,使用non-member函数

无法何时如果你可以避免friend函数就该避免, 因为像真实世界一样, 朋友带来的麻烦往往多过其价值.

Item 25: 考虑写出一个不抛异常的swap函数

注: 暂时不看. 等看完C++ templates再说

第五部分: 实现

Item 26: 尽可能延后变量定义式的出现时间

效率: 尽量在需要明确初值时去定义变量. 这样可以省去调用default构造函数. 在循环中使用n组析构函数或者n个赋值函数+更大的作用域污染两种选择面前, 除非特别讲究效率的代码, 选择小作用域的代码更利于维护和升级.

Item 27: 尽量少做转型动作

C++规则的设计目标之一是, 保证"类型错误"绝不可能发生. 理论上如果你的程序很"干净的"通过编译, 就表示它并不企图在任何对象身上执行任何不安全, 无意义以及愚蠢荒谬的操作.

注意: 单一对象可能拥有一个以上的地址, 我们应该避免做出"对象在C++中如何如何布局"的假设, 当然更不应该依次假设为基础执行任何转型动作. 例如将对象地址转型为char*指针然后进行指针算术. 几乎总是会导致无定义以及平台相关性. 这个世界有许多悲惨的程序员, 他们历经千辛万苦才学到这堂课.

dynamic转型效率往往低下, 所以尽量避免使用.

static\_cast.cpp

Item 28: 避免返回handles指向对象内部成分

返回Reference, 指针和迭代器统统都是所谓的handles, 而返回一个"代表对象内部数据"的handle, 随之而来的便是"降低对象封装性"的风险. 即使调用const成员函数却造成对象状态被更改.

返回handle的后果是"handle比起所指对象更加的长寿"

handle\_dangling.cpp

Item 29: 为"异常安全"而努力是值得的

异常安全函数: 即使发生异常, 保证资源不泄露, 任何数据结构不被破坏. 使用智能指针保证内存, metex等资源不出问题. 使用copy-and-swap或者精巧代码保证原始数据结构不被破坏.

异常安全分为基本型(异常抛出后程序状态有效但是不一定不变), 强烈保证(程序状态不变, 往往不现实), 不抛保证(但是可能调用了abort等函数). 函数提供的"异常安全保证"通常最高只等于其所调用的各个函数中的最弱者.

Item 30: 透彻了解inlining的里里外外

inlining的坏处: 代码膨胀(大量本体替换导致代码膨胀); 降低告诉缓存装置的命中率, 由于无法复用一个page里面的代码; inline只是一个建议而已; 所有调用inline函数的代码需要随着inline函数的改变而重新编译; 不利于调试.

大多数inline在编译期完成.

不适合inline的场景: 循环语句, 构造函数和析构函数. 模板也要小心.

一个程序往往将80%的执行时间花费在20%的代码上头. 作为一个开发者, 找出其中20%的代码, 并竭力为之瘦身.

Item 31: 将文件间的编译依存关系降至最低

class中如果可以用object reference或object pointers完成的任务, 就不要使用objects, 从而降低编译依存关系. 尽量以class声明式替换class定义式. 希望类的变化不会影响到Person类

两种方式

  1. 相依于声明式, 而不是定义式; Person(仅需一个PersonImpl前置声明即可)和PersonImpl类.
  2. 使用抽象基类, 抽象基类编译时采用动态内存分配, 所以没有依赖性

代价: 引入虚函数的开销, 无法使用inline方法(由于所有函数实现必须放在cpp文件中, 否则在Person.h中引入其它头文件的话又引入了其它类的实现, 导致Person被污染)

见Person和PersonImpl的两种实现.

第六部分: 继承和面向对象设计

如果你了解C++各种特性的意义, 你会发现它不再是一项用来划分语言特性的仪典, 而是可以让你通过它说出你对软件系统的想法.

Item 32: 确认你的public继承塑模出is-a关系

public继承就是"is-a"的关系. 把这个规则牢牢烙印在心中. 适合base class的情况一定适合derived class对象.

某些领域里面的直觉不一定会帮助你. 比如正方形是一种长方形. 矩形(宽度可独立于其高度被外界修改)却不可行施行在正方形身上(宽度总是和高度一样).

Item 33: 避免遮掩继承而来的名称

遮掩继承函数将会违反is-a的设计思想. 即使你是想避免疏远的类也是不应该的.再远的亲戚也是亲戚.

使用using或者转交函数来避免加载所有函数.

hide\_fun.cpp

Item 34: 区分接口继承和实现继承

pure virtual 函数提供一个接口, 无法提供实现; impure virtual提供接口和一份默认实现, 鼓励用户重写; (前两者的中间情况: 实现pure virtual函数, 既保证了虚基类无法实例化, 又可以防止子类错误继承实现.或者书写一个protected方法) non-virtual函数一般不要修改其实现.

见`pure_implementaion.cpp

Item 35: 考虑virtual函数以外的其它选择

有点难: 暂时记下了, 使用策略, NVI等模式.

Item 36: 绝不重新定义继承而来的non-virtual函数

is-a的特性, 如果重新定义了"不变形凌驾于特异性"的non-virtual函数, 那么子类将表现不一致.

Item 37: 绝不重新定义继承而来的缺省参数值

virtual函数系动态绑定, 缺省参数却是静态绑定.

virtual\_default\_para.cpp

Item 38: 通过复合塑模出has-a或"根据某物实现出"

通过组合has-a或者重写某个类来实现一个新的类

Item 39: 明智而审慎地使用private继承

当class间关系是private时, 编译器不会自动将派生类对象转换为基类对象. Private继承意味着implemented-in-terms-of, 只能通过基类的接口来重塑. 两者没有任何观念上的关系.

尽可能使用复合, 必要时才使用private继承.

优点:

  • 复合无需继承相应的函数接口
  • 复合中使用pimpl, 可以使编译依存性降到最低.
  • private继承优点: 可以访问protected成员, 以及重新定义virtual方法.

注: EBO问题, private继承的基类大小为0, 但是复合的基类大小为char, 而成员基类对象可能为int(使用对齐)

Item 40: 明智而审慎地使用多重继承

函数重载先确定最佳匹配而后才检验其可用性, 因而private和public的两个函数也可以造成二义性.

virtual继承代价较大.

有一种多重继承的实现方式, public继承Person类, private继承Person中pimpl用到的Person_Info类(由于新类需要重写virtual方法).

注意事项

  1. 建议引入C++标准的c文件 引入C中.h文件时, size\_t等变量存在global作用域::中; 而引入C++的c\*文件时, 变量在std::中.

  2. C++中不要使用动态内存作为异常, 容易造成内存泄露. 参考http://stackoverflow.com/questions/23590266/how-does-the-memory-leak-happen

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published