More Effective C++35条款速记版

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/davidsher_zhou/article/details/81104267
  1. 仔细区别pointers和references——1、reference必须代表某个对象,没有所谓null reference,因此必须有初值。2、使用reference 可能会比使用pointers更富效率,因为使用reference之前不需要测试其有效性。3、pointers可以被重新赋值,指向另一个对象,reference 却总是指向(代表)它最初获得的那个对象。
    string s1("Nancy");
    string s2("Clancy");
    
    string& rs = s1;    //rs代表s1
    string *ps = &s1;    //ps指向s1
    rs = s2;              //rs任然代表s1,但是s1的值现在变成了“Clancy”
    ps = &s2;             //ps现在指向s2,s1没有变化。
    一般而言,
    使用引用:1、确定“总是会代表某个对象”,而且“一旦代表了该对象就不能够再改变”。2、当实现某些操作符时候,例如:operator[] ,必须返回某种“能够被当作assignment赋值对象”的东西:引用。3、当实现一个操作符而其语法需求无法由pointers 达成,就选择reference,其他任何时候,清采用pointers。
    使用指针:1、需要考虑“不指向任何对象”的可能性,或考虑“在不同时间指向不同对象”的能力。

    不同点
    1、指针是一个变量,引用是别名。
    2、对引用求地址,就是对目标变量求地址。即引用名是目标变量名的一个别名。引用在定义上是说引用不占据任何内存空间,但是编译器在一般将其实现为const指针,即指向位置不可变的指针,所以引用实际上与一般指针同样占用内存。。3、指针有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)。
    4、“sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小。
    5、指针和引用的自增(++)运算意义不一样;
    6、引用常见的使用用途:作为函数的参数、函数的返回值。
  2. 最好使用C++转型操作符——static_cast, const_cast, dynamic_cast, reinterpret_cast。
    static_cast(常规类型转换):类似C风格类型,具有相同的限制。1、基本数据类型之间的转换 2、用户自定义类型之间,必须有相应构造函数 3、对象指针类型转换,不进行运行时类型检查,不安全 4、任何类型表达式转换成 void类型,不能把struct转换成int,或者把double转换成指针类型,不能移除表达式的常量性。
    const_cast(去常量性转换):将某个对象的常量性去掉即:const —> nonconst 而nonconst —> const请使用static_cast。
    dynamic_cast(继承转换):执行继承体系中“安全的向下转型或跨系转型动作”,你可以将指向base class objects的pointers或references转型为指向“derived(或sibling base)class objects的pointers或 reference”。如果转型失败,返回一个null指针(当转型对象是pointers)或者一个异常(当转型对象是reference)。
    reinterpret_cast(函数指针转换):与编译平台相关,不具移植性,尽量避免使用。
    type void (*FuncPtr)();    //FuncPtr 是个指针,指向某个函数,函数无任何参数,返回值为void
    FuncPtr funcPtrArray[10];    //funcPtrArray 是个数组,内含10个FuncPtrs
    int doSomething();
    //funcPtrArray[0] = &doSomething;    //错误,类型不符
    funcPtrArry[0] = reinterpret_cast<FuncPtr>(&doSomething);    //这样便可通过编译
  3. 绝对不要以多态方式处理数组——如果你避免让一个具体类继承自另一个具体类,就不太会犯“以多态方式处理数组”的错误。注意:多态和指针算术不能混用,数组对象几乎总是涉及指针的算术运算,所以数组不要和多态混用。array[i]其实是一个“指针算术表达式”的简写:它代表的其实是*(array + i)。array是个指针,指向数组起始处,array和array+i内存距离i * sizeof(数组中的对象)。
  4. 非必要不提供 Default constructor——凡是“合理地从无到有生成对象”的classes,都应该内含 default constructor,而“必须有某些外来信息才能生成对象”的classses,则不必拥有default constructor。classes 如果缺乏default constructor带来的三个缺点:
    1)产生数组时候,一般而言没有任何方法可以为数组中对象指定constructor自变量。
    class EquipmentPiece{
    public:
        EquipmentPiece(int IDNumber);
        ...
    }
    EquipmentPiece bestPieces[10];    //错误!无法调用EquipmentPiece ctors
    EquipmentPiece *bestPieces = new EquipmentPiece[10];    //错误!
    
    //解决方法,使用 “指针数组” 而非 “对象数组”
    typedef EquipmentPiece* PEP;        //PEP是个指向EquipmentPiece的指针
    PEP bestPieces[10];             //很好,不需要调用ctor
    PEP *bestPieces = new PEP[10];
    for(int i = 0; i < 10; ++i)    //为每一个数组成员赋值
        bestPiece[i] = new EquipmentPiece(ID number);  //此方法有两个缺点,第一:数组所指向的对象必须删除。第二:你需要的内存总量比较大。
    可以使用下面方法避免“过度内存使用”,先为数组分配raw memory,然后使用“placement new”在这块内存上构造EquipmentPiece objects。
    void *rawMemory = operator new[](10*sizeof(EquipmentPiece));
    //让bestPiece指向此块内存,使这块内存被视为一个EquipmentPiece数组
    EquipmentPiece *bestPiece = static_cast<EquipmentPiece*>(rawMemory);
    //利用“placement new”构造这块内存中的EquipmentPiece objects
    for(int i = 0; i < 10; ++i)
        new (&bestPiece[i])EquipmentPiece(IDNumber);
    ...
    //将bestPieces中各个对象,以其构造顺序的相反顺序析构掉
    for(int i = 9; i >= 0; --i)
        bestPiece[i].~EquipmentPiece();
    //释放raw memory
    operator delete[](rawMemory);
    2)Classes如果缺乏default constructor,带来的第二个缺点是:他们将不适用于许多template-based container classes。
    3)Virtual base classes如果缺乏default constructor,必须要求其所有derived classes都必须知道了解其含义,并提供virtual base class的constructor自变量。
    添加无意义的default constructor,也会影响classes的效率,成员函数必须测试所有的部分是否都被正确的初始化。
  5. 对定制的“类型转换函数”保持警觉——有两种函数允许编译器作为隐式类型转换之用:单自变量constructor和隐式类型转换操作符。注意:最好不要提供任何类型转换函数,因为在你从未打算也未预期的情况下,此类函数可能会被调用,而结果可能是不正确、不直观的程序行为,很难调试。解决的办法是以功能相等的另一个函数(asdouble()函数)取代类型转换操作符。如果不想进行隐式类型转换,只要将constructor声明为explicit即可,显式类型转换仍是被允许的
  6. 区别 increment/decrement操作符的前置(prefix)和后置(postfix)形式——重载函数是以其参数类型来区分前置和后置的。例如:
    class UPInt
    {
    public:
        UPInt& operator++();    //前置式++,返回的是一个引用,没有参数。不产生临时变量,效率更高,推荐使用
        const UPInt operator++(int);    //后置式++,返回的是一个const对象,有一个int类型参数,调用时候编译器默认设置为0。
                                        //产生临时变量,不推荐使用。
    ...
    };
    
    UPInt& UPInt::operator++()    //前置式:累加然后取出
    {
        *this +=1;
        return *this;
    }
    
    const UPInt UPInt::operator++(int)    //后置式:取出然后累加
    {
        UPInt oldVvalue = *this;
        ++(*this);    
        return oldValue;
    }
    后置式返回一个const 对象,是为了防止 i++++(实施操作符两次)的情况。
  7. 千万不要重载&&、|| 和 ,(逗号)操作符——和C一样,C++对于“真假值表达式”采用所谓的“骤死式”评估方式。C++规定不能重载的操作符有:. (点) .*(点星):: (作用域运算符) ?:(三目运算符)new、delete、sizeof、typeid、static_cast、dynamic_cast、const_cast、reinterpret_cast。
  8. 了解各种不同意义的new和delete——1、new operator:分配足够的内存,用来放置某个对象。调用一个constructor,为刚才分配的内存中的那个对象设定初值。2、operator new:唯一的任务就是分配内存。见条款4,函数operator new函数通常声明如下:
    void * operator new(size_t size);
    
    返回类型为void*,指向一块原始的、未设初值的内存。size_t表示需要分配多少内存。
    
    string *ps = new string("Memory Management");
    转换为
    void* rawMemory = operactor new(sizeof(string));        //获取原始内存放置一个string对象
    call string::string("Memory Management") on *memory;    //将内存中的对象初始化
    string *ps = static_cast<string*>(rawMemory);            //让ps指向新完成的对象
    Placement new:特殊版本operator new,在分配好(并拥有指针)的原始内存上构建对象。
    class Widget
    {
    public:
        Wiget(int widgetSize);
    ...
    };
    Widget * constructWidgetInBuffer(void* buffer, int widgetSize)
    {
        return new (buffer)Widget(widgetSize);    //是operator的用法之一,其中指定一个额外参数作为new operator “隐式调用operator new”
                        //时所用,于是被调用的operator new除了接受 “size_t自变量”之外,还接受一个void* 参数,指向一块内存,准备构造对象
                        //这样的operator new就是所谓的placement new。
    }
    //
    void* operator new(size_t, void* location)
        return location;
    
    数组版operator new[]:调用constructor数量更多,注意应该成对使用new和delete。
  9. 利用destructors避免泄漏资源——C++标准程序库中auto_ptr(只适用于“单一对象”)的class template。每个auto_ptr的constructor都要求获得一个指向heap object的指针,其destructor会将该heap object删除。只要遵循这个规则:把资源封装在对象内,通常便可以在exception出现时避免资源泄漏。
  10. 在constructor内阻止资源泄漏——由于C++不自动清理那些“构造期间抛出exception”的对象,所以你必须设计你的constructor,使他们在那种情况下亦能自我清理。通常只需要将所有异常捕捉起来,执行某种清理工作,然后重新抛出异常,使他继续传播出去即可。常量指针必须通过构造函数初始化成员初值列表加以初始化(见条款12)。处理“构造过程中可能发生的exception”相当棘手,但是使用auto_ptr(以及与auto_ptr相似的classes)可以消除大部分劳役,使用它们不仅能够让代码更容易理解,也使得程序在面对异常时更健壮。
    C++保证“删除null指针”是安全的。C++只会析构已构造完成的对象,对象只有在其constructor执行完毕才算是完全构造妥当。
  11. 禁止异常(exception)流出destructors之外——两种情况下destructor会被调用1、当对象在正常情况下被销毁,也就是它离开了它的生存空间或是被明确地删除;2、当对象被exception处理机制(也就是exception传播过程中的(栈展开)stack-unwinding机制)销毁。
    Session::~Session()
    {
        try{
        logDestruction(this);
        }
        catch(...){ }    //通过三个点...捕捉所有异常,避免程序崩溃。
    }
    让异常传递到析构函数之外有两个原因:1、可以避免terminate函数在exception传播过程的栈展开(stack-unwinding)机制中被调用。2、可以协助确保destructors完成其应该完成的所有事情。
  12. 了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异——1、异常对象被抛出作为exception时,总是会发生复制(copy),无论被捕捉的exception是以by value或by reference方式传递,而交到catch子句手上的正是个副本。对象作为函数参数传递时候,不一定需要被复制(by reference)2、当对象被复制当作一个exception,复制行为是由对象的copy constructor执行的,而这个constructor相应于改对象的“静态类型”而非“动态类型”。和其他所有“C++复制对象”的情况一样,复制动作永远是以对象的静态类型为本。(条款25除外)。
    catch(Widget& w)    //捕捉Widget exceptions
    {
        ...        //处理exception
        trow;    //重新抛出此exception
    }
    catch(Widget& w)    //效率更好,推荐使用
    {
        ...
        trow w;    //传播被捕捉的exception的一个副本
    }
    catch (Widget w)    //得付出“被抛出物”的“两个副本”的构造代价,其中一个构造动作用于“任何exceptions都会产生临时对象”,
                        //另一个用于将临时对象复制到w,此后析构两个对象也有代价,不推荐
    catch (Widget& w)    //付出“被抛出物”的“单一副本”的构造代价
    撒旦法v
  13. 以by reference方式捕捉exceptions——1、首先考虑catch by pointer,throw by pointer是唯一在搬移“异常相关信息”时不需复制对象的一种做法。如:
    class exception { ... };
    void someFunction()
    {
        static exception ex;    //static 约束很重要,若为局部变量catch收到的指针,指向不存在的对象
        ...
        throw &ex;    //抛出一个指针,指向ex
    }
    void doSomething()
    {
        try {
        someFunction();        //可能抛出一个exception* 。
        }
        catch(exception *ex) {    //捕捉到exception*,
        ...                        //没有任何对象被复制
        }
    }
    或者抛出heap对象
    void someFunction()
    {
        throw new exception;    //或者抛出一个heap-based object,并假设operator new 本身不会抛出exception
    }
    但是他们应该删除他们获取的指针吗?——如果exception object被分配于heap,他们必须删除,否则便会资源泄漏。catch - by - pointer和语言本身建立起来的惯例有矛盾。因此不推荐使用
    catch - by - value可以消除上述“exception是否需要删除”等问题,但是每当exception被抛出,就得复制两次,此外也会引起切割(slicing)问题。
    catch - by - reference没有对象删除问题,没有切割问题,而且exception object只会被复制一次。强烈推荐使用。
  14. 明智运用exception specifications(异常规范)——1、千万不要为template提供意味深长的exception specifications。2、如果A函数内调用了B函数,而B函数无exception specifications,那么A函数本身也不要设定exception specifications。特别是回调函数(callback)的时候。3、处理“系统”可能抛出的exception。最常见的是bad_alloc,那是在内存分配失败时,由operator new 和operator new[]抛出的。可以使用自己定义的Unexpected-Exception取代非预期的exception。
    class UnexpectedException {};
    void convertUnexpected()
    {
        throw UnexpectedException();
    }
    并以convertUnexpected取代默认的unexpected函数
    set_unexpected(convertUnexpected);
    一旦完成这些部署,任何非预期的exception便会导致convertUnexpected被调用,于是非预期的exception被一个新的、类型为UnexpectedException的exception取而代之。
  15. 了解异常处理(exception handing)的成本——使用try语句块,代码整体膨胀5%-10%,执行速度亦下降这个数。和正常函数返回动作比较,由于抛出异常而导致的函数返回,其速度可能比正常速度慢3个数量级。因为exception应该是罕见的,80-20法则(条款16)告诉我们,如此的事件应该不会对一个程序的整体性能有太大的冲击。注意:为了让exception的相关成本最小化,只要能够不支持exception,编译器便不支持;请将你对try语句块和exception specifications 的使用限制于非用不可的地点,并且在真正异常的情况下才抛出异常。
  16. 谨记 80 — 20法则——重点在于:软件的整体性能几乎总是由其构成要素(代码)的一小部分决定。1、大部分人采用的瓶颈查找法是“猜”,用经验猜、用直觉猜。大部分程序员对程序性能特质,都有错误的直觉(程序的性能特质倾向于高度的非直觉性)。可行之道是完全根据观察或实验来识别出造成你心痛的那20%代码。而辨别之道是借助某个程序分析器。
  17. 考虑使用 lazy evaluation(缓式评估)——关键在于所谓的拖延战术。lazy evaluation可在一下四个场合排上用场1、引用计数,避免非必要的对象复制。在真正需要之前,不要着急为某物做一个副本。2、区分读和写,读的成本比较低,写的成本比较高,因此使用COP - On - Write(cow技术)写时拷贝。3、Lazy Fetching (缓式取出避免非必要数据库读写。产生一个对象的外壳,不从磁盘读取任何字段数据,当对象内的某个字段被需要了,程序才从数据库中取回对应的数据。4、Lazy Expression Evaluation(表达式缓评估)避免非必要数值计算动作。
  18. 分期摊还预期的计算成本——超急评估(over - eager evaluation):在被要求之前就先把事情做下去。思想是:预期要用到某个数值,提前保留下来。1、策略是:使用一个局部缓存,将相对昂贵的“数据库查询动作”以相对价廉的“内存内数据结构查找动作”取而代之。2、预先取出:读整块数据块,比分成两三次每次读一小块数据,速度上块很多。如果某处数据被需要,通常其临近的数据也会被需要。
  19. 了解临时对象的来源——区别局部对象,临时对象是不可见的,只要产生一个non-heap object而没有命名,便诞生一个临时对象。通常发生于两种情况1、当隐式类型转换被施行起来以求函数调用能够成功。2、当函数返回对象时候。可以通过“返回值优化”见条款20。
  20. 协助完成“返回值优化(RVO)”——如果返回值必须返回对象(如:operator*),那就返回对象。但是可以通过特殊写法,让编译器消除临时对象的成本。
    //最优效率的做法
    inline const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
        return Rational(lhs.number() * rhs.number(), lhs.denominator() * rhs.denominator());
    }
  21. 利用重载技术(overload)避免隐式类型转换——利用重载技术时候需要注意,每一个重载的operator必须带有一个“用户定制类型”的参数。不要忘了80-20原则,增加一大堆重载函数不一定是件好事,除非使用重载函数后,程序的整体效率获得重大的改善。
  22. 考虑以操作符复合形式(op =)取代其独身形式(op)——要确保操作符的复合形式(operator+=)和其独身形式(operator+)之间的自然关系能够存在,一个好方法就是以前者为基础实现后者。
    class Rational {
    public:
        Rational& operator+=(const Rational& rhs);
        Rational& operator-=(const Rational& rhs);
    };
    //利用operator+=实现
    const Rational operator+(const Rational& lhs, const Rational& rhs)
    {
        return Rational(lhs) += rhs;
    }
    //或者使用模板实现
    template<class T>
    const T operator+(const T& lhs, const T& rhs);
    {
        return T(lhs) += rhs;
    }
    1、一般而言,复合操作符比其对应的独身版本效率高。因为独身版本通常必须返回一个新对象,我们必须负担临时对象的构造和析构成本。
    2、如果同时提供某个操作符的复合形式和独身形式,便允许你的客户在有效率与便利性之间做取舍。
  23. 考虑使用其他程序库——不同的程序库即使提供相似的机能,也往往表现出不同性能取舍策略,所以一旦你找出程序的瓶颈,你应该考虑是否有可能因为该用另一个程序库而移除这些瓶颈。
  24. 了解 virtual functions、multiple inheritance(多继承)、virtual base classes、runtime type identification(运行时类型识别)的成本——1、vtbl通常是一个由“函数指针”架构而成的数组,某些编译器会以链表取代数组,但基本策略相同。尽量避免将虚函数声明为inline,方法一是探测式,类的虚函数一般产生于内含其第一个non-inline,non-pure虚函数定义式的目标文件中。2、多继承时,单个对象有多个vptrs,会增加对象体积大小。3、RTTI能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询,这些信息被存储在类型为type_info的对象里。通常,RTTI被设计为在类的vbtl上实现。
    性质 对象大小增加 Class数据量增加 Inlining几率降低
    虚函数
    多重继承
    虚拟基类 常常 有时候
    运行时类型识别
    技术——讲到:
    1、如何让constructors及non-member function像虚函数一样的作用?
    2、如何限制class的实体(对象)个数?
    3、如何阻止对象产生于heap内?
    4、如何保证对象产生于heap内?
    5、如何产生某个对象使它在“其他某些class的member functions”被调用时,自动执行某些动作?
    6、如何区分operator[]的读/写用途?
    7、如何产生一个虚函数,使其行为视多个(而非单个)对象的动态类型而定?
  25. 将constructor和non-member functions虚化——所谓virtual constructor是某种函数,视其获得的输入,课产生不同类型的对象。virtual constructor在许多情况下有用,其中之一就是从磁盘读取对象信息。
  26. 限制某个class所能产生的对象数量——1、阻止某个class产生对象最简单的方法是将其constructor 声明为private。方法一:让打印机成为一个function static,如下所示:
    namespace PrintingStuff{
    class Printer
    {
    public: 
        submitJob(const PrintJob& job);
        void reset();
        void performSelfTest();
        ...
        friend Printer& thePrinter();
    private:
        Printer();
        Printer(const Printer& shs);
    ...
    };
    Printer& thePrinter()
    {
        static Printer p;        //形成唯一一个Printer对象,是函数中的static 对象而非class中的static对象。
        return p;
    }
    }
    //使用了namespace之后我们可以这样调用
    PrintingStuff::thePrinter().reset();
    PrintingStuff::thePrinter().submitJob(buffer);
    //使用 using PrintingStuff::thePrinter;
    thePrinter().reset();
    thePrinter().submitJob(buffer);
    “class拥有一个static 对象”的意思是:即使从未被使用到,它也会被构造(及析构)。
    “函数拥有一个static对象”的意思是:此对象在函数第一次被调用时才产生。(然而你必须在函数每次调用时检查对象是否需要诞生)
    C++哲学基础是:你不应该为你并不使用的东西付出任何代价,而“将打印机这类对象定义为函数内的一个static”,正是固守此哲学的一个做法,也是我们应该尽可能坚持的一个哲学。
    inline概念上:它意味着编译器应该将每一个调用动作,以函数本身取代,但对于non-member functions,它还意味着这个函数有内部连接(internal linkage)。函数如果带有内部连接,可能会在程序中复制。因此如果你有一个inline non-member function并于其中含有一个local static对象,你的程序可能会有多份该static 对象副本。所以:千万不要产生内含local static 对象的 inline non-member functions
    2、利用numObjects来追踪记录目前存在多少个Printer对象。这个数值在constructor中累加,并在destructor中递减。如果外界企图构造太多Printer对象,我们就抛出一个类型为TooManyObjects的exception。
    //将对象计数和伪构造函数结合
    class Printer
    {
    public:
        class TooManyObjects();
        //伪构造函数
        static Printer * makePrinter();
        static Printer * makePrinter(const Printer& rhs);
        ~Printer();
        void reset();
        ...
    private:
        static size_t numObjects;
        static const size_t maxObjects = 10;    //设置允许对象个数
        //若不支持使用enum{ maxObjects = 10 };或者像numObjects一样
        Printer();
        Printer(const Printer& rhs);        //若只允许一个时候,不要定义此函数,因为我们决不允许复制行为
    };
    
    size_t Printer::numObjects = 0;
    const size_t Printer::maxObjects;
    Printer::Printer()
    {
        if(numObject >= maxObjects){        //可以自定义限制对象个数。
        throw TooManyObjects();
        }
        proceed with normal construction here;
        ++numObjects;
    }
    Printer::Printer(const Printer& rhs)
    {
        if(numObject >= maxObjects){        //可以自定义限制对象个数。
        throw TooManyObjects();
        }
        proceed with normal construction here;
        ++numObjects;
    }
    
    Printer * Printer::makePrinter()
    {    return new Printer;    }
    
    Printer * Printer::makePrinter(const Printer& rhs)
    {    return new Printer(rhs);    }
    
    Printer::~Printer()
    {
        perform normal destruction here;
        --numObject;
    }
    
    Printer p1;     //错误!default ctor 是private;   
    Printer *p2 = Printer::makePrinter();    //没问题,间接调用 default ctor;
    Printer p3 = *p2;        //错误!copy ctor是private
    
    p2->reset();
    ...
    delete p2;        //避免资源泄漏,如果p2是个auto_ptr此动作不需要
    仅仅使用“对象计数这种方式带来的问题是:继承和组合时候,numObjects可能并不能正确表示。Printer对象可于三种不同状态下生存:(1)它自己(2)派生物的“base class成分”(3)内嵌(组合)于较大对象之中。
    “带有private constructors 的 class 不得被继承”这一事实导致“禁止派生”的一般性体制。
    一个用来计算对象个数的Base Class
    template<class BeingCounted>
    class Counted
    {
    public:
        class TooManyObjects {};
        static int objectCount() { return numObjects; }
    protected:        //设计作为base classes
        Counted();
        Counted(const Count& rhs);
        ~Counted() { --numObjects; }
    private:
        static int numObjects;        
        static const size_t maxObjects;
        void init();        //用来避免ctor码重复出现
    };
    
    template<class BeingCounted>
    Counted<BeingCounted>::Counted(){ init(); }
    
    template<class BeingCounted>
    Counted<BeingCounted>::Counted(const Counted<BeingCounted>&){ init(); }
    
    template<class BeingCounted>
    void Counted<BeingCounted>::init()
    { 
        if(numObjects >= maxObjects) throw TooManyObjects();
        ++numObjects;
    }
    template<class BeingCounted>
    int Counted<BeingCounted>::numObjects;    //定义numObject,并自动初始化为0;const maxObject,留给用户自己定义。
    
    //Printer class 运用Counted template:
    class Printer : private Counted<Printer>    //利用Counted template 追踪目前有多少对象的实现细节最好保持private。
    {
    public:
        static Printer * makePrinter();
        static Printer * makePrinter(const Printer& rhs);
        ~Printer();
        using Counted<Printer>::objectCount;        //让此函数对于Printer的用户而言成为public的,用户就可以知道有多少个对象存在
        void submitJob(const PrinterJob& job);
        void reset();
        ...
        using Counted<Printer>::objectCount;
        using Counted<Printer>::TooManyObject;
    private:
        Printer();
        Printer(const Printer& rhs);
    };
  27. 要求(或禁止)对象产生于heap之中——1、要求对象产生于heap之中,比较好的方法是让destructor成为private,而constructor仍然是public,利用伪destructor函数来调用真正的destructor。但是妨碍了继承和内含,但是都可以解决
    class UPNumber
    {
    public:
        UPNumber();
        UPNmumber(int initValue);
        UPNumber(double initValue);
        UPNumber(const UPNumber& rhs);
    //伪destructor
    void destroy() const { delete this; }
    ...
    private:
        ~UPNumber();        //注意:dtor位于private 区内
    };
    
    UPNumber n;    //错误!(虽然合法,但当n的dtor稍后被隐式调用,就不合法了)
    UPNumber *p = new UPNumber;    //良好
    ...
    delete p;        //错误!企图调用private destructor
    p->destroy();    //良好
    
    要继承时候,可以将private的destructor声明了protected即可。
    要内含的话,可以修改为“内含一个指针,指向UPNumber”。
    判断某个对象是否位于Heap内?
    static对象(涵盖声明为static的对象、global scope 和namespace scope内的对象)在什么位置?——视系统而定,在许多系统中,如图所示:
    Stack向下成长——高地址

    Heap向上成长
    Static Objects——底地址
     判断指针是否以oeprator new分配出来?——如下
    class HeapTracked
    {
    public:
        class MissingAddress{};        //
        virtual ~HeapTracked() = 0;        //纯虚函数使得HeapTracked成为抽象类
        static void *operator new(size_t size);
        static void operator delete(void *ptr);
        bool isOnHeap() const;
    private:
        typedef const void* RawAddress;
        static list<RawAddress> addresses;
    };
    
    list<RawAddress>HeapTracked::addresses;
    HeapTracked::~HeapTracked(){}            //HeapTracked为抽象类,但是destructor仍然必须有定义,所以提供一个空定义
    void * HeapTracked::operator new(size_t size)
    {
        void *memPtr = ::operator new(size);
        addresses.push_front(memPtr);
        return memPtr;
    }
    
    void HeapTracked::operator delete(void *ptr)
    {
        list<RawAddress>::iterator ot = find(addresses.begin(), addresses.end(), ptr);
        if(it !=addresses.end()){        //如果找到符合条件的元素,移除,并释放内存
        addresses.erase(it);            //否则表示ptr 不是operator new 所分配,于是抛出一个exception。
        ::operator delete(ptr);
        }else{
        throw MissingAddress();
        }
    }
    
    bool HeapTracked::isOnHeap() const
    {    //取得一个指针,指向*this 所占内存的起始处;
        const void *rawAddress = dynamic_cast<const void*>(this);
        list<RawAddress>::iterator it = find(address.begin(), addresses.end(), rawAddress);
        return it != addresses.end();
    }
    禁止对象产生于heap之中——(1)对象被直接实例化于heap之中(2)对象被实例化为derived class object内的“base class 成分”(3)对象被内嵌于其他对象之中。
    class UPNumber
    {
    private:
        static void *operator new(size_t size);
        static void operator delete(void *ptr);
    ...
    };
    UPNumber n1;        //可以
    static UPNumber n2;    //也可以
    UPNumber *p = new UPNumber;    //错误,企图调用private operator new。
    (1)因为对象总是调用operator new,而后者我们可以自行声明。因此可以将operator new声明为private应该足够了。(2)将operator new 声明为private,往往也会妨碍UPNumber被实例化为heap-base derived class objects的“base class成分”。
  28. Smart Pointers(智能指针)——所谓smart pointers,是“看起来,用起来、感觉起来都像内建指针,但提供更多机能”的一种对象。当你以smart Pointer取代C++的内建指针,你将获得以下各种指针的控制权:
    1、构造和析构。通常给smart pointer一个默认值0,以避免“指针未获初始化”的问题。
    2、复制和赋值。你可以控制指针进行深复制或者浅赋值。
    3、解引。当取用smart Pointer所指之物时,你有权决定发生什么事情。
    smart Pointer由template产生,由于它就像内建指针一样,所以它必须有“强烈的类型性”。
  29. Reference counting(引用计数)——1、使用引用计数后,对象便拥有了它自己,一旦不再有任何人使用它,它便自动销毁自己。也因此,reference counting构建出垃圾回收机制的一个简单形式。此计数有两个动机:第一为了简化堆对象周边的簿记工作。第二是为了实现一种常识,所有等值对象共享同一实值,不仅节省内存,也使程序速度加快。
    将一个struct嵌套放进一个class的private段落内,可以很方便地让改class的所有members有权处理这个struct,而又能够禁止任何其他人访问这个struct(当然,class的friend不在此限)。
    class String
    {
    public:
        String(const char *initValue = "");
        String(const String& rhs);
        ~String();
    private:
        struct StringValue {
        int refCount;
        char *data;
        StringValue(const char *initValue);
        ~StringValue();
        };
    ...
    };
    String::~String()
    {
        if(--value->refCount == 0) delete value;
    }
    String::String(const char *initValue) : value(new StringValue(initValue)) 
    {}
    String::String(const String& rhs) : value(rhs.value)
    {
        ++value->refCount;
    }
    String::StringValue::StringValue(const char* initValue) : refCount(1)
    {
        data = new char[strlen(initValue) + 1);
        strcpy(data, initValue);
    }
    String::StringValue::~StringValue()
    {
        delete [] data;
    }
  30. Proxy classes(替身类、代理类)——多维数组。凡用来代表(象征)其他对象的对象,常被称为proxy object(替身对象),而用以表现proxy objects者,我们称为proxy classes。代理类最神奇的功能是区分通过operator[]进行的是读操作还是写操作,它的思想是对于operator[]操作,返回的不是真正的对象,而是一个 proxy类,这个代理类记录了对象的信息。将它作为赋值操作的目标时,proxy类扮演的是左值;用其它方式使用它时,proxy类扮演的是右值。用赋值操作符来实现左值操作,用隐式类型转换来实现右值操作。用proxy类区分operator[]作左值还是右值的局限性:要实现proxy类和原类型的无缝替代,必须声明原类型的一整套操作符;另外,使用proxy类还有隐式类型转换的所有缺点。
  31. 让函数根据一个以上的对象类型来决定如何虚化——我们需要一种作用在多个对象上的虚函数。这类型问题,在C++中被称为二重调度问题,下面介绍几种方法解决二重调度问题。详细见书
  32. 在未来时态下发展程序——总是接受“事情总会改变”的事实,并准备应因之道。
    1、如果某个class在设计时,绝不打算成为derived classes,那么就不应该只是在头文件的class上端摆一样注释就好,而是应该以C++语法来阻止派生(见条款26)。
    2、如果要求其所有对象实体都必须于heap内产生,应该以条款27厉行这项约束。
    3、请为每一个class处理assignment 和copy constructor动作,即使没有人使用,现在没有不意味着将来没有。
    4、请努力让classes的操作符和函数拥有自然的语法和直观的语义。请和内建类型的行为保持一致:如有疑惑,请看ints有怎么样的表现。
    5、请让你的classes容易被正确地使用,不容易被误用,请接收“客户会犯错”的事实,并设计你的classes有预防、侦测或更正的能力。
    6、请努力写出可移植代码。
    7、请设计你的代码,使“系统改变所带来的冲击”得以局部化。尽可能采用封装性质,尽可能让实现细目成为private。尽量避免设计出virtual base classes,因为这种classes必须被其每一个derived class初始化。
    8、提供完整的classes——即使某些部分目前用不到。
    9、设计你的接口,使有利于共同的操作行为,阻止共同的错误。
    10、尽量使你的代码一般化(泛化),除非有不良的巨大后果。尽量用“未来式思维”去思考,它可增加你的代码重用性、加强其可维护性、使它更健壮,并促使在一个“改变实乃必然”的环境中有着优雅的改变。
  33. 将非尾端类(non-leaf classes)设计为抽象类(abstract classes)——允许通过指针进行“同型赋值”而阻止“异型赋值”的方法,1、通过两个赋值操作符重载,一个是virtual,一个是正常版本。该方法可以实现但是不推荐使用。2、让operator=成为Animal的private函数。
    class Animal
    {
    private:
    Animal& operatro=(const Animal& rhs);
    ...
    };
    class Lizard : public Animal {
    public:
    Lizard& operator=(const Lizard& rhs);
    ...
    };
    class Chicken : public Animal {
    public:
    Chicken& operator=(const Chicken& rhs);
    ...
    };
    
    Lizard liz1, liz2;
    ...
    liz1 = liz2;                //很好
    Chicken chick1, chick2;
    ...
    chick1 = chick2;            //很好
    
    Animal *pAnimal1 = &liz1;
    Animal *pAnimal2 = &chick1;
    ...
    *pAnimal1 = *pAnimal2;            //错误,企图调用private Animal::operator=
    
    不幸的是,Animal是一个具体类,而上述方法却使得Animal对象彼此间的赋值动作也不合法:
    Animal  animal1,animal2;
    animal1 = animal2;
    正确的做法是:消除“允许Animal对象相互赋值”的需要,而完成此事的最简单做法是让Animal成为一个抽象类,Animal就无法被实例化。如果当初设计Animal要相互赋值,则改一个新的抽象类AbstractAnimal。
    class AbstractAnimal
    {
    protected:
        AbstractAnimal& operator=(const AbstractAnimal& rhs);
    public:
        virtual ~AbstractAnimal() = 0;        //它必须内含至少一个纯虚函数,一般让destructor成为纯虚函数
    ...
    };
    class Animal : public AbstractAnimal
    {
    public:
        Animal& operatro=(const Animal& rhs);
    ...
    };
    class Lizard : public AbstractAnimal {
    public:
        Lizard& operator=(const Lizard& rhs);
    ...
    };
    class Chicken : public AbstractAnimal {
    public:
        Chicken& operator=(const Chicken& rhs);
    ...
    };
    将函数“声明为纯虚函数”并非暗示它没有实现码,而是意味着:
    1、目前这个class是抽象的。
    2、任何继承此class的具体类,都必须将纯虚函数重新声明为一个正常的虚函数(也就是说,不可以再令它 = 0)。
    如果是程序库中的具体类,你要继承有如下做法:
    1、将具体派生自既存的(程序库中的)具体类,但需要注意本条款一开始所验证的assignment相关问题,并且小心条款3所描述的数组相关陷阱。
    2、试着在程序库的继承体系中找到一个更高层的抽象类,其中你需要大部分功能,然后继承它。
    3、以“你所希望继承的那个程序库类”来实现你自己的新类,例如你可以令程序库类的一个对象成为你的data member,然后在你的新类中重新实现该程序库类的接口。
    4、做个乖宝宝,手上有什么就用什么。
  34. 如何在同一个程序中结合 C++ 和 C——需要考虑4件事情:
    1、name mangling(名称重整),C中你的函数名称不能重载。要压抑name mangling,必须使用C++的extern "C"指令:
    extern "C" {
    void drawLine(int x1, int y1);
    void simulate(int iterations);
    }
    2、Static 的初始化,一般在main函数最开始端插入一个static对象初始化构造操作,在最尾端安插一个static 对象的析构。
    3、动态内存分配,程序的C++部分使用new 和delete,程序的C部分使用malloc(及其变种)和free。
    4、数据结构的兼容性,将两个语言间的“数据结构传递”限制于C所能了解的形式;C++structs如果内含非虚函数,倒是不受此限。
  35. 让自己习惯于标准C++语言——在ARM(Annotated C++ Reference Manual)出版后的这些年,C++最重要的几项改变如下:
    1、增加了一些新的语言特性:RTTI、namespace、bool、关键词mutable 和explicit、enums最为重载函数之自变量所引发的类型晋升转换,以及在“class定义区内直接为整数型const static class members设定初值”的能力。
    2、扩充了Templates的弹性:允许member template存在、接纳“明白指示template当场实例化”的标准语法、允许function templates接受“非类型自变量”、可用class templates作为其他template的自变量。
    3、强化了异常处理机制
    4、修改了内存分配历程:加入operator new[]和operator delete[],内存为分配成功时候抛出异常,在内存失败时返回0;
    5、增加了新的转型形式:static_cast,dynamic_cast,const_cast 和 reinterpret_cast.
    6、语言规则更为优雅精炼:重新定义虚函数时,其返回类型不再一定得与原定义完全吻合。
    标准程序库改变:
    1、支持C标准函数库。
    2、支持strings。
    3、支持国别。
    4、支持I/O。
    5、支持数值应用。复数等
    6、支持广泛用途的container (容器)和 algorithm(算法)。
    auto_ptr实现代码

猜你喜欢

转载自blog.csdn.net/davidsher_zhou/article/details/81104267