zoukankan      html  css  js  c++  java
  • 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);
    }
    
    1. 不要往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)
    }
    
    //Drive 1
    
    
    1. 缺省参数值是静态绑定
    2. 虚函数是动态绑定
    3. 遵守这条规定防止出错

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

    1. 复合的意义和public完全不一样
    2. 根据某物实现出和is-a的区别:

    这个也是什么时候使用继承,什么时候使用复合。复合代表使用了这个对象的某些方法,但是却不想它的接口入侵。

    明智而审慎地使用private继承

    1. private继承是”根据某物实现出“
    2. 唯一一个使用private继承的理由就是,可以使用空白基类优化技术,节约内存空间

    C++对空类的处理

    C++ 设计者在设计这门语言要求所有的对象必须要有不同的地址(C语言没有这个要求)。C++编译器的实现方式是给让空类占据一个字节。

    class Base{
    public:
        void fun(){}
    }
    
    //8个字节
    class Object{
    private:
        int a;
        Base b;
    };
    
    //4个字节
    class Object : private Base{
    private:
        int a;
    }
    

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

    首先我们来了解一下多重继承的内存布局。

    //包含A对象
    class A{
        
    };
    //包含A,B对象
    class B:public A{
        
    };
    //包含A,C对象
    class C:public A{
        
    };
    //包含A,A,B,C,D对象
    class D:public B, public C{
        
    }
    

    由于菱形继承,基类被构造了两次。其实,C++也提供了针对菱形继承的解决方案的

    //包含A对象
    class A{
        
    };
    //包含A,B对象
    class B:virtual public A{
        
    };
    //包含A,C对象
    class C:virtual public A{
        
    };
    //包含A,B,C,D对象
    class D:public B, public C{
        
    }
    

    使用虚继承,B,C对象里面会产生一个指针指向唯一一份A对象。这样付出的代价是必须再运行期根据这个指针的偏移量寻找A对象。

    多重继承唯一的那么一点点用就是一个Base class提供public继承,另一个Base class提供private继承。(还是没什么用啊,干嘛不适用复合)

    模板与泛型编程

    了解隐式接口和编译期多态

    • 接口:强制用户实现某些函数
    • 多态:相同的函数名,却有不同的实现
    1. 继承和模板都支持接口和多态
    2. 对继承而言,接口是显式的,以函数为中心,多态发生在运行期;
    3. 对模板而言,接口是隐式的,多态表现在template具象化和函数重载
    //这里接口要求T必须实现operator >
    template<typename T>
    T max(T a, T b){
        return (a > b) ? a : b;
    }
    
    

    了解typename的双重意义

    1. 声明template参数时,前缀关键字class和typename可以互换
    2. 使用typename表明嵌套类型(防止产生歧义)

    学习处理模板化基类内的名称

    template <typename T>
    class Base{                                                                      
      public:                                                                          
        void print(T a) {cout <<"Base "<< a <<endl;};                                  
      };
    
    template<typename T>                                                             
    class Drive : public Base<T>{                                                    
    public:                                                                          
      void printf(T a){                                                          
      
      //error 编译器不知道基类有print函数
        print(a);  
      } 
    };
    
    //解决方案
    //this->print();
    //using Base<T>::print
    //base<T>::print直接调用
    

    将参数无关代码抽离template

    1. 非类型模板参数造成的代码膨胀:以函数参数或者成员变量替换
    2. 类型模板参数造成的代码膨胀:特化它们,让含义相近的类型模板参数使用同一份底层代码。例如int,long, const int

    运用成员函数模版接收所有兼容类型

    我们来考虑一下智能指针的拷贝构造函数和赋值操作符怎么实现。它需要子类的智能指针能够隐式转型为父类智能指针

    template<typename T>
    class shared_ptr{
    public:
        //拷贝构造函数,接受所有能够从U*隐式转换到T*的参数
        template<typename U>
        shared_ptr(shared_ptr<U> const &rh):p(rh.get()){
            ...
        }
        //赋值操作符,接受所有能够从U*隐式转换到T*的参数
        template<typename U>
        shared_ptr& operator= (shared_ptr<U> const &rh):p(rh.get()){
            ...
        }
        
        //声明正常的拷贝构造函数
        shared_ptr(shared_ptr const &rh);
        shared_ptr& operator= (shared_ptr const &rh);
    private:
        T *p;
    }
    
    1. 使用成员函数模版生成“可接受所有兼容类型”的函数
    2. 即使有了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和赋值操作符
    3. 在一个类模版内,template名称可被用来作为作为“template和其参数”的简略表达式

    所有参数需要类型转换的时候请为模版定义非成员函数

    1. 当我们编写一个模版类,某个相关函数都需要类型转换,需要把这个函数定义为非成员函数
    2. 但是模版的类型推到遇见了问题,需要把这个函数声明为友元函数帮助推导
    3. 模版函数只有声明编译器不会帮忙具现化,所以我们需要实现的是友元模版函数
    template <class T>
    class Rational
    {
        …
        friend Rational operator* (const Rational& a, const Rational& b)
        {
            return Rational (a.GetNumerator() * b.GetNumerator(),
                a.GetDenominator() * b.GetDenominator());
        }
        …
    }
    

    请使用traits classes表现类型信息

    template<typename T>
    class type_traits;
    
    template<>
    class type_traits<int>{
    public:
        static int size = 4;
    }
    
    template<>
    class type_traits<char>{
    public:
        static int size = 1;
    }
    
    template<>
    class type_traits<double>{
        static int size = 8;
    }
    
    template<typename T>
    int ccSizeof(T){
        return type_traits<T>::size;
    }
    
    1. traits采用类模版和特化的方式,为不同的类型提供了相同的类型抽象(都由size)
    2. 为某些类型提供编译期测试,例如is_fundamental(是否为内置类型)

    模版元编程

    本质上就是函数式编程

    //上楼梯,每次上一步或者两步,有多少种
    int climb(int n){
        if(n == 1)
            return 1;
        if(n == 2)
            return 2;
        return climb(n - 1) + climb(n - 2);
    }
    
    //元编程,采用类模版
    template<int N>
    class Climb{
    public:
      const static int n = Climb<N-1>::n + Climb<N-2>::n;
    };
    
    template<>
    class Climb<2>{
    public:
      const static int n = 2;
    };
    
    template<>
    class Climb<1>{
    public:
      const static int n = 1;
    };
    
    1. C++元编程可以将计算转移到编译期,执行速度迅速(缺陷?)

    定制new和delete

    了解new-handler的行为

    new和malloc对比:

    1. new构造对象,malloc不会
    2. new分配不出内存会抛异常,malloc返回NULL
    3. new分配不出内存可以调用用户设置的new-handler,malloc没有
    namespace std{
        typedef void (*new_handler)();
        //返回旧的handler
        new_handler set_new_handler(new_handler p) throw();
    }
    
    • 可以为每个类设置专属new handler

    了解new和delete合理的替换时机

    C++中对象的构造和析构经历了都两个阶段

    1. operator new, operator delete:分配和释放内存
    2. 调用构造函数,调用析构函数

    替换new和delete的理由,就是需要收集分配内存的资源信息

    编写符合常规的new和delete

    1. operator new应该内含一个无穷循环尝试分配内存,如果无法满足,就调用new-handler。class版本要处理“比正确大小更大的(错误)申请”
    2. operator deleter应该处理Null。classz专属版本还要处理“比正确大小更小的(错误)申请”

    写了operator new也要写相应的operator delete

    我们知道,new一个对象要经历两步。如果在调用构造函数失败,编译器会寻找一个“带相同额外参数”的operator delete,否则就不调用,造成资源泄漏

    STL使用小细节

    为不同的容器选择不同删除方式

    删除连续容器(vector,deque,string)的元素

    // 当c是vector、string,删除value
    c.erase(remove(c.begin(), c.end(), value), c.end());
    
    // 判断value是否满足某个条件,删除
    bool assertFun(valuetype);
    c.erase(remove_if(c.begin(), c.end(), assertFun), c.end());
    
    // 有时候我们不得不遍历去完成,并删除
    for(auto it = c.begin(); it != c.end(); ){
        if(assertFun(*it)){
            ···
            it = c.erase(it);
        }
        else
            ++it;
    }
    

    删除list中某个元素

    c.remove(value);
    
    // 判断value是否满足某个条件,删除    
    c.remove(assertFun);
    

    删除关联容器(set,map)中某个元素

    c.erase(value)
        
    for(auto it = c.begin(); it != c.end(); ){
        if(assertFun(*it)){
            ···
            c.erase(it++);
        }
        else
            ++it;
    }    
    
  • 相关阅读:
    导包路径
    django导入环境变量 Please specify Django project root directory
    替换django的user模型,mysql迁移表报错 django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependen cy user.0001_initial on database 'default'.
    解决Chrome调试(debugger)
    check the manual that corresponds to your MySQL server version for the right syntax to use near 'order) values ('徐小波','XuXiaoB','男','1',' at line 1")
    MySQL命令(其三)
    MySQL操作命令(其二)
    MySQL命令(其一)
    [POJ2559]Largest Rectangle in a Histogram (栈)
    [HDU4864]Task (贪心)
  • 原文地址:https://www.cnblogs.com/biterror/p/6909577.html
Copyright © 2011-2022 走看看