UP | HOME

Effective C++

Table of Contents

Chapter 1: Accustoming Yourself to C++

Item 1: View C++ as a federation of languages

  • C++ 发展多年以后其实更像如下的几个部分的结合体:
    1. C语言
    2. 面向对象的C语言: 构造析构,继承,封装,多肽
    3. Template: 就是generic programming
    4. STL: generic programming库
  • 我们要根据自己当前出的区域,来决定用什么技术更高效, 比如:
    1. 对于inline类型来说(c语言), build-in类型pass-by-value通常比pass-by-reference 高效, 但是在OOC里面, 由于有构造和析构的存在, 我们就更倾向于使用 pass-by-reference-const.
    2. 在Template领域里面, 也是要传pass-by-reference-const
    3. 但是在STL领域里面pass-by-value再次适用, 因为迭代器和函数对象都是在指针上面塑造 出来的, 那就必须传递指针(指针就是value, 引用才是reference)

Item 2: Prefer const, enums and inlines to #defines

  • #define的缺点:
    1. 用define定义以后,由于有预编译的存在, ASPECT_RATIO根本就没进入符号表,那么 出了错误,你也就不知道在哪里去改
      #define ASPECT_RATIO 1.653
      // Better Option
      const double AspectRation = 1.653
      
    2. 下面这个例子由于预编译的存在, STRING的内存被分配多次,如果用const替代的话, 只需要分配一次
      #define STRING "abcdefg\n"
      
      const char string[] = "abcdefg";
      
      printf(STRING); // Allocate memory for STRING
      
      printf(string); // Allocate memory for string, for the first and last time.
      
      //....
      printf(STRING); // Allocate memory for STRING again
      printf(string); // No allocate memory here
      
    3. #define并不重视作用域, 除非#undef, 否则她一直有效, 这对于封装来说是个灾 难(不存在private define这一说), const可以解决这个问题
    4. #define实现宏函数, 问题非常多, 有时候加再多的括号也没办法解决:
      #define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b)
      int a = 5, b = 0;
      
      CALL_WITH_MAX(++a, b);  // a is incremented twice
      CALL_WITH_MAX(++a, b);  // a is incremented once
      

Item 3: Use const whenever possible

  • const 指针是非常容易错的部分, 总结起来就是
    • const在*号左边,就是指针指向的东西const
    • const在*号右边,就是指针本身const(不能指向其他地址)
    char greeting[] = "Hello";
    char *p = greeting;              //non-const pointer, non-const data
    
    const char *p = greeting;        //non-const pointer, const data
    char const *p = greeting;        //same as above
    
    char * const p = greeting;       //const pointer, non-const data
    
    const char * const p = greeting; //const pointer, const data
    
  • const在STL里面的应用如下:
    std::vector<int> vec;
    const std::vector<int>::iterator iter = vec.begin(); // iter acts like a T* const
    
    std:vector<int>::const_iterator cIter = vec.begin(); // cIter acts like a const T*
    
  • 如果一个成员变量被声明为const, 那么她就不能改变过其他成员变量, 下面例子中, 函数operator[]的返回值为const, 那么x[const成员变量]的结果就无法被赋值.
    class TextBlock
    {
    public:
        const char& operator[](std::size_t position) const {  //operator[] for
            return text[position];                            //const object
        }
    
        char& operator[](std::size_t position) {              //operator[] for
            return text[position];                            // non-const object
        }
    private:
        std::string text;
    };
    

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

  • 如果是build-in类型的变量, 在定义的时候给一个初始化值, 因为不然的话, 其内容 是随机的
  • 对于其他非build-in类型来说, 要调用ctor来初始化, 当然也有注意事项:
    • 每个构造函数将所有成员初始化
    • 初始化顺序和定义顺序相同
    • 用member initialization list来进行初始化, 原因有:
      1. 有些时候有些值是只能初始化,而无法赋值的
      2. 调用成员初始化列表其实只是调用了一次copy ctor, 而default ctor在赋值的 话,等于除了default ctor又调用了一次operator=

Chapter 2: 构造析构赋值运算

Item 05: 了解C++默默编写并调用哪些函数

  • 一个类,如果你没声明的话,编译器会默认给你创建三个函数:
    • default ctor
    • copy ctor
    • copy assignment
    • dtor
  • 所有这些函数都是public, inline的
  • 编译器产生的dtor是non-virtual的
  • 如果某个base的类把copy assignment声明为private,那么derived类是不会生成 一个copy assignment的

