Effective C++读书笔记

Posted AlexP5

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective C++读书笔记相关的知识,希望对你有一定的参考价值。

让自己习惯C++

视C++为一个语言联邦

  1. C语言
  2. 面对对象
  3. C++模板
  4. STL容器

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

  1. const的好处:
    1. define直接常量替换,出现编译错误不易定位(不知道常量是哪个变量)
    2. define没有作用域,const有作用域提供了封装性
  2. enum的好处:
    1. 提供了封装性
    2. 编译器肯定不会分配额外内存空间(其实const也不会)
  3. inline的好处:
    1. define宏函数容易造成误用(下面有个例子)
//define误用举例

#define MAX(a, b) a > b ? a : b

int a = 5, b = 0;
MAX(++a, b) //a++调用2次
MAX(++a, b+10) //a++调用一次

然而,了解宏的机制以后,我们也可以用宏实现特殊的技巧。例如:C++反射,TEST

宏实现工厂模式

  1. 需要一个全局的map用于存储类的信息以及创建实例的函数
  2. 需要调用全局对象的构造函数用于注册
using namespace std;

typedef void *(*register_fun)();

class CCFactory
public:
  static void *NewInstance(string class_name)
    auto it = map_.find(class_name);
    if(it == map_.end())
      return NULL;
    else
      return it->second();
  
  static void Register(string class_name, register_fun func)
    map_[class_name] = func;
  
private:
  static map<string, register_fun> map_; 
;

map<string, register_fun> CCFactory::map_;

class Register
public:
  Register(string class_name, register_fun func)
    CCFactory::Register(class_name, func);
  
;

