Effective C++ 笔记

Effective C++ 笔记

Sec0 Introduction

  • 本书的目的:
    如何有效运用C++,使软件易理解、易维护、可移植、可扩充、高效、并有预期行为
  • 提出的忠告分两类:
    • 一般性的设计策略,带有具体细节的特定语言特性
      • 如何在两个不同做法中择一完成某项任务?
      • inheritance还是templates?
      • public还是private?
      • private继承还是composition?
      • 选择member函数还是non-member函数?
      • 选择pass-by-value还是pass-by-reference?
    • 更多的细节:
      • assignment操作符的适当返回类型
      • 何时该令析构函数为virtual?
      • 当operator new无法找到足够内存该怎么办?
  • 阅读本书的方式:
    • 从感兴趣的items开始

Sec1 Accustoming Yourself to C++

Item01: View C++ as a federation of languages

  • 现在的C++: Multiparadigm programming language
    一个同时支持procedural, object-oriented, functional, generic, metaprogramming的语言

  • 如何理解这样一个语言?
    将C++视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通例都倾向于简单、直观易懂并且容易记住。
    主要的次语言有4个:

    • C
      blocks、statements、preprocessor、built-in data types、arrays、pointers等统统来自C
    • Object-Oriented C++
      即C with Classes 所诉求的:classes(析构函数和构造函数)、encapsulation、inheritance、polymorphism、virtual函数(动态绑定)等等。
    • Template C++
      即C++的泛型编程(generic programming) 部分。带来了崭新的编程范型(programming paradigm),也就是所谓的template metaprogramming(TMP)。
    • STL
      template程序库。定义了containers、iterators、algorithms和function objects。
  • Tips:
    C++高效编程守则视状况而变化,取决于你使用C++的哪个部分。

Iterm02: Prefer consts, and inlines to #defines

  • 使用常量来替换宏定义

    • 使用#define的坏处:

      #define ASPECT_RATIO 1.653
      
      • 可能编译器开始处理源码之前就被预处理器移走了。于是记号名称ASPECT_RATIO有可能没有进入记号表(symbol table)
      • 所以运用此常量但是获得编译错误信息的时候,难以定位。
    • 解决之道:用常量来替换宏

      const double AspectRatio = 1.653;
      
      • 可以进入记号表(symbol table)内。
  • 替换的两种特殊情况:

    • 定义常量指针(constant pointers)

      要在头文件定义一个常量cahr*-based字符串。必须写 const两次:

      const char* const authorName = "Scott Meyers";
      

      其实string对象更合适

      const std::string authorName("Scott Meyers");
      
    • class专属常量:
      为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member):而为确保此常量最多只有一份实体,你必须让它称为一个static成员:

      class GamePlayer {
      private:
          static const int NumTurns = 5;	// 常量声明式
          int scores[NumTurns];
          ...
      };
      
      • 注1:

        变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。

        变量声明:用于向程序表明变量的类型和名字。

        定义也是声明:当定义变量时我们声明了它的类型和名字。

        extern关键字:通过使用extern关键字声明变量名而不定义它。

      • 注2:

        1.定义也是声明,extern声明不是定义,即不分配存储空间。extern告诉编译器变量在其他地方定义了。

        2.如果声明有初始化式,就被当作定义,即使前面加了extern。只有当extern声明位于函数外部时,才可以被初始化。

        例如:extern double pi=3.1416; //定义

        3.函数的声明和定义区别比较简单,带有{ }的就是定义,否则就是声明。

        4.除非有extern关键字,否则都是变量的定义。

      • 注3:程序设计风格:

        1. 不要把变量定义放入.h文件,这样容易导致重复定义错误。

        2. 尽量使用static关键字把变量定义限制于该源文件作用域,除非变量被设计成全局的。

        3. 可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。

      只要不取地址,可以声明并使用它们而无须提供定义式。
      但如果取某个class专属常量的地址,要看到定义式的话,需要这么写:

      const int GamePlayer::NumTurns;	// 定义
      

      因为class常量在声明时获得初值。因此定义时不可以再设初值。

    • 也可以用enum来代替const或者define。但是enum行为上更像一个define

  • 宏作为函数需要注意的地方:

    #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
    int a = 5, b = 0;
    CALL_WITH_MAX(++a, b);	// 递增两次
    CALL_WITH_MAX(++a, b+10);	//递增一次
    

    所以建议用template inline函数:

    template<typename T>
    inline void callWithMax(const T& a, const T& b)
    {
        f(a > b ? a : b);
    }
    

    这是一个函数,所以遵守作用域和访问规则。】

  • Tips:

    • 对于单纯常量,最好以const对象或者enums替换#defines
    • 对于形似函数的宏(macros),最好改用inline函数替换#defines