Item 06: 若不想使用编译器生成的函数,就该明确拒绝

  • 为了不让编译器暗自提供某些功能,可以将相应的成员函数声明为private,并且不予 实现

Item 07: 为多肽基类声明virtual析构函数

  • 当derived 对象经由base 指针删除, 而base 带着一个non-virtual dtor, 那么其 结果未定义–通常情况下是derived成分没有被销毁
  • 如果一个class里面哪怕只有一个virtual函数,那么它一定要有一个virtual的dtor, 如果一个virtual函数都没有,那么它没有想过继承这件事情,设置一个virtual dtor会 增大对象的大小
  • 说到多肽, 我们可以有这么一种继承方法:动物类->鸟类->鸵鸟类, 其中鸟类和鸵鸟类 都可以实例化,因为有这种动物, 而动物类就不应该实例化. 为了防止这种类被实例化 c++中给他们了一个类型叫做"抽象基类(abstract base class)", 想让一个类成为 抽象基类很简单, 只需要在这个类里面定义一个纯虚函数即可
  • 所谓纯虚函数,就是derived类里面必须要实现,而base类里面不需要实现的函数,其定义 语法比较不常见
    class Base {
        virtual  void pure_virtual_example() = 0;
    };
    
  • 既然有一个纯虚函数就可以成为抽象基类可以了,而且既然是抽象基类肯定是要继承的 那么dtor肯定要是虚函数, 那么我们不妨"合二为一"把dtor设计成纯虚函数, 如下, 需要注意的是,纯虚dtor需要一个定义
    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base() {
            cout << "Base Ctor" << endl;
        }
        virtual ~Base() = 0;
        virtual void speak() = 0;
    };
    Base::~Base() {
        cout << "Pure Virtual Dtor Need One Difinition!" << endl;
    }
    
    class De : public Base {
    public:
        De() {
            cout << "De Ctor" << endl;
        }
        virtual ~De() {
            cout << "De Dtor" << endl;
        }
        void speak() {}
    };
    
    int main(int argc, char *argv[])
    {
        De *dd = new De();
        delete dd;
        return 0;
    }
    

Item 08: 别让异常逃离析构函数

  • c++并不禁止析构函数突出异常,但是它并不鼓励你这么做
  • 如果客户需要堆某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个 普通函数(而非在析构函数中)执行该操作

Item 09: 绝对不在构造和析构过程中调用virtual函数

  • Base class构造期间, virtual函数不是virtual函数,只是base的一个函数, 其 绝对不可能到derived class阶层

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

  • 赋值是可以写成连锁形式的, 其赋值顺序是才用右结合:
    x = y = z = 15;
    // Following is the real order by default
    x = (y = (z=15));
    
  • 为了实现赋值连锁, 赋值操作符函数必须返回一个reference指向操作符的左侧实参
    class Widget {
    public:
        Widget& operator=(const Widget& rhs) {
            // ...
            return *this;
        }
    };
    

