美文网首页
《Effective C++》的做法原理剖析

《Effective C++》的做法原理剖析

作者: 淇漯草 | 来源:发表于2020-11-06 11:53 被阅读0次

    在本篇文章中,会去写一些小实验,以实现Effective C++中提到的一个原则。

    让自己习惯C++

    1.视C++为一个语言联邦

    • C:
      blocks、statements、preprocessor、built-in data types、arrays、pintors
    • Object-Oriented C++:
      classes(构造、析构)
      封装(encapsulation)
      继承(inheritance)
      多态(polymorphism)
      virtual函数(动态绑定)...
    • Template C++:
    • STL:
      容器、迭代器、算法、适配器、仿函数、分配器

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

    使用const常量,可以出现在符号表中(symbol table)
    enum(略
    inline(略

    3.尽可能使用const

    const的目的:获取编译器的帮助
    逻辑上有两种const:bitwise、conceptual。
    编译器实现bitwise、逻辑上使用conceptual。
    使用场景

    • class 外
      global、namespace、static对象
    • class 内
      static、non-static变量
    • 指针
      指针所指物、指针本身

    const成员函数
    针对const对象的函数(针对对象包含的数据)(可重载)
    注:非const可以使用const,const不可以使用非const(兼容性)
    trick:使用mutable跳过限制、利用(static_cast转const与const_cast丢弃const)使用const。

    一个小trick。重载operator * 的时候,带上 const,就可以实现只读,避免修改。比如a*b = c。

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

    初始化、赋值
    初始化:发生在进入构造函数本体之前。
    使用列表初始化
    trick:每一个non-local static搬到自己的专属函数内,这些函数返回一个引用(reference)指向所含的对象。
    原理:保证指向一个经历初始化的对象,还非常便宜(不调用就不会构造和析构)。

    class A{...};
    A& tfs();
    {
      static A tfs;
      return tfs;
    }
    

    其他:可线程产生的不确定性-->启动的时候,全部调用一次,避免多线程下的麻烦。

    补充:默认初始化顺序
    base class、声明顺序。


    构造/析构/赋值运算

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

    《深度搜索C++对象模型》中的构造函数语义学、构造/析构/拷贝语义学有相应的解释。
    内容主要涵盖:默认构造函数做了什么(主要涉及Member Class Object 类成员、base class、virtual function、virtual base class,后两者跟虚指针相关,前两者考虑已存在的构造函数)、拷贝构造函数做了什么(基本类似),默认和拷贝构造函数的应用场景。

    值得注意的点:赋值构造函数
    通常跟拷贝构造函数一模一样,但涉及到引用的时候,const的时候、需要自己去写一个赋值构造函数。

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

    “将成员函数声明为private而且故意不实现它们”
    trick:1.private-->链接期。2.编译期的时候-->>特别写一个基类,基类中使用trick,子类去继承-->。

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

    当class内含至少一个virtual函数,才为它声明virtual析构函数。
    类的设计是为了多态用途,才需要virtual析构函数,否则没有必要。

    pure virtual function
    纯虚函数:纯虚函数导致抽象类不能被实体化。

    没有纯虚函数却想获取一个抽象类。将析构函数定义为纯虚函数。同时提供一份定义。(类的外部是可以定义的(来源于《C++primer》))

    析构函数的调用规则:最深层派生(most derived)的class其析构函数最先被调用,然后是每一个base class的析构函数被调用。

    // 具体情况:析构函数设置为纯虚函数,但外部定义。
    class animal{
    public:
        animal(){}
        void printZ(){
            cout << "zhourongtu" << endl;
        }
    public:
        virtual ~animal() = 0;
    };
    
    animal::~animal(){
        cout << "zhourongtu2";
    }
    
    class bear:public animal
    {
    public:
        ~bear(){
            cout << "bear" << endl;
        }
    public:
        int a;
    };
    int main()
    {
        bear a;
    }
    

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

    原因:两个异常同时存在的情况下,程序若不结束执行就是导致不明确行为。
    异常的处理方式:1.特定处理。2.忽略。3.终止。(一般都是2和3)

    策略:
    1.析构函数自己不吐出异常。析构函数调用某个函数,如果发生异常,要吞下它们。
    2.可以重新设计接口,让用户选择异常时出现的反应。

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

    构造函数如果调用virtual函数,“在base class构造期间,virtual函数不是virtual函数。”

    无论是编译期,还是执行期,都会将对象视为base class类型。

    ## include <iostream>
    ## include <cmath>
    ## include <vector>
    ## include <algorithm>
    ## include <map>
    ## include <stack>
    using namespace std;
    
    class animal{
    public:
        animal(){printZ();}
        virtual void printZ(){cout << "输出Z1" << endl;}
        virtual ~animal(){
            cout << "析构1" << endl;
            printZ();
        }
    public:
        int data;
    };
    class bear:public animal
    {
    public:
        bear(){printZ();}
        void printZ(){cout << "输出Z2" << endl;}
        ~bear(){cout << "析构2" << endl;
            printZ();}
    public:
        int a;
    };
    int main()
    {
        bear a;
    }
    

    以下引用,重点关注 3.

    以下内容来自于《深度搜索C++对象模型》5.5 析构语义学
    1.析构函数的函数本体先被执行。
    2.如果class有member class objects,后者拥有destructors,那么它们会以其声明顺序的相反顺序被调用。

    3.如果object含一个vptr,现在被重新设定,指向适当的base class的virtual table。
    4.如果有任何直接的(上一层)nonvirtual base classes的destructors,按声明顺序的相反顺序被调用。
    5.如果有任何virtual base classes拥有destructor,而目前讨论对这个class是(most-derived)class,那么它们会以其原来的构造顺序的相反顺序被调用。

    即,在构造和析构期间,当处于base class的构造期,与base class的销毁期,都无法使用virtual function的性质。

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

    • 有趣点:x = y = z; 的连续赋值形式。
    classA& operator=(const classA& rhs){
      return *this;
    }
    

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

    错误情况:相同出现删除

    Widget&
    Widget::operator=(const Widget& rhs)
    {
      delete pb;
      pb = new Bitmap(&rhs.pb);
      return *this;
    }
    

    Trick1:传统解决方案:证同

    if(this == &rhs)return *this;
    

    Trick2:新拷贝一个

    Bitmap* pOrigin = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
    

    Trick3(拷贝构造函数):copy and swap。异常安全、自我赋值安全。

    Widget temp(rhs);
    swap(temp);
    return *this;
    

    Trick4:by value传入实参也是可行的(会调用拷贝构造函数),再swap。

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

    • 1.复制所有local成员变量。
    • 2.调用所有base classes内的适当copying函数。

    任何时候都应该承担起“为derived class 编写copying函数。”

    其他:copying函数与copying函数的代码相同部分,应放进第三个函数。

    以下代码阐述了派生类的拷贝构造函数调用基类的拷贝构造函数的具体情况。

    class classB: public classA{
    public:
      ...
    public:
      classB(const classB& rhs):classA(rhs),....
    }
    

    资源管理

    C++中的资源一般来源于动态分配内存,内存泄漏问题。

    常见资源:文件描述符 ( file descriptors )、互斥锁 ( mutex locks )、图形中的字形和笔刷、数据库连接、网络sockets。

    目的:基于对象的资源管理办法,建立在C++对构造函数、析构函数、copying函数的基础上。严守一定的规则,可以消除资源管理问题。

    13.以对象管理资源

    提示1:书中所提智能指针只是以对象管理资源的例子,主要阐述的是思想。

    提示2:书中所提auto_ptr已被废弃。思想值得保留。智能指针相关可参考“不同智能指针的语义学含义(待补充)”
    以下将阐述智能指针的关键想法,同时常见两个RAII类是shared_ptr与unique_ptr(修正后)

    原因:资源放进对象内,依赖C++的“析构函数自动调用机制”确保资源释放。
    智能指针

    • 想法一、获得资源后立即放入管理对象。RAII(Resource Acquisition Is Initialization),获取一笔资源后于同一语句内以它初始化某个管理对象。
    • 想法二、管理对象运用析构函数确保资源被释放。
    • RAII对象:以对象管理资源的观念被认为RAII。

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

    面对复制怎么办?(管理锁的RAII对象)

    • 1.禁止复制-->条款6。
    • 2.对底层资源祭处“引用计数法”。
    • 3.复制底部资源。
    • 4.转移所有权

    书中例子

    • 1.书中的例子是Lock中使用指针,通过构造和析构函数管理加锁和释放锁。
    • 2.为了解决引用计数的问题-->Lock中用智能指针,利用智能指针管理。删除器。(但是只提供了初始化的方案、没有提供内容被处理的情况)

    个人认为:书中例子运用了shared_ptr。但是没有用过shared_ptr去利用引用计数相关的东西,仅使用了其unique_ptr的性质。

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

    APIs往往要求访问原始资源,所以RAII Class应该提供访问原始资源的方法

    • 提供访问的方法有显式,如智能指针的get(),或者隐式,如利用
    operator FontHandle() const{return f;}//使用传值是因为API正好要求传值。
    

    但隐式会产生问题-->FontHandle f2 = f1; // 误用性质(条款18) 个人认为暂时不用在意

    16.成对使用new和delete的时候要采取相同形式。

    单一对象和对象数组的抽象模型
    | Object|
    | n | object | object |...

    建议:尽量不要随便对数组使用typedef。
    记得new和delete对称,即数组行为对称,单一对象行为对称。

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

    这里的独立性是为了保证:new 对象利用该对象建立智能指针是相连的。避免在new完对象以后,出现其他异常,导致泄漏。

    设计和声明

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

    根据书中说明,该准则设立舞台
    其他准则对付大范围题目:正确性、高效性、封装性、维护性、延展性,协议的一致性。

    • 1.客户以错误的次序调用你的函数。
      使用了类型系统(type system)。
      利用 外覆类型(wrapper types)
    class Date{
    public:
      Date(Month month, Day day, Year year);
      ...
    };
    struct Day{
    explicit Day(int d):val(d){}
    int val;
    };
    ...
    

    提示1:explicit旨在使用构造函数时,避免隐式转换。

    • 2.用函数替换对象,表现某个特定月份。
    class Month{
    public:
      static Month Jan(){return Month(1);}
    ...
    private:// 避免生成新的月份
      explicit Month(int m);
      ...
    };
    

    提示2:促进正确使用的办法包括接口的一致性,以及内置类型的行为兼容。

    提示3:shared_ptr的定制删除器,可以实现某种意义上的跨平台功能(如对象在动态链接库创建,另一个动态链接库销毁)

    19.设计class犹如设计type

    本条建议看原书。
    1. 新type的对象应该如何被创建和销毁呢?
    构造、析构、内存分配和释放
    2. 对象的初始化和对象的赋值该有什么样的差别?
    初始化和赋值是不是不同的?不同在什么地方?
    3. 新type的对象如果被passed by value(以值传递),意味着什么?
    调用函数时,会调用拷贝构造函数实现传值。
    关于构造函数,我有一篇文章做了一些小小的实验。
    构造函数的使用位置与特点

    4. 什么是新type的“合法值”?
    成员变量的数值集要求。可能决定了构造函数等,以及异常函数等的检查。
    5. 你的新type需要配合某个继承图系(inheritance graph)吗?

    1. 你的新type需要什么样子的转换?
      允许T1隐式转换成T2-->在T1中写一个类型转换函数 operator T2.
      T2中写一个non-explicit-one-argument的构造函数。
    FontHandle get() const {return f;} // 显式转换函数
    operator FontHandle() const {return f;} // 隐式转换函数
    
    1. 什么样的操作符和函数对此新type而言是合理的?
      成员函数的思考。
    2. 什么样的标准函数应该被驳回?
      不应该被调用的函数,private。
    3. 谁该取用新type的成员?
      封装性。
    4. 什么是新type的“未声明接口”
      保证一些有趣的东西。
    5. 你的新type有多么的一般化?
      一般化到什么程度??template?
    6. 你真的需要一个新type吗?

    20.宁以pass_by-reference-to-const 替换 pass-by-value

      1. 效率问题。
      1. 对象切割问题
        这一点在《深度搜索C++对象模型》中的内存模型有相关描述。
        特点:所有的特化信息都会被清除。vptr只会以base class的构造函数进行构造,将不会拥有derived的虚函数表。

    提示1:内置类型一般都是pass by value,对待内置类型的态度总会有所不同。这是由条款1所决定的,规则的改变取决于使用C++的哪一部分。C++作为一个语言联邦而存在。

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

    • 任何一个reference,指向一个local对象时,将一败涂地。
      这是由于local对象是在stack中存储的。内存分布相关知识。
    • 在函数的内部进行new,之后返回引用。是一个糟糕的行为。
    const Rational& operator(const Rational &lhs, const Rational &rhs){
      Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
      return *result;
    }
    Rational w, x, y, z;
    w = x*y*z;//导致内存泄漏
    

    提示1:本人在这一点中,常常没有意识到,因为在写小demo的时候总是会自己在外侧进行delete,所以一直没有意识到这个问题。

    • 内部使用static也是个坏主意。

    提示2:书中提到,我们挑出行为正确的那一个,优化可以给编译器厂商去做。

    22.将成员变量声明为private

    • 1.把一些事情隐藏在客户使用之外,为客户提供响应速度的优先。(计算平均速度)
    • 2.避免修改public变量时,会导致大量相关代码的修改。

    protected 并不比public更具有封装性。影响derived class

    23.宁以non-member、non-friend替换member函数

    提示1:这是一条违反直觉的条款。原因在于,究竟什么才是封装性?

    • 封装性:如果某些东西被封装,它就不再可见。
      封装意味着更少人可以看到它,越少人看到它,就有越大的弹性去改变它。
      它使我们改变事物而只影响有限的客户。

    提示2:namespace与class的不同,允许一种强大的扩展机能。namespace可以跨越多个源码文件,而后不能。

    提示3:non-member non-frind 替换member函数,增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

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

    总结部分:如果你需要为某个函数的所有参数(包括this指针所指的哪个隐喻参数(这个参数不是隐式转换的合格参与者))进行类型转换,那么这个函数必须是一个non-member。

      const Rational operator*(const Rational &lhs, const Rational &rhs)const;
    

    返回const by-value 接受一个 reference-to-const 实参。
    适合:轻松自在的相乘。

    class Rational{
    public:
      Rational(int numerator = 0, int denominator = 1);//支持隐式转换
      int numerator() const;
      int denominator() const;
    };
    

    提示1:隐式转换是一个很有趣的东西。请万分注意,并搞清楚如何进行隐式转换。

    最初给出的函数原型不支持混合式运算
    a * 2可行而2 * a不可行。
    可以设计一个operator * 做相关实现。

    1.operator * (int, const Rational&);或者两个都在外面。
    2.operator * (const Rational &, const Rational &);
    

    只有当参数被列于参数列(parameter list)内,这个参数才是隐式转换的合格参与者。

    意思:this对象,不是隐式转换的合格参与者。

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

    • 1.std::swap的功能。
    • 2.class提供swap。
    • 3.std::swap实现偏特化,调用内部swap。
    • 4.std不可纂改,实现non-member调用成员swap。
    • 5.template class的偏特化函数-->利用函数重载,调用swap。

    提示:书上写的比较杂乱,但主要还是以上5点。尤其是std::swap的偏特化部分,写的非常冗余,既强调了std::swap偏特化,又提到std空间不可用。最终的核心在于1、2、4、5.

    写了一个例子,针对特定class写的版本

    #include <iostream>
    #include <cmath>
    #include <vector>
    #include <algorithm>
    #include <map>
    #include <stack>
    namespace ZRT{
        class MyWidget
        {
        public:
            MyWidget(int val, std::string data):pval(new int(val)), data(data){}
            MyWidget(const MyWidget &rhs){
                pval = new int(*rhs.pval);
                data = rhs.data;
            }
            void swap(const MyWidget &rhs){
                pval = new int(*rhs.pval);
                data = rhs.data;
            }
        private:
            int *pval;
            std::string data;
        };
    
        // void swap(MyWidget &a, MyWidget &b){
        //     std::cout << "ZRT特别版本" << std::endl;
        //     a.swap(b);
        // }
    }
    
    using namespace ZRT;
    int main()
    {
        using std::swap;
        MyWidget a(5, "ZHOU");
        MyWidget b(6, "Shi");
        swap(a, b);
        std::cout << "正常版本";
    }
    

    注释去掉,则是我的特别版本(输出特别+正常)。否则就是正常版本(只有正常)。

    实现

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

    核心:尽可能使用前再使用

    循环中,尽可能使用方案B(除非1.明确赋值成本<构造+析构。2.效率高敏感。

    27.尽量少做转型动作

    四种转型的介绍
    const_cast :

    类型 简介 分析
    const_cast 常量性移除(cast away the constness)
    dynamic_cast 安全向下转型(safe downcasting) 成本考虑,原因:
    reinterpret_cast 低级转型 取决于编译器,C-style
    static_cast 强迫隐式转换(implicit conversions) 注意一个小误解

    对this指针转型的一个误解

    #include <iostream>
    #include <cmath>
    #include <vector>
    using namespace std;
    class Window{
    public:
        Window():width(0), height(0){}
        explicit Window(int a, int b):width(a), height(b){}
        virtual void printSize(){
            cout << "width:" << width << "  height:" << height << endl;
            width = width+1; height = height + 1;
            return ;
        }
        void setWidth(int a){width = a;
        }
        void setHeight(int b){
            height = b;}
    public:
        int width;
        int height;
    };
    
    class SpecialWindow:public Window
    {
    public:
        explicit SpecialWindow(int a, int b){
            setWidth(a);
            setHeight(b);
        } 
        virtual void printSize(){
            static_cast<Window>(*this).printSize();
            return;
        }
    };
    
    int main()
    {
        Window window(3, 5);
        cout << "window(3, 5)的输出结果如下,输出后再+1:";
        window.printSize(); cout << endl;
        cout << "再一次,输出结果如下,输出后再+1:";
        window.printSize(); cout << endl;
        SpecialWindow sp_window(3, 5);
        cout << "sp_window(3, 5)的输出结果如下(和上面相同,但数据的修改发生在副本上+1):";
        sp_window.printSize(); cout << endl;
        cout << "再一次,输出结果如下,修改在副本上,所以没有改动:";
        sp_window.printSize();
        return 0;
    }
    
    image.png

    该对象在进行static_cast<Window>时,利用SpetialWindon中Window(即base成分)的副本,进行操作,当出现修改时,不影响对象结果

    建议

    Window::printSize();
    

    关于dynamic
    使用情况:手头有base指针或reference,想认定的derived class执行derived function。

    手段:
    1.使用容器直接存储指向derived class,不通过base class。(不用dynamic_cast了)
    2.使用virtual函数往继承方向上移动(基类缺省,但是尽量不要)

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

    问题:1.被修改(理论上是const,但引用是可修改的-->常量引用)。2.造成悬空(数据被销毁)。

    相关文章

      网友评论

          本文标题:《Effective C++》的做法原理剖析

          本文链接:https://www.haomeiwen.com/subject/lwhfyktx.html