#define REGISTER_CLASS(class_name); \\
  const Register class_name_register(#class_name, []()->void *return new class_name;);

尽可能使用const

  1. const定义接口,防止误用
  2. const成员函数,代表这个成员函数承诺不会改变对象值
    1. const成员只能调用const成员函数(加-fpermissive编译选项就可以了)
    2. 非const成员可以调用所有成员函数

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

  1. 内置类型需要定义时初始化
  2. 最好使用初始化序列(序列顺序与声明顺序相同),而不是在构造函数中赋值
  3. 跨编译单元定义全局对象不能确保初始化顺序
    1. 将static对象放入一个函数
Fuck& fuck()
    static Fuck f;
    return f;

构造/析构/赋值运算

了解C++默默编调用了哪些函数

如果类中没有定义,程序却调用了,编译器会产生一些函数

  1. 一个 default 构造函数
  2. 一个 copy 构造函数
  3. 一个 copy assignment 操作符
  4. 一个析构函数(non virtual)
  • 如果自己构造了带参数的构造函数,编译器不会产生default构造函数
  • base class如果把拷贝构造函数或者赋值操作符设置为private,不会产生这两个函数
  • 含有引用成员变量或者const成员变量不产生赋值操作符
class Fuck
private:
    std::string& str;//引用定义后不能修改绑定对象
    const std::string con_str;//const对象定义后不能修改
;

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

将默认生成的函数声明为private,或者C++ 11新特性"=delete"

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

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

  1. 给多态基类应该主动声明virtual析构函数
  2. 非多态基类,没有virtual函数,不要声明virtual析构函数

别让异常逃离析构函数

构造函数可以抛出异常,析构函数不能抛出异常。

因为析构函数有两个地方可能被调用。一是用户调用,这时抛出异常完全没问题。二是前面有异常抛出,正在清理堆栈,调用析构函数。这时如果再抛出异常,两个异常同时存在,异常处理机制只能terminate().

  1. 构造函数抛出异常,会有内存泄漏吗?
    不会

    try 
    // 第二步,调用构造函数构造对象
    new (p)T;       // placement new: 只调用T的构造函数
    
    catch(...) 
    delete p;     // 释放第一步分配的内存
    throw;          // 重抛异常,通知应用程序
    

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

构造和析构过程中,虚表指针指向的虚表在变化。调用的是对应虚表指针指向的函数。

令operator= 返回一个reference to *this
没什么理由,照着做就行

在operator= 里处理自我赋值

Widget& Widget::operator== (const Widget& rhs)
    if(this == &rhs) return *this
    
    ···

复制对象时务忘其每一个成分

  1. 记得实现拷贝构造函数和赋值操作符的时候,调用base的相关函数
  2. 可以让拷贝构造函数和赋值操作符调用一个共同的函数,例如init

资源管理

以对象管理资源

  1. 为了防止资源泄漏,请使用RAII对象,在构造函数里面获得资源,在析构函数里面释放资源
  2. shared_ptr,unique_lock都是RAII对象

在资源管理类小心copy行为

  • 常见的RAII对象copy行为
    • 禁止copy
    • 引用计数
    • 深度复制
    • 转移资源拥有权

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

用户可能需要原始资源作为参数传入某个接口。有两种方式:

  1. 提供显示调用接口
  2. 提供隐式转换接口(不推荐)

成对使用new和delete要采用相同的格式

new和delete对应;new []和delete []对应

//前面还分配了4个字节代表数组的个数
int *A = new int[10];

//前面分配了8个字节,分别代表对象的个数和Object的大小
Object *O = new Object[10];

以独立的语句将newd对象置入智能指针

调用std::make_shared,而不要调用new,防止new Obeject和传入智能指针的过程产生异常

process(new Widget, priority);

//其实这样也可以,独立的语句
shard_ptr<Widget> p(new Widget);
process(p, priority);

设计与声明

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

  1. 好的接口很容易被正确使用,不容易被误用。努力达成这些性质(例如 explicit关键字)
  2. “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
  3. “防治误用”b包括建立新类型,限制类型上的操作,束缚对象值,以及消除用户的资源管理责任
  4. shared_ptr支持定制deleter,需要灵活使用

设计class犹如设计type

宁以pass-by-refrence-to-const替换pass-by-value

  1. 尽量以pass-by-reference-to-const替换pass-by-value,比较高效,并且可以避免切割问题
  2. 以上规则并不使用内置类型,以及STL迭代器,和函数对象。它们采用pass-by-value更合适(其实采用pass-by-reference-to-const也可以)

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

  1. 不要返回pointer或者reference指向一个on stack对象(被析构)
  2. 不要返回pointer或者reference指向一个on heap对象(需要用户delete,我觉得必要的时候也不是不可以)
  3. 不要返回pointer或者reference指向local static对象,却需要多个这样的对象(static只能有一份)

将成员变量申明为private

  1. 切记将成员变量申明为private
  2. protected并不比public更有封装性(用户可能继承你的base class)

宁以non-member,non-friend替换member

作者说多一个成员函数,就多一分破坏封装性,好像有点道理,但是我们都没有这样遵守。直接写member函数方便一些。

若所有参数都需要类型转换,请为此采用non-member函数

如果调用member函数,就使得第一个参数的类失去一次类型转换的机会。

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

  1. 当std::swap效率不高(std::swap调用拷贝构造函数和赋值操作符,如果是深拷贝,效率不会高),提供一个swap成员函数,并确定不会抛出异常。
class Obj
    Obj(const Obj&)//深拷贝
    Obj& operator= (const Obj&)深拷贝
private:
    OtherClass *p;
;
  1. 如果提供一个member swap,也该提供一个non-member swap用来调用前者
  2. 调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何"命名空间修饰”

    void doSomething(Obj& o1, Obj& o2)
    //这样可以让编译器自己决定调用哪个swap,万一用户没有实现针对Obj的swap,还能调用std::swap
    using std::swap;
    
    swap(o1, o2);
    
  3. 不要往std命名空间里面加东西

实现

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

C语言推荐在函数开始的时候定义所有变量(最开始的C语言编译器要求,现在并不需要),C++推荐在使用对象前才定义对象

尽量少做转型动作

  1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。
  2. 如果转型是必要的,试着将它隐藏于某个函数后。客户可以随时调用该函数,而不需要将转型放入自己的代码。
  3. 使用C++风格的转型。

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

简单说,就是成员函数返回指针或者非const引用不要指向成员变量,这样会破坏封装性

为“异常安全”而努力是值得的

  1. "异常安全函数"承诺即使发生异常也不会有资源泄漏。在这个基础下,它有3个级别
    1. 基本保证:抛出异常,需要用户处理程序状态改变(自己写代码保证这个级别就行了把)
    2. 强烈保证:抛出异常,程序状态恢复到调用前
    3. 不抛异常:内置类型的操作就绝不会抛出异常
  2. "强烈保证"往往可以通过copy-and-swap实现,但是"强烈保证"并非对所有函数都具有实现意义

    //我反正从来没有这样写过
    void doSomething(Object& obj)
    Object new_obj(obj);
    new_obj++;
    swap(obj, new_obj);
    

透彻了解inline函数的里里外外

这里插播一个C++处理定义的重要原则,一处定义原则:

  • 全局变量,静态数据成员,非内联函数和成员函数只能整个程序定义一次
  • 类类型(class,struct,union),内联函数可以每个翻译单元定义一次
    • template类的成员函数或者template函数,定义在头文件中,编译器可以帮忙去重
    • 普通类的template函数,定义在头文件中,需要加inline
  1. inline应该限制在小的,频繁调用的函数上
  2. inline只是给编译器的建议,编译器不一定执行

将文件的编译依存关系降到最低

  1. 支持"编译依存最小化"的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes(impl对象提供服务)和Interface classes。

其实就是使用前置声明,下面有个需要注意的点

//Obj.h
class ObjImpl;
class Obj
public:
private:
    std::shared_ptr<ObjImpl> pObjImpl;
;

//上面的写法会报错,因为编译器会再.h文件里面产生默认的析构函数,
//析构函数要调用ObjImpl的析构函数,然后我们现在只有声明式,不能调用ObjImpl的实现。
//下面的实现才是正确的

//Obj.h
class ObjImpl;
class Obj
public:
    //声明
    ~Obj();
private:
    std::shared_ptr<ObjImpl> pObjImpl;
;

//Obj.cpp
//现在可以看到ObjImpl的实现
#include<ObjImpl>

Obj::~Obj()
    
  1. 对于STL的对象不需要前置声明。

继承与面对对象设计

确定你的public继承塑模出is-a模型

public继承意味着is-a。适用于base class身上的每一个函数也一定适用于derived class。

避免遮掩继承而来的名称

子作用域会遮掩父作用域的名称。一般来讲,我们可以有以下几层作用域

  1. global作用域
  2. namespace作用域
    1. Base class作用域
      1. Drive class作用域
        • 成员函数
          • 控制块作用域
    2. 非成员函数作用域
      • 控制块作用域

注意:遮掩的是上一层作用域的名称,重载(不同参数)的函数也会直接遮掩

class Base
public:
    void f1();


class Drive
public:
    //会遮掩f1(),子类并没有继承f1()
    void f1(int);


Drive d;
d.f1();  //错误
d.f1(3); //正确

可以通过using声明式或者inline转交解决这一问题

class Base
public:
    void f1();


//using 声明式
class Drive
public:
    using Base::f1;
    void f1(int);


//inline转交
class Drive
public:
    void f1()
        Base::f1();
    
    void f1(int);

区分接口继承和实现继承

  1. 纯虚函数:提供接口继承
    1. Drived class必须实现纯虚函数
    2. 不能构造含有纯虚函数的类
    3. 纯虚函数可以有成员变量
    4. 可以给纯虚函数提供定义(wtf)
  2. 虚函数:提供接口继承和默认的实现继承
  3. 非虚函数:提供了接口继承和强制的实现继承(最好不要在Drived class重新定义非虚函数)

考虑virtual函数以外的选择

non-virtual interface:提供非虚接口

class Object
public:
    void Interface()
        ···
        doInterface();
        ···
    
private/protected:
    virtual doInterface()

优点:

  1. 可以在调用虚函数的前后,做一些准备工作(抽出一段重复代码)
  2. 提供良好的ABI兼容性

聊一聊ABI兼容性

我们知道,程序库的优势之一是库版本升级,只要保证借口的一致性,用户不用修改任何代码。

一般一个设计完好的程序库都会提供一份C语言接口,为什么呢,我们来看看C++ ABI有哪些脆弱性。

  1. 虚函数的调用方式,通常是 vptr/vtbl 加偏移量调用
//Object.h
class Object
public:
···
    virtual print()//第3个虚函数
···


//用户代码
int main()
    Object *p = new Object;
    p->print();                    //编译器:vptr[3]()


//如果加了虚函数,用户代码根据偏移量找到的是newfun函数
//Object.h
class Object
public:
···
    virtual newfun()//第3个虚函数
    virtual print()//第4个虚函数
···
  1. name mangling 名字粉碎实现重载

C++没有为name mangling制定标准。例如void fun(int),有的编译器定为fun_int_,有的编译器指定为fun%int%。

因此,C++接口的库要求用户必须和自己使用同样的编译器(这个要求好过分)

  1. 其实C语言接口也不完美

例如struct和class。编译阶段,编译器将struct或class的对象对成员的访问通过偏移量来实现

使用std::fun提供回调

class Object
public:
    void Interface()
        ···
        doInterface();
        ···
    
private/protected:
    std::function<void()> doInterface;

古典策略模式

用另外一个继承体系替代

class Object
public:
    void Interface()
        ···
        p->doInterface();
        ···
    
private/protected:
    BaseInterface *p;



class BaseInterface
public:
    virtual void doInterface()

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

记住就行

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

class Base 
public:
    virtual void print(int a = 1) cout <<"Base "<< a <<endl;;
    int a;
;

class Drive : public Base
public:
    void print(int a = 2)cout << "Drive " << a <<endl;
;                                                                                 
                                                                                   
int main()                                                                        
  Base *b = new Drive;                                                             
  b->print();   //   vptr[0](1)


《Effective Modern C++》读书笔记

Effective C++读书笔记

Effective C++读书笔记

effective C++ 读书精华笔记提取

读书笔记《Effective Java》——创建和销毁对象

java effective 读书笔记