Item 11: 在operator=中处理"自我赋值"

  • "自我赋值"发生在对象给自己赋值的时候,如果在这个期间有指针的话,那么事情就有可 能出现问题:
    class Bitmap {
    //..
    };
    
    class Widget {
    private:
        Bitmap* pb;
    };
    
    // Wrong Version!! what about if rhs is this
    Widget& Widget::operator=(const Widget& rhs) {
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
    
  • 破解"自我赋值"难题的方法是"证同测试(identity test)"
    Widget& Widget::operator=(const Widget& rhs) {
        if (this == &rhs) return *this; // identity test
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
    
  • 上面这个版本依然有异常方面的麻烦, 如果new Bitmap出现了异常, 那么pb已经被 删除,现在是一个空指针.所以我们要调整前面的语句顺序: 先记录pb的地址,在new Bitmap之后再删除这个指针
    class Widget {
        Bitmap* pOrig = pb; // remember previous pb
        // if exception here, the next line delete pOrig will not execute
        pb = new Bitmap(*rhs.pb);
        delete pOrig;
        return *this;
    }
    

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

  • OO编程会把内部都封装起来,而外界留下两个函数进行复制拷贝: copy ctor 和 copy assignment, 我们称之为copying函数
  • copying函数应该确保赋值"对象内部所有的成员变量", 和 "所有base class成员"
    class Customer {
        //...
    };
    
    class PriorityCustomer : public Customer {
    public:
        PriorityCustomer(const PriorityCustomer& rhs);
        PriorityCustomer& operator=(const PriorityCustomer& rhs);
    private:
        int priority;
    };
    
    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
        : Customer(rhs), priority(rhs.priority) {}
    PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
        Customer::operator=(rhs);
        priority = rhs.priority;
        return *this;
    }
    
  • 不要尝试某个copying函数取实现另一个copying函数, 应该将共同机能部分放进第 三个函数中(通常叫做init),由两个copying函数共同调用

Chapter 3: 资源管理

Item 13: 以对象管理资源:

  • 对c++来说,所谓资源就是申请了就要释放的资源,不仅仅是内存,包括文件描述符,互斥 锁(mutex lock)都要申请了还给系统
  • 假设我们有一个投资的类如下:
    class Investment {
        //...
    };
    
  • 我们通过工厂模式来返回某个Investment对象
    // 返回指针,指向动态分配的内存(存储Investment对象),
    // 调用者有责任释放他们
    Investment* createInvestment();
    
  • 使用使用工厂模式返回的对象,我们可以用一下代码来释放它.
    void f() {
        Investment* pInv = createInvestment();
        //...
        delete pInv;
    }
    
  • 这看起来妥当的方法却很有可能无法正确删除createInvestment返回的对象,因为//… 里面可能出现不正常的情况(无论如何,就是无法到达delete), 比如:
    • 过早的return
    • 抛出了异常
  • 为了应对这种情况,c++设计了auto_ptr来管理资源
    void f() {
        //会在对象离开作用域的时候, 自动调用析构函数保证资源释放
        std::auto_ptr<Investment> pInv(createInvestment());
        //...
    }
    
  • 这个简单的例子, 精确的演绎了"以对象管理资源"的两个关键想法:
    • 获得资源后立刻放入"管理对象(managing object)"里面
    • "管理对象(managing object)"来确保资源被释放
  • auto_ptr虽然实现了上面的两个关键想法,但是其为了防止"拷贝的时候,出现多个对象, 然后析构多次", 毅然而然的设计了一种特殊的"复制":一旦copy(或者copy assinment) 就把资源唯一的代理权给新的指针,自己变成null
    // pInv1指向createInvestment的返回值
    std::auto_ptr<Investment> pInv1(createInvestment());
    //现在pInv2指向对象, pInv1为null
    std::auto_ptr<Investment> pInv2(pInv1);
    //现在pInv1指向对象, pInv2为null
    pInv1 = pInv2
    
  • 由于起诡异的复制行为,而STL容器要求其元素发挥"正常的"复制行为,因此,这些容器里面 不能包含auto_ptr成员
  • 一个复制行为正常的智能指针是tr1::shared_ptr, 它使用引用计数来持续追踪乖哦能用 多少对象指向某个资源.
  • 不过可惜的是,shared_ptr也有很多缺点:无法判别循环引用(两个其实已经没有使用的对 象相互指向, 因而好像感觉都还在"被使用")
  • 智能指针(包括auto_ptr和shared_ptr)还有一个特别大的缺点,他们的实现中,析构的时候 总是suppose用户使用的是单个成员,所以只用delete,而不是delete[],所以下面两个例子 都是错误的
    // 馊主意,会用错误的delete形式
    std::auto_ptr<std::string> aps(new std::string[10]);
    std::tr1::shared_ptr<int> spi(new int[1024]);
    
  • 最后的最后,请大家注意的是createInvestment返回"未加工指针(raw pointer)", 简直 就是对资源泄漏的死亡邀请,因为调用者极易忘记释放资源.所以我们会在item18里面修改 createInvestment的借口

Item 14: 在资源管理类中小心coping行为

  • 复制RAII(Resource Acquisition Is Initialization) 对象必须一并复制它所管理的 资源,所以资源的copying行为决定RAII对象的copying行为
  • 普遍而常见的RAII copying行为是:抑制copying,使用引用计数法

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

  • 条款13的例子中使用了智能指针来管理Investment, 但是由于改变了指针的类型(变成了智能 指针型), 所以需要一些转换,有两种方式:
    • 显示转换, 智能指针提供的get函数
      std::tr1::shared_ptr<Investment> pInv(createInvestment());
      
      //declare
      int dayHeld(const Investment* pi);
      //usage
      int days = dayHeld(pInv.get());
      
    • 隐式转换

Item 16: 成对的使用new和delete时,要采用相同的形式

  • 游戏规则很简单,如果调用new的时候使用了[],那么在对应的delete的时候,也要加上[]
  • 如果typedef使用了的话, 那么delete的时候,要注意,不要被typedef的类型蒙蔽
    typedef std::string AddressLine[4];
    // new Addressline返回一个new string[4]
    std::string* pal = new AddressLine;
    
    //...
    delete pal; //Not defined
    delete []pal; //Good
    

Item 17: 以独立语句将newd对象置入智能指针

  • 这里看似是讨论智能指针的问题,其实是讨论c++编译器"灵活性"的一种:参数执行顺序 的灵活性:一个函数的几个参数如果需要计算值作为参数,那么谁先谁后,是无法保证的 (java, c#可以保证)
  • 比如我们有个例子,函数processWidget的两个参数pw, priority, 而我们想把函数 priority()的返回值作为priority的值. 某个new Widget的临时指针作为pw的值:
    int priority();
    
    //declaration for the function
    void processWidget(std::tr1::shared_ptr<Widget>, int priority);
    
    //usage fo the function
    processWidget(new Widget, priority());
    
  • 上面的这种语法在编译的时候无法通过,因为tr1::shared_ptr的构造函数虽然是以 一个raw pointer为参数,但是这个构造函数被声明为explicit,所以是无法进行隐式 转换的, 显示转换然后调用的代码如下
    processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
    
  • 上面看似挺好的代码,却隐藏着资源泄漏的危险, 原因就是我们上面说的c++编译器在 以什么顺序计算参数方面,是没有定数的. processWidget的参数要完成下面三件事:
    • 调用priority()
    • 执行new Widget, 生成一个临时的指针X
    • 把临时指针X作为参数,调用tr1::shared_ptr构造函数
  • 上面调用shared_ptr构造函数一定要在生成临时指针之后,但是调用priority(),可以 出现在第一步,第二步,或者最后一步,假设最终是以下面的这个顺序执行的:
    1. 执行new Widget, 生成一个临时的指针X
    2. 调用priority()
    3. 把临时指针X作为参数,调用tr1::shared_ptr构造函数
  • 如果在执行priority()的时候抛出了异常, 那么临时指针X就会遗失,因为它尚未放入到 我们自动管理资源的shared_ptr内部.也就不可能被释放了.
  • 解决的方法也很简单:使用分离的语句1,创建Widget. 2,讲Widget放入智能指针内部
    std::tr1::shared_ptr<Widget> pw(new Widget);
    
    processWidget(pw, priority());
    

Chapter 4: 设计与声明

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

  • "促进正确使用"的办法包括接口的一致性,以及与内置类型行为的兼容
  • "阻止误用"的办法包括建立新类型, 限制类型操作, 束缚对象值, 以及消除客户的资 源管理责任

Item 19: 设计class犹如设计type

  • 在设计每个class的时候,回答以下问题:
    • 新type对象应该如何创建和销毁
    • 对象的初始化和对象的赋值操作有什么差别
    • 新的type的对象如果被passed by value意味着什么 (copy构造函数用来定义一个 type的pass-by-value如何实现)
    • 什么是新type的"合法值"
    • 需要继承什么
    • 新type需要什么样的转换
    • 什么样的操作符和函数对次type而言是合理的
    • 什么样的标准函数应该被驳回(声明为private)
    • 函数该采用public,private还是protected
    • 什么是新type的"未声明接口"
    • 新的type有多一般化,如果一般化的够多,比如integer, string等都要一般化的化, 就要考虑generic编程了
    • 你真的需要一个新的type么

Item 20: 宁以pass-by-reference-to-const替换pass-by-value

  • pass-by-reference-to-const效率高的多, 因为没有任何构造或者析构函数的调用(没 有新的对象被创建)
  • pass-by-reference-to-const不仅仅是效率高,而且可以避免对象切割(slicing)问题: 当一个derived对象以by value的方式传递到一个并被视为base对象, 那么base class的 拷贝构造函数会被调用而"造成此对象的行为像个derived class对象"的那些特质化的性质 全被切割掉了
  • 不用pass-by-reference-to-const的唯一情况是1)内置类型2)STL的迭代器和函数对象

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

  • 说到返回值,在c++中是可以返回一个对象的.方法入下,其实这就是最正常的写法
    inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
        return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    }
    
  • 在Item20的影响下,可能会想到返回值"也用reference", 其实这是非常危险的, 因为reference 不是单独存在的,一个reference一定有一个"实体", 比如下面这个例子. 返回值reference其实 对应的是一个local的result对象, 一旦从函数返回,那么local的result就被销毁了.返回一个 已经销毁的,不存在的"实体"的reference是非常可怕的
    const Rational& operator* (const Rational& lhs,
                               const Rational& rhs) {
        //警告!糟糕的代码!!
        Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
        return result;
    }
    
  • 我们稍微改变一下,在heap里面创建一个对象,然后返回,例子如下.
    const Rational& operator* (const Rational& lhs,
                               const Rational& rhs) {
        Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d*);
        return *result;
    }
    
  • 这个例子其实也是不行的,因为释放内存的责任是调用者的,那就有可能会因为无法找到heapd的句柄, 无法释放内存,而发生内存泄漏. 比如下面, 调用了两次operator*, 那么就要释放两次内存,但是 却没有办法让operator* 使用者进行delete调用, 因为没有合理的办法让他们取得operator*返 回的references背后隐藏的那个指针.
    Rational w, x, y, z;
    w = x * y *z;
    