Item03: Use const whenever possible

  • const的一些知识点:
    const出现在星号左边,表示被指物是常量。星号右边则表示指针自身为常量。

    • 对应到迭代器:
      因为迭代器其实就是像T*指针。所以声明迭代器为const就像声明指针为const一样,表示指针不能变。但是想要迭代器指向的数据不变,就得用const_iteraotr
  • const面对函数声明时的应用:

    • 令函数返回一个常量值
      避免无意义的赋值动作
  • const成员函数:
    是为了确认该成员函数可作用于const对象身上。

    • 它们使class接口比较容易被理解
    • 操作const对象成为可能

    一个小知识点:两个成员函数如果只是常量性的不同,可以被重载!

    TextBlock tb("Hello");
    std::cout << tb[0];	// 调用non-const TextBlock::operator[]
    const TextBlock ctb("Hello");
    std::cout << ctb[0];	// 调用const TextBlock::operator[]
    
    • mutable关键字:
      可以在const成员函数也可以更改成员变量!
  • 在const和non-const成员函数中避免重复(常量性转移 casting away constness)

    即:让non-const operator[]调用其const兄弟。可以避免代码重复!

    class TextBlock {
    public:
        ...;
        const char& operator[](std::size_t position) const {
            ...;
            return text[position];
        }
        char& operator[](std::size_t position) {
            return
                const_cast<char&>(
            		static_cast<const TextBlock&>(*this)
                		[position]
            	);
        }
    }
    

    第一次转型:用static_cast<const TextBlock&>将*this转换。以可以调用const类型的[]。第二次是从const operator[]的返回类型值中移除const。

    • 注:不应该用const类型调用non-const类型!
  • 总结:

    • 将某些东西声明为const,可以帮助编译器侦测出错误用法,const可以被施加于作用域中的任何对象、函数参数、函数返回类型、成员函数本体。
    • 编译器强制实施bitwise constness,但写程序的时候应该使用概念上的常量性 conceptual constness。
    • 注意实现non-const和const版本的代码重复!

Item04: Make sure that objects are initialized before they're used

  • array(来自C part of C++) 不保证其内容被初始化,但是vector(来自STL part of C++)就保证初始化

  • 所以建议永远都使用初始化!

    • 对于构造函数,确保将每个成员初始化。
      而注意,在构造函数的{}里面进行赋值,并不是初始化!而是assignment!赋值!
      C++中,对象的成员变量的初始化动作发生在进入构造函数本体之前。最佳写法是:使用所谓的member initialization list。成员初始列替换赋值动作。从而构造函数本体不需要进行任何动作。
  • 成员初始化次序:
    base classes总是更早于derived classes被初始化的。
    而class的成员变量总是以其声明次序被初始化。

  • 不同编译单元内定义之 non-local static 对象的 初始化次序:

    • 注:local static对象指的是在函数里面的static对象。其他的,global对象、定义于namespace作用域内的对象、在classes内、在file作用域内被声明为static的对象称为 local static对象。
      non-local static对象,会在main()结束的时候调用析构函数自动销毁。

    客户机和本机的non-local static的初始化顺序很难决定,所以建议:

    • 将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些寒湖是返回一个reference指向它所含的对象,然后用户调用这些函数,而不涉及对象。即non-local static对象被local static对象替换了。
      这其实是Singleton模式的一个常见实现手法。可以保证获得的reference的对象指向一个历经初始化的对象。
    class FileSystem { ... };
    FileSystem tfs() {
        static FileSystem fs;
        return fs;
    }
    class Directory { ... };
    Directory::Directory( params ) {
        ...;
        std::size_t disks = tfs().numDisks();
        ...;
    }
    Directory& tempDir() {
        static Directory td;
        return td;
    }
    
    • 这种函数第一行定义并初始化一个local static对象,第二行返回它。挺适合inlining的。

      但是在多线程下可能有麻烦!解决方法是,在单线程启动阶段(single-threaded startup portion) 手工调用所有reference-returning 函数。

  • 总结:

    • 为内置型对象手工初始化
    • 使用成员初值列。少在构造函数里卖弄使用赋值操作。注意次序!
    • 用local-static对象替代non-local static对象。

