zoukankan      html  css  js  c++  java
  • 《Effective C++》——读书笔记

    让自己习惯C++

    视C++为一个语言联邦

    • C语言
    • 面对对象
    • C++模板
    • 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成员可以调用所有成员函数
         
        尽量使用const来修饰函数名和参数变量名

    尽量使用const来修饰类名
    void function(classA test); //classA为自定义的类型

    这样使用值传参的缺点:

    • 会导致自定义类型的构造函数和析构函数多次被调用,当自定义类型的构造函数和析构函数比较费时的时候,效率比较底下。
    • 在传递派生类时容易产生对象被切割的问题。
       
      建议:
      void function(const classA &s);

    优点:

    • 可以回避自定义类型的构造和析构函数的调用,不会影响效率。
    • 不会产生对象切割问题,因为引用的底层其实就是指针,在内存中只有一份实例。

    原则:

    • 如果可以的话,尽量传递 const 的引用作为函数的参数。
    • 不要将这个条款应用在内置类型上,对于内置类型(int double),pass-by-value更适合。

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

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

    将static对象放入一个函数
    Fuck& fuck(){
    static Fuck f;
    return f;
    }

    构造/析构/赋值运算

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

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

    • 一个 default 构造函数

    • 一个 copy 构造函数

    • 一个 copy assignment 操作符

    • 一个析构函数(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().

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

    不会

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

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

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

    令operator= 返回一个reference to *this

    int x,y,z;
    x=y=z=15;同样有趣的是,赋值采用右结合律,
    x=(y=(z=15));
    Widget&operator=(const Widget&rhs)//返回类型是个reference,
    {return*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行为
      1. 禁止copy
      2. 引用计数
      3. 深度复制
      4. 转移资源拥有权

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

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

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

    成对使用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,需要灵活使用
    • 宁以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也可以)
      对象以by value的方式传递,其实际意义是由该对象的copy constructor决定的。这可能会使pass-by-value成为成本很高的动作。
      以pass by reference,还可以避免所谓的“切割slicing问题”,又称为“upcasting问题”。详见《Thingking in C++》P629
      pass by reference是一件美妙的事情,但会导致某些复杂性。最知名的问题就是aliasing(别名问题),见条款17。某些情况下必须pass by value。references的底层几乎都是指针完成,所以passing by reference通常意味着传递的是指针。如果有个小对象,例如一个int,那么pass by value可能比pass by reference的效率更高一些。
    • 必须返回对象时,别妄想返回其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命名空间里面加东西

    在constructor中尽量以initialization动作取代assignment动作

    【尽量使用初始化列表来代替赋值】
    const members和reference members只能被初始化,不能够被赋值(assigned)。这个时候,如果在构造函数中要对其初始化时必须用member initialization list。 另外,从效率方面考虑,也建议用以initialization动作取代assignment动作。
    例外的情况:

    1. static class member不应该在构造函数中初始化。
    2. 如果有很多built-in type类型的class member要初始化,建议使用assignment,这和initialization在效率上没有什么区别,而且容易维护。
    3. initialization list中的members初始化次序应该和其在class内的声明次序相同

    class members系以它们在class内的声明次序来初始化,和它们在member initialization list中出现的次序完全无关。基类的成员变量永远在继承类成员变量之前被初始化,所以如果运用了继承,你应该在member intialization lists起始处列出base class的初始设定值。

    结论是:对象被初始化时,如果你希望确实掌握真正发生了什么事,请以class内的members声明次序,将各个memebers列于initialization list中。

    区分member functions,non-member functions和friend functions三者

    member functions和non-member functions的区别是:member functions可以是虚函数,而non-member functions不可以。
    在一个类中,只要能够避免friend函数,就应该尽量避免,“因为就像真实世界一样,朋友带来的麻烦常常多于其价值。”:)_

    1. 虚拟函数必须是class members。
    2. 绝不要让operator>>和operator<<成为members。
    3. 如果non-member functions需要用到class的non-public members,让它成为class的friend functions。

    实现

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

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

    尽量少做转型动作

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

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

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

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

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

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

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

    • 全局变量,静态数据成员,非内联函数和成员函数只能整个程序定义一次

    • 类类型(class,struct,union),内联函数可以每个翻译单元定义一次

      1. template类的成员函数或者template函数,定义在头文件中,编译器可以帮忙去重
      2. 普通类的template函数,定义在头文件中,需要加inline
    • inline应该限制在小的,频繁调用的函数上

    • 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(){
        
    }
    

    对于STL的对象不需要前置声明。

    继承与面对对象设计

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

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

    避免遮掩继承而来的名称

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

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

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

    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重新定义非虚函数)
       
      在编写自己的 class 时,你应该明白提供下面 3 种类型函数的理由
    • pure virtual function
    • virtual function
    • no-virtual function
       
      何时提供 pure virtual function?

    要求派生类只继承接口时,提供纯虚函数。【就像下单函数一样,其作用就是提供接口让你来重写】

    class MyType
    {
    public:
    //派生类只继承接口
    vitrual void fun() const = 0;
    };
     
    何时提供 virtual function?
    要求派生类只继承接口和缺省实现时,提供虚函数。
    class MyType
    {
    public:
    //派生类只继承接口
    vitrual void fun() const 
    {
    }
    };
    

    何时提供 no-virtual function?

    要求派生类只继承接口的强制实现时,提供非虚函数。

    class MyType
    {
    public:
    //派生类只继承接口
    void fun() const
    {
    }
    };
    

    原则

    • 纯虚函数指定接口继承。//这跟XTP中的订阅反馈函数的重载一样
    • 虚函数指定接口和缺省实现继承。 //
    • 非虚函数指定接口的强制实现继承。
    • 接口继承和实现继承不同。

    考虑virtual函数以外的选择

    non-virtual interface:提供非虚接口
    class Object{
    public:
        void Interface(){
            ···
            doInterface();
            ···
        }
    private/protected:
        virtual doInterface(){}
    }
    

    优点:

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

    聊一聊ABI兼容性

    我们知道,程序库的优势之一是库版本升级,只要保证借口的一致性,用户不用修改任何代码。
    一般一个设计完好的程序库都会提供一份C语言接口,为什么呢,我们来看看C++ ABI有哪些脆弱性。

    • 虚函数的调用方式,通常是 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个虚函数
    ···
    }
    
    
    • name mangling 名字粉碎实现重载

    C++没有为name mangling制定标准。例如void fun(int),有的编译器定为fun_int_,有的编译器指定为fun%int%。
    因此,C++接口的库要求用户必须和自己使用同样的编译器(这个要求好过分)

    • 其实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
    
    • 缺省参数值是静态绑定
    • 虚函数是动态绑定
    • 遵守这条规定防止出错

    通过复合塑模出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个字节
    classObject : 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. 继承和模板都支持接口和多态
    4. 对继承而言,接口是显式的,以函数为中心,多态发生在运行期;
    5. 对模板而言,接口是隐式的,多态表现在template具象化和函数重载
    //这里接口要求T必须实现operator >
    template<typename T>
    T max(T a, T b){
        return (a > b) ? a : b;
    }
    

    了解typename的双重意义

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

    第一层:作为类模板的参数时,与 class 功能相同。

    template<class T> class MyType;
    template<typename T> class MyTYpe;
    

    这两个定义完全相同。
     
    第二层: typename 可以让模板里面定义嵌套从属名称的类型变成有效的类型,因为 C++ 的解析器在模板中遇到嵌套从属类型时,默认认为它是无效的类型。

    例如:无效的嵌套从属类型

    template<typename T>
    void fun(const T& t)
    {
            T::const_iterator iter(t.begin());
    }
    

    我们需要认为指定它为有效的嵌套从属类型

    template<typename T>
    void fun(const T& t)
    {
          typename T::const_iterator iter(t.begin());
    }
    

    一般情况 当你想在 template 中指定一个有效的嵌套从属类型名称,只需要在嵌套从属类型前面加上 typename 关键字即可。
    例外

    * 不得在 base class list 中使用 typename
    /* 错误用法 */
    class Deriver : public typename Base<T>::MyType;
    * 不得在 member initialization list 中使用 typename
    class Deriver : public Base<T>::MyType
    {
    public:
      /* 错误用法 */
      explicit Deriver(int x) : typename Base<T>::MyType(x)
      {
      }
    };
    

    原则

    • class 和 typename 在声明模板参数时作用相同。

    • 使用 typename 标识嵌套从属类型,但是不得在 base class list 和 member initialization list 中使用。

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

    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;
    }
    
    • 使用成员函数模版生成“可接受所有兼容类型”的函数
    • 即使有了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和赋值操作符
    • 在一个类模版内,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;
    }
    
    • traits采用类模版和特化的方式,为不同的类型提供了相同的类型抽象(都由size)
    • 为某些类型提供编译期测试,例如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;
    };
    
    • C++元编程可以将计算转移到编译期,执行速度迅速(缺陷?)

    函数调用的栈结构:

    当编译器为函数调用生成代码时,首先将参数从右至左压栈,然后是函数返回的地址(Return Address)压栈,同时在函数内部,生成代码来将堆栈指针移动(向上或向下,这要视机器而定),为函数的本地变量提供存储空间。当函数调用完毕,栈指针将移动到函数(Return Address)的位置,这样函数的本地变量出栈。那么函数的返回值(尤其是一个自定义的类型)存放在什么地方?答案是将函数的返回值作为一个参数压栈,直接将返回值的信息拷贝至该参数中。这个答案没有解决所有的问题,但它效率很高。

    下面是一个函数调用的例子:

    int f(int x,char c);
    int g=f(a,b);
    看一下它对应的汇编代码:
    push b;
    push a;
    call f();
    add sp,4;
    mov g,register a;
    

    先是两个参数压栈,然后调用函数,完了将参数出栈,将返回值放在寄存器中(因为int是built-in type),传递给返回值g。这与上面讲的函数调用的过程稍有不同。插一句:前段时间碰到很多次stack overflow的错误,搞死我了。但是当我理解了函数调用背后的故事后,stack overflow的问题终于暂时解决了。

    何为copy constructor?

    当需要从一个已存在的对象创建另一个对象时,会调用copy constructor。当然,我们也可以阻止这样的行为。忠告中会讲到。
    看下面的例子:

    class String
    { public:
    String(const char *str=NULL);
    String(const String &other); //copy constructor
    virtual ~String();
    String & operator=(const String &other);//assignment operator
    private:
    char *m_data;
    }
    

    如果缺少了copy constructor和assignment operator,当进行复制时会进行bitcopy,也就是按位进行拷贝。试想如果上面的类中没有copy constructor和assignment operator,调用如下语句时的问题:

    String a(“hello”);//m_data指向字符串“hello”
    String b(“World”);//m_data指向字符串“world”
    b=a;
    

    这样,经过bitcopy,a和b中的m_data都指向“hello”,“world”没人管了,Memory Leak!!!而且,当a或b中的一个调用了析构函数后,“hello”所在的内存将被释放,这样另一个中的指针指向了一片非法内存!!!

    忠告:如果class的成员变量中含有任何指针,请为这个类写copy constructor和assignment operator。但是你如果确信你的class不会执行copy和assignment动作,这时候写copy constructor和assignment operator会觉得有点得不偿失,这时候将copy-construction(No definition)声明为private,将阻止使用值传递方式(pass an object of your class by value)。呵呵,我知道这个时候你的头在游泳了。

    定制new和delete

    了解new-handler的行为

    new和malloc对比:

    • new构造对象,malloc不会
    • new分配不出内存会抛异常,malloc返回NULL
    • 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++中对象的构造和析构经历了都两个阶段

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

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

    编写符合常规的new和delete

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

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

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

    我觉得这个条款讲的不是太通俗,所以我决定来个“俗”点的:
    重载new和delete时必须要做到的,这里的重载包括(参见《Thinking in C++》):

    Overloading global new & delete;
    Overloading new & delete fro a class;
    Overloading new & delete for arrays。
    

    不过现在有了这练习上乘内功的口诀,就不怕走火入魔了。

    口诀:

    1. 正确的返回值
    2. 内存不足,调用错误处理函数
    3. 不索求任何内存时的调用
    4. 避免不经意遮掩了“正常”形式的new(见条款9)

    问题:当重载了new后,我们有时候需要调用“正规形式”的new,这时怎么办?

    解决:

    1. 祭出inline函数,搞定!
    class X{
    public:
    void f();
    static void * operator new(size_t size, new_handler p);//我们重载,为了区别,则多添加一个参数
    static void * operator new(size_t size)//这是系统自己的
    { return ::operator new(size);}
    }
    
    1. 为自己重载new添加的额外参数添加默认参数值,一样搞定!
    class X{
    public:
    void f();
    static void * operator new(size_t size, new_handler p=0);
    }
    

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

    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
    
    • 缺省参数值是静态绑定
    • 虚函数是动态绑定
    • 遵守这条规定防止出错

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

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

    
    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;
    }
    
    • 使用成员函数模版生成“可接受所有兼容类型”的函数
    • 即使有了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和赋值操作符
    • 在一个类模版内,template名称可被用来作为作为“template和其参数”的简略表达式

    模版元编程

    本质上就是函数式编程

    //上楼梯,每次上一步或者两步,有多少种
    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;
    };
    

    C++元编程可以将计算转移到编译期,执行速度迅速(缺陷?)

    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;
    }    
    
  • 相关阅读:
    洛谷 P2827 蚯蚓(NOIp 提高组2016)
    洛谷 P2822 组合数问题(NOIp 提高组2016)
    洛谷 P2671 求和
    洛谷 P1119 灾后重建
    电子海图开发第二十二篇 web电子海图 历史航迹的展示(共一百篇)
    电子海图开发第二十一篇 web电子海图 监听图标点击事件(共一百篇)
    电子海图开发第二十篇 web电子海图 在电子海图上加载图标(共一百篇)
    物联网时代存储告急 边缘存储如何缓解存储压力?
    国家超算深圳中心计划2年内提升计算能力至少1000倍;图神经网络的生成式预训练论文解读
    轻量型 GPU 应用首选 京东智联云推出 NVIDIA vGPU 实例
  • 原文地址:https://www.cnblogs.com/zzw1024/p/12130957.html
Copyright © 2011-2022 走看看