Item 22: 讲成员变量声明为private

  • 切记将成员变量声明为private, 因为这样有很多好处:
    • 可以实现更精确的访问控制:如果是public的话,谁都可以访问,但是private你却可以设置成: 只读,不准访问,读写访问,只写访问, 等等
    • 一个变量声明为public那就意味着永远不可改变, 声明为private给了class作者有了更大的 实现弹性
  • protected其实是历史一流, 和public一样,没有任何的封装性.

Item 23: 宁以non-memeber, non-friend替换member函数

  • class的member函数和friend函数在访问private数据方面的权利是一样的,所以具有一样的, 所以封装效果是一样的.这里比较的是non-member, non-friend函数和member函数. 我们的 原则是:如果一个member函数可以用non-member,non-friend函数来替代,那么就替代
  • 下面是一个webBroswerStuff的例子, 这个例子有三个member函数,其中clearBrowser其实 可以用一个non-member, non-friend函数代替,这样就是增加了class的封装性
    class WebBrowser{
        void clearCache();
        void clearHistory();
        void removeCookies();
        void clearBrowser() {
            clearCache();
            clearHistory();
            removeCookies();
        }
    };
    
  • 用non-member, non-friend函数代替可以代替的member函数,并且使用一个namespace,是 c++常用的方法
    namespace WebBrowserStuff{
        class WebBrowser{
            void clearCache();
            void clearHistory();
            void removeCookies();
        };
        void  clearBrowser(WebBrowser& wb){
            wb.clearCache();
            wb.clearHistory();
            wb.removeCookies();
        }
    }
    
  • 话说,namespace是STL里面组织代码的主要方式,因为namespace可以跨越不同的文件 (class不行), std命名空间分布在数十个头文件(<vector>, <algorithm>, <memory> 里面等等), 用户想用某个就include某一个头文件. (class就不行,class无法分割)