Sec2 Constructors, Destructors, and Assignment Operators

Item05: Know what functions C++ silently writes and calls

  • default 构造函数和析构函数
    编译器产生的构造函数是个non-virtual。除非这个class的base class自身声明有virtual析构函数。

  • 如果在一个内含reference成员的class内支持赋值操作(assignment),必须自己定义copy assignment操作符!内含const成员也一样。

    或者,如果某个base classes将copyassginment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

  • 总结:
    编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符和析构函数
    但是要注意特殊情况。

Item06: Explicitly disallow the use of compiler-generated functions you do not want

  • 所有编译器产出的函数都是public。为组织创建,我们得自行声明它们。所以我们可以将copy构造函数或者copy assignment函数声明为private。借此可以组织自动创建。但是这也不太安全!

    • 更好的做法:

      class HomeForSale {
      public:
          ...;
      private:
          ...;
          HomeForSale(const HomeForSale&);	// 只有声明
          HomeForSale& operator=(const HomeForSale&);
      };
      

      这里就不需要写函数参数的名称了。反正也不会用。有了这样的定义,如果客户企图拷贝对象,编译器会报错,如果member函数或friend函数这么做,连接器会报错。
      这里可以将连接器错误移动到编译器,只需要将这些声明移动到private里面就行。

  • 为了阻止HomeForSale对象被拷贝,我们也可以将它继承Uncopyable:

    class HomeForSale : private Uncopyable {
        ...;
    }
    

    这里,class就不需要声明copy构造函数或者class assign操作符了。 相对更简单一些。

  • 总结:
    为驳回编译器自动生成的情况,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable 这样的base class也是一种做法

Item07:Declare destructors virtual in polymorphic base classes

  • 当基类有一个non-virtual析构函数的时候,且derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义 --- 实际执行的时候通常发生的是对象的derived成分没有被销毁。而derived析构函数也没有执行起来。(也就是局部销毁,这很可能形成内存泄漏、数据损坏)

  • 消除这个问题的方法:给base class定义一个virtual析构函数。

    class TimeKeeper {
    public:
        TimeKeeper();
        virtual ~TimeKeeper();
        ...;
    };
    

    任何class只要带有virtual函数,几乎确定应该有一个virtual析构函数。

    如果class不含virtual函数,通常不会作为base class。则建议不要将其析构函数定义为virtual(因为该类所占用的内存会发生变化,需要有虚指针和虚表)

  • 例子:标准string不含有任何virtual函数,但有时候程序员会将其作为base class。这个是非常不好的!

    • 因为如果程序在任意某处无意间将一个pointer-to-SpecialString转换为一个pointer-to-string。然后将转换所得的那个string指针delete,则会有行为不明确的错误
    • 类似的例子适用于任何不带virtual析构函数的class。包括STL
    • 总之, 不要继承一个标准容器或者任何其他带有non-virtual析构函数的class!
  • 纯虚析构函数:
    一般有抽象class的时候可以写!
    这样就不用担心析构函数的问题了。但必须为pure virtual析构函数提供一份定义。

    class AWOV {
    public:
        virtual ~AWOV() = 0;
    };
    AWOV::~AWOV() { }
    
  • 总结:

    • 多态基类应该声明一个virtual析构函数。
      而且如果class带有任何的virtual函数,它就应该有一个virtual析构函数
    • Class的设计目的是如果不是作为base classes使用,或者不是为了具备多态性,就不该声明virtual析构函数