Item 24: 若所有参数皆需类型转换,请为此采用non-member函数

  • 这个item其实是讲如何在c++中把事情做对,而不是怎样做更好. 比如,一开始把operator* 实现成成员变量的化,碰到result = 2 * oneHalf, 必然会编译器报错,然后修改也不迟.
  • 错误的operator* 实现
    class Rational {
    pulic:
        //...
        // NOT work for result = 2 * oneHalf
        const Rational operator*(const Rational& rhs) const;
    };
    
  • 正确的operator*是一个non-member, 借用numberator()和denominator()函数读取 private值,而不是设计成有元函数
    class Rational {
        //...
    };
    
    const Rational operator*(lhs.numberator() * rhs.numberator(),
                             lhs.denominator() * rhs.denominator()) {
        return Rational(lhs.numberator() * rhs.numberator(),
                        lhs.denominator() * rhs.denominator());
    }
    

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

  • 当std::swap对你的类型效率不高的时候,提供一个swap成员函数, 并确定这个函数不 抛出异常
  • 如果你提供了一个member swap,也应该提供改一改non-member swap来调用前者,对于 classes(而不是templates), 也请特化std::swap
  • 对自己特例化的swap调用方式是先using, 再调用swap,而且没有直接写std::swap,这 源于编译器对namespace的名字查找时, 遵循的一系列规则
    class Widget {
    public:
        //...
        void swap(Widget& other) {
            using std::swap;
            swap(pImpl, other.pImpl);
        }
        //...
    };
    namespace std {
        template<>
        void swap<Widget>(Widget& a, Widget& b) {
            a.swap(b);
        }
    }
    