Item08: Prevent exceptions from leaving destructions

  • C++并不禁止析构函数吐出异常,但是不鼓励这样做。(比如如果有10个该析构,但是第一个就异常了,下面的9个就销毁不了了!)

  • 例子:数据库连接,在析构函数中写close

    class DBConnection {
    public:
        ...;
        static DBConnection create();
        
        void close;
    };
    class DBConn {	// 用来管理DBConnection对象
    public:
        ...;
        ~DBConn() {
            db.close();
        }
    private:
        DBConnection db;
    };
    
    • 要是close调用异常,析构函数就会传播该异常了。解决方法:DBConn的析构函数可以:

      • 如果close异常就结束程序,通常通过调用abort完成

        DBConn::~DBConn() {
            try {db.close();}
            catch (...) {
                ... 记录失败
        		std::abort();
            }
        }
        

        这样可以制止不明确行为

      • 吞下因调用close而发生的异常

        DBConn::~DBConn() {
            try {db.close();}
            catch (...) {
                ... 记录失败
            }
        }
        

        即:遇到错误继续执行

      但这两种方法都不太好:都无法对导致close抛出异常的情况做出反应
      一个较佳策略:

      • 重新设计DBConn接口:使其客户有机会对可能出现的问题作出反应

        class DBConn {
        public:
            ...
           	void close() {	// 供客户使用
                db.close();
                closed = true;
            }
            ~DBConn() {
                if(!closed) {
                  	try {db.close();}
                	catch (...) {
                    ... 记录失败
                	}
                }
        	}
        private:
            DBConnection db;
            bool closed;
        };
        

        将调用close的责任从DBConn析构函数转移到客户手上。

  • 总结:

    • 析构函数绝对不要吐出异常。
      如果可能异常,析构函数应该捕捉任何异常,然后吞下它们,或者结束程序。
    • 如果客户需要对某个操作函数运行期间抛出的异常做出反应。那么class应该提供一个普通函数,执行该操作。

Item09: Never call virtual functions during construction or destruction

  • 不应该在构造和析构函数中调用virtual函数。

    • 因为base class的构造函数一定会在derived class之前调用,所以当会调用virtual函数的时候,这时候调用的是base class版本!
      即:当derived class对象的base class构造期间,当前对象的类型是base class!

    • 析构函数也是类似的!

  • 如何确保一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用?

    • 在class Transaction内将logTransaction函数改为non-virtual。然后要求derived class构造函数传递必要信息给Transaction构造函数。则可以安全调用logTransaction
  • 总结:
    在构造和析构期间不要调用virtual函数。因为这列调用从不下降至derived class。

Item10: Have assignment operators return a reference to *this

int x, y, z;
x = y = z; 	// 连锁赋值

为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧 实参!

class Widget {
public:
    ...
   	Widget& operator=(const Widget& this) {
        ...
       	return* this;	// 返回左侧对象
    }
}

这个协议不仅仅适用于标准赋值形式,也适用于所有赋值相关运算。比如+=等等。

这只是一个建议!

Item11: Handle assignment to self in operator=

在C++primer还说了挺多

  • 自我赋值:

    class Widget {...};
    Widget w;
    w = w;
    

    很蠢但是合法。
    或者*px = *py;如果两个指向同一个东西。

    一般而言,如果某段代码操作pointers或者references而它们被用来指向多个相同类型的对象。就需要考虑这些对象是否为同一个!实际上两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名。因为一个base class的reference或者pointer可以指向一个derived class对象。

  • 如果遵循13,14的忠告,会运用对象来管理资源,而且可以确定所谓的资源管理对象在copy的时候有正确的举措,这种情况下赋值操作是自我赋值安全的(self-assignment safe)
    但如果尝试自行管理资源。可能会有陷阱:在停止使用资源之前就释放了。

  • 例子:

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

    这里的问题是,operator=函数内的*this跟rhs可能是同一个对象!这样就会导致delete错误!

    • 解决问题方法:
      operator=前面加一个identity test。证同测试

      Widget&
      Widget::operator=(const Widget& rhs) {
          if(this == &rhs)	return *this;
          ...	// 这个跟上面的一样
      }
      
    • 或者这样做:只需要注意在复制pb所指东西之前别删除pb就行了

    Widget&
    Widget::operator=(const Widget& rhs) {
        Bitmap* pOrig = pb;	// 记住原来的pb
        pb = new Bitmap(*rhs.pb);	// 指向了新的复件
        delete pOrig;
        return *this;
    }
    

    ​ 如果new Bitmap抛出异常。pb会保持原状。

    • 最佳方法:

      用copy and swap技术:

      class Widge {
          ...
      	void swap(Widget& rhs);
          ...
      };
      Widget& Widget::operator=(const Widget& rhs) {
          Widget temp(rhs);
          swap(temp);	// 交换
          return *this;
      }
      

      利用了事实:

      • 某个class 的copy assignment操作符可能被声明为by value传参。
      • by value传参会造成一份副本。

      个人想法:
      感觉可能直接在swap中实现证同测试,这样直接swap就行了。

      上面想法是错误的!因为这样rhs就不会删除了!而temp可以在离开{}后,自我销毁!

  • 总结:

    • 确保当对象自我复制的时候operator=有良好的行为。
    • 确定任何函数如果操作一个以上的对象,而其中对个对象是同一个对象的时候,行为仍然正确。

Item12: Copy all parts of an object

  • 设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝(赋值)。即copy构造函数和copy assignment 操作符。我们将其称为copying函数。

  • 注意:如果我们自己写了拷贝构造或者拷贝赋值函数,如果我们加了成员变量,我们也需要在这两个函数中加上初始化操作。

  • 注意:如果我们还写了derived class,我们也需要注意我们编写derived class的构造函数或者赋值函数的时候也要初始化从base class那里继承来的所有值!
    也就是:主要调用base class的拷贝构造函数!

    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
        : Customer(rhs), priority(rhs.priority)
    {
    	logCall();        
    }
    PriorityCustomer&
    PriorityCustomer::operator=(const PriorityCustomer& rhs) {
        logCall();
        Customer::operator=(rhs);	// 对base class成分进行赋值操作
        priority = rhs.priority;
        return *this;
    }
    
  • 故:复制每一个成分指的是:

    当编写一个copying 函数,请确保:

    • 复制所有local成员变量
    • 调用所有base classes内的适当copying函数、
  • 注意:不该令copy assignment操作符调用copy 构造函数!
    也不该让copy构造函数调用copy assignment操作符
    消除代码重复的方法:建议是在private里面定义一个init成员函数给两者使用!

Sec03: Resource Management

  • 所谓资源:用了之后需要返还给系统
  • 资源:内存、文件描述其、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets

Item13: Use objects to manage resource

  • 若只是这样:

    void f() {
        Investment* pInv = createInvestment();
        ...
    	delete pInv;
    }
    

    如果...中报错或者返回,会产生灾难性后果,所以解决方法是把资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。(C++的析构函数自动调用机制)

  • auto_ptr : 类指针 (pointer-like) 对象。也就是所谓的智能指针。
    其析构函数自动对其所指对象调用delete。

    这个就是unique_ptr等智能指针的鼻祖

  • 两个关键思想

    • 获得资源后立刻放进管理对象(managing object)内

      以对象管理资源的观念常常被称为“资源取得时机便是初始化时机” (Resource Acquisition Is Initialization (RAII))

    • 管理对象运用析构函数确保资源被释放
      所以注意别让auto_ptr同时指向同一对象(所以使用unique_ptr和shared_ptr)
      所以auto_ptr的一个特性,当通过复制构造函数或者复制赋值操作符复制他们的时候,原来的会变成nullptr!然后后面的会指向原来的资源

  • 替代思想:

    • 引用计数型智能指针:reference-counting smart-pointer: RCSP
      shared_ptr
      • 缺点:无法打破环状互指:即两个已经没有使用的对象在彼此互指,因为好像还在使用的状态
  • 注意:
    auto_ptr和shared_ptr两者在析构函数内做delete而不是delete [] . 那意味着动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是不明智的。但是这是可以通过编译的。

    std::auto_ptr<std::string> aps(new std::string[10]);
    std::shared_ptr<int> spi(new int[1024]);
    

    因为vector和string几乎总是可以取代动态分配而得到的数组!

  • 总结:

    • 使用RAII对象,在构造函数获取资源,析构函数释放资源
    • shared_ptr