Chapter 5: 实现

Item 26: 尽可能延后变量定义式出现时间

  • 在老的c语言规范里面(C89/90)里面, 变量只能定义在block开始的地方.注意!!不是 function开始的地方, 即便是C89/90, 把所有变量都定义在函数开始的地方是非常 不明智的, 因为这样会导致逻辑不清晰, 变量作用域模糊的代码
  • 对于C++来说,变量"直到需要的时候"才定义,有更多的好处:
    • 变量作用域更清晰, 下面的成本如下(但是方法B更清晰,作用域更准确):
      1. 方法A : 1个构造函数 + 1个析构函数 + n个赋值操作
      2. 方法B : n个构造函数 + n个析构函数
      // 方法A, 定义于循环外
      Widget w;
      
      for (int i = 0; i < n; ++i) {
          w = 取决于i的某个值;
          //...
      }
      
      // 方法B, 定义于循环内
      for (int i = 0; i < n; ++i) {
          Widget w(取决于i的某个值);
          //...
      }
      
    • 防止"定义了不使用"(由于异常的存在,当然异常现在不推荐使用)
      std::string encryptPassword(const std::string& password) {
          using namespace std;
          string encrypted;
      
          if (password.length() < MinimumPasswordLength) {
              throw logic_error("Password is too short");
          }
      
          //...
          return encrypted; //May NOT be used!
      }
      
    • 直到真正需要的时候,就意味着大多数情况下这个变量已经有了一个靠谱的初始化 值,那么就可以用"直接在构造的时候指定初始值", 这效率比起"通过default构造 函数构造出一个对象,然后对它复制"效果好
      std::string encryptPassword(const std::string& password) {
          // 检查长度
          std::string encrypted(password);
      
          encrypt(encrypted);
          return encrypted;
      }
      

Item 27: 尽量少做转型动作

  • c语言风格的转型有以下两种(其实也只是小括号摆放位置不同而已),因此我们称之为"旧 式转型"(old-style casts)
    (T)expression   //将expression 转型为T
    T(expression)   //将expression 转型为T
    
  • c++有四种新型的转型:
    • const_cast 将对象的常量性去除(cast away the constness)
    • dynamic_cast 执行"安全向下转型" (safe downcasting), 也就是用来决定某对象 是否归属继承体系中的某个类型. 换句话说就是,你想在一个你认定了肯定是derived class 对象身上执行derived class操作函数, 但是你手上却只有一个"指向base" 的pointer或者reference,你只能依靠他们来处理,所以你想到了把它dynamic_cast 到"你认为的类型"
    • reinterpret_cast意图执行低级转型, 由于取决于编译器,也即表示它不可移植
    • static_cast用来强迫隐式转换(implicit conversions).比如将non-const对象 转换为const对象, 或者将int转换为double. 但是无法将const转换为non-const, 只有const_cast才能做到
  • c++规则设计目的之一就是, 保证"类型错误"绝对不可能发生.理论上如果你"很干净"的 通过编译,就表示它并不企图在任何对象身上执行任何不安全,无意义的操作. 这是一个 极具价值的保证,不要轻率的放弃.
  • c++的新型转型语法更受欢迎, 因为:
    • 很容易在代码中被辨识出来(相比较老的类型转换就一个括号), 也就容易查找到问题
    • 新型类型转换分成了四种,每种功能都限定在一个比较小的范围内,便于让编译器找到 问题
  • 虽然c++的转型非常的好,但是有时候我们还是会使用"旧式转型",比如调用一个explicit 构造函数,把一个对象传递给一个函数的时候.很多时候这也是c++里面推荐的,唯一使用"旧 式转型"的情况(其实也可以使用新式转型):
    class Widget {
    public:
        explicit Widget(int size);
        // ...
    };
    
    void doSomeWork(const Widget& w);
    // 以一个int加上"旧式函数风格"的转型动作创建了一个Widget
    doSomeWork(Widget(15));
    // 以一个int加上"新式C++风格"的转型动作创建了一个Widget
    doSomeWork(static_cast<Widget>(15));
    
  • 转型不是简单的告诉编译器把某种类型视为另一种类型, 它往往会零编译器编译出"运行期间 执行的代码",比如下面的例子,由于底层机制的不同,把int转换成double,几乎肯定会产生一 些代码
    int x, y;
    // ...
    double d = static_cast<double>(x) / y;
    
  • 在继承体系中使用cast,也会产生"运行时代码", 因为c++支持多重继承,所以一个derived 对象可能拥有两个地址(Base1指向时候的地址,或者Base2指向时候的地址). 当然,有些实现 中单继承一会出现一个Derived的对象拥有两个地址的奇怪景象!
    #include <iostream>
    using namespace std;
    
    class B1 {
        int i;
    };
    
    class B2 {
        int i;
    };
    
    class D : public B1, public B2 {
        int i;
    };
    
    int main(int argc, char *argv[])
    {
        D aD;
        B1* pb1 = &aD;
        B2* pb2 = &aD;
    
        cout << &aD << endl;
        //////////////////////////////////////////////////////
        // There's no possible way for the B1 sub-object to //
        // have the same address as the B2 sub-object.      //
        //////////////////////////////////////////////////////
        cout << pb1 << endl;
        cout << pb2 << endl;
        return 0;
    }
    
    ///////////////////////////////////////////////
    // <==========running the test.out=========> //
    //                                           //
    // 0x28fecc                                  //
    // 0x28fecc                                  //
    // 0x28fed0                                  //
    //                                           //
    // <==========test.out ends here===========> //
    ///////////////////////////////////////////////
    

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

  • 假设我们要有一个"矩形"类, 其成员是两个点, 左上和右下.那么我们就有如下的类的 基础代码:
    class Point {
    public:
        Point(int x, int y);
        // ...
        void setX(int newVal);
        void setY(int newVal);
    };
    
    struct RectData {
        Point ulhc; // ulhc = "upper left-hand corner"
        Point lrhc; // lrhc = "lower right-hand corner"
    };
    
    class Rectangle {
        // ...
    private:
        std::tr1::shared_ptr<RectData> pData;
    };
    
  • 函数需要对外公开"左上"和"右下"的数值, 所以有下面两个函数.
    class Rectangle {
    public:
        // ...
        Point& upperLeft() const {
            return pData->ulhc;
        }
        Point& lowerRight() const {
            return pData->lrhc;
        }
    };
    
  • 这两个函数可以通过编译,确是两个个错的函数,因为他们破坏了封装性!不是声明为了 const了么, 只有const对象可以调用这两个函数,为什么还破坏了封装性? 原因请看 下面的例子.虽然rec 是const的,不可更改,但是因为你返回了内部Point成员变量的 reference,可以利用这个reference来改动内部数据
    Point coord1(0.0);
    Point coord2(100, 100);
    const Rectangle rec(coord1, coord2);
    
    // rec is const, but rec.upperLeft() is Point&, and
    // can be changed
    rec.upperLeft().setX(50);
    
  • 上面的例子是返回了一个reference,其实指针或者迭代器和reference的效果是一样 的, 它们三个都叫做handles(号码牌,用来取得某个对象)
  • 上面的例子,稍加修改(返回const reference)就可以保证其封装性.
    class Rectangle {
    public:
        // ...
        const Point& upperLeft() const {
            return pData->ulhc;
        }
        const Point& lowerRight() const {
            return pData->lrhc;
        }
    };
    
  • 上面这种实现方法, 虽然勉强可以使用(operator[]就是这么实现的.),但深究起来, 还是会有dangling handles(空悬号码牌的问题), 比如下面的例子,GUIObject作 为一个参数传入,求它的"左上右下"矩形数据.(boundingBox(*pgo))整个这一部分 会产生一个临时的Rectangle对象tmp, 随后upperLeft,作用于temp身上, 返回一 个指向temp内部成分的reference(也就是temp的Point).这个语句过后, temp 就会被销毁,间接导致temp的Point也会析构, 也就是说pUpperLeft一开始就指向 了一块已经析构了的内存地址!
    class GUIObject {
        // ...
    };
    
    // 以 by value方式返回, 一定要声明为const, 可以防止不小心被'='赋值
    const Rectangle boundingBox(const GUIObject& obj);
    
    // 用户可能会如下使用, 因为临时对象的产生,所以会导致dangling handles
    GUIObject* pgo;
    // ...
    const Points* pUpperLeft = &(boundingBox(*pgo).upperLeft());