Item14: Think carefully about copying behavior in resource-managing class

  • 并非所有资源都是heap-based。对那种资源来说,智能指针就不太适合了,可能需要我们建立自己的资源管理类。

  • 例子:
    使用C api函数处理类型为Mutex的互斥器对象。有lock和unlock两个函数可以用

    void lock(Mutex* pm);
    void unlock(Mutex* pm);
    

    为确定不会忘记解锁,我们可以建立一个class用来管理锁。由RAII守则支配。

    class Lock {
    public:
        explicit Lock(Mutex* pm)
            : mutexPtr(pm)
       	{ lock(mutexPtr); }
        ~Lock() { unlock(mutexPtr); }
    private:
        Mutex* mutexPtr;
    }
    
  • 如果Lock对象被复制,会发生什么事情?(也就是当一个RAII对象被复制,会发生什么事情?)

    解决情况:

    • 禁止复制
      参考item6:private继承其Uncopyable类就行了(就是把拷贝赋值和拷贝构造定义到了private里面)

      class Lock : private Uncopyable {
      public:
          ...
      }
      
    • 对底层资源使用“引用计数法” reference count。比如shared_ptr
      RAII classes便可以是先出reference-counting copying的行为。

      • 进一步:用shared_ptr实现Lock
        因为shared_ptr允许指定所谓的删除器(deleter - 一个函数或者函数对象)
        删除器将在引用为0的时候被调用。

        class Lock {
        public:
            explicit Lock(Mutex* pm)
                : mutexPtr(pm, unlock)	// 指定unlock!
           	{
             	lock(mutexPtr.get());	// get就是得到原始指针       
        	}
        private:
            std::tr1::shared_ptr<Mutex> mutexPtr;
        }
        

        此次没必要声明Lock class的析构函数,因为没有必要。因为item5说过,class析构函数会自动调用其non-static成员变量的析构函数!然后mutexPtr会在引用为0的时候自动调用删除器!

    • 复制底部资源
      复制资源管理对象的时候,进行的是深度拷贝!也就是要复制其所包覆的资源!

    • 转移底部资源的拥有权:
      即auto_ptr
      复制的时候原来的变成nullptr,转移指针!

  • 总结:

    • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为
    • 普遍常见的RAII copying做法是禁止复制和引用计数。

Item15: Provide access to raw resources in resource-managing classes

  • 有的API直接需要raw resource。所以建议提供一个能访raw resource的方法

  • 例子:

    std::shared_ptr<Investment> pInv(createInvestment());
    // 然后有一个函数
    int daysHeld(const Investment* pi);
    // 这样调用是错误的!
    int days = daysHeld(pInv);
    
  • 解决方法:

    • 显示转换
      shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换为其原始指针(的复件)

      int days = daysHeld(pInv.get());
      
    • 隐式转换
      比如shared_ptr等智能指针可以使用->和.运算符

    • 提供隐式转换函数

      调用get有泄露危险! -> 提供隐式转换函数
      见书上的Font例子。

      class Font {
      public:
          ...;
          operator FontHandle() const {
              return f;
          }
          ...;
      }
      

      但可能会增加错误机会!

  • 具体使用哪个解决方法要看具体情况,当然最好还是用item18的让接口容易使用,不易被误用

  • 一般来说get()是比较好的选择

  • 总结:

    • APIs往往要求访问raw resources,所以每一个RAII class应该提供一个取得源数据的方法
    • 显示转换更安全,但隐式转换对客户方便

Item16: Use the same form in corresponding uses of new and delete

  • 就是删除数组要用delete [] xxx;
    而且要特别注意typedef定义的是不是一个数组!所以最好不要将数组typedef!

Item17: Store newed objects in smart pointers in standalone statements

  • 使用对象管理式资源,也可能泄露资源

    processWidget(std::shared_ptr<Widget>(new Widget), priority());
    

    这些事情的执行顺序可能不太一样!特别是对priority()的调用!
    万一priority()调用异常,new Widget返回的指针将会遗失!

  • 避免此类问题的方法:
    分离语句

    std::shared_ptr<Widget> pw(new Widget);
    processWidget(pw, priority());
    

Sec4 Designs and Declarations

posted @ 2023-01-21 00:55  M1kanN  阅读(52)  评论(0编辑  收藏  举报