zoukankan      html  css  js  c++  java
  • Effective C++ 一些记录和思考

    Effective C++

    • Iter 3 - 尽可能使用 const
      • 一个反逻辑的 bitwise const

        class Text {
            ...
            char& operator[](std::size_t pos) const { return text[pos]; }
        private:
            char *text;
        };
        

        . 在 clang 3.8 上编译失败,编译器已经修复这个反逻辑的问题。const 成员函数只能返回 const char& 类型的变量,这就保证了对象不能被修改。

      • 当存在 const 成员函数和 non-const 成员函数的时候,可先实现 const 成员函数,non-const 成员函数通过调用 const 函数来实现,具体做法为先将对象转换为const类型(static_cast<const T&>()),调用const成员函数,再去除const属性(const_cast<T&>())

        // const
        const char& operator[](std::size_t pos) const { return text[pos]; }
        // non-const
        char& operator[](std::size_t pos) {
            return const_cast<char&>(static_cast<const char&>(*this)[pos]);
        }
        

        . 这样做的好处是 const 成员函数保证了数据的不变,减少代码量,缺点是转换的性能缺失。


    • Item 4 - 确定对象使用前已先被初始化
      • 勿混淆赋值和初始化
      • 对象的成员变量的初始化动作发生在进入构造函数 {/* body /*} 之前,使用 member initialization list 初始化对象是一种比较好的做法
      • 内置type(int char)默认为 0
      • 初始化顺序为其声明次序
      • non-local static 编译器对不同编译单元的 non-local static 的编译顺序是随机的
        • 解决的办法是不直接访问 non-local static 对象,而是使用一个函数包装在其内部声明为 local static 对象并且返回改对象引用
        • static 为声明在其作用域内的全局变量,比如在函数内,只要该函数栈没有被回收该变量便一直存在。在调用该函数时,该 local static 对象会在首次访问时被初始化

    • Item 5 6 - C++ 隐式实现和调用的函数
      • 编译器在没有 默认构造函数,copy构造函数,赋值重载操作符 时会自动生成,且为 public
      • 阻止调用 copy构造函数,赋值重载操作符函数
        • C++11 之前,实现一个基类然后继承
        class UnCopyable {
        public:
            UnCopyable() {}
            ~UnCopyable() {}
        private:
            UnCopyable(const UnCopyable&);
            UnCopyable& operator=(const UnCopyable&);
        };
        
        class Impl : private UnCopyable {};
        
        • C++11 使用 delete 关键字
        class Impl {
        public:
            Impl(const Impl&) = delete;
            Impl& operator=(const Impl&) = delete;
        };
        

    • Item 7 - 为多态基类声明 virtual 析构函数
      • C++ 11 中子类的可以用 override 来覆盖基类的 virtual 函数(析构函数可不用)
      • 由于virtual table 和virtual table pointer的存在,会使的class 的大小膨胀。
      • 尽量不继承non-virtual 析构函数的类,C++11后可以用final关键字修饰类而禁止被继承
      • 构造函数和析构函数执行方式相反,构造函数是从最顶层的基类开始执行
    • Item 8 - 别让异常逃离析构函数
    • Item 9 - 不在构造和析构函数内调用 virtual 函数
      • 在多态基类中构造函数调用虚函数,子类对象构造时执行的基类构造函数调用的基类的对象,而非子类对象,析构函数相同,这样虚函数就变成普通的函数了

    • Item 10 - 令 operator= 返回一个 reference to *this
    • Item 11 - 在 operator= 中处理“自我赋值”
      • 只看正确性,一般的做法是先保留一个副本,再进行赋值,最后删除副本,这样在赋值操作失败的时候,不会丢失原来的数据
    • Item 12 - 复制对象时勿忘其每一个部分
      • 一个较容易忽略的地方,子类在copy构造的时候易忽略对基类对象变量进行copy,而这时默认调用了基类的default构造函数
      • 不要在一个构造函数内调用另外一个构造函数,可行的做法是将共同的机能放进一个普通函数中,在两个构造函数内调用

    • Item 13 - 以对象管理资源 Resource Acquistion Is Initialization, RAII
      • auto_ptr 在标准中已经废除,用 unique_ptr 替代
      • shared_ptr 互相引用的问题,可以用 weak_ptr 解决
    • Item 14 - 在资源管理类中注意copying行为
    • Item 15 - 在资源管理类中提供对原始资源的访问
      • 在只能指针中提供原始指针即可
    • Item 16 - 成对使用newdelete
      • 尽量使用只能指针替代 new
      • 数组可用 std::vector 或者 C++11 中的 std::array 代替
    • Item 17 - 以独立语句将 new对象置入指针指针
      • 分离创建和使用的过程

    • Item 18 - 让接口易于使用,不易被误用
      • 类的设计通常应与内置类型的逻辑保持一致
      • 函数的有资源相关的操作时可以考虑使用智能指针来处理
    • Item 19 - 设计 class 如同 type
    • Item 20 - 宁以 pass-by-reference-to-const 替换 pass-by-value
      • 涉及到底层的处理,编译器对待指针和自定义类型(class)的处理可能会不一样
      • 对于内置类型、STL的迭代器和函数对象,pass-by-value 更合适
    • Item 21 - 必须返回对象时,勿返回reference
      • clang 3.8 可以发现这个问题,并发出警告
        warning: reference to stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
        
    • Item 22 - 将成员变量声明为 private能够访问 private的函数只有 friend函数和成员函数
      • 将成员变量隐藏在函数接口后,可以为实现提供弹性
    • Item 23 - 宁以 non-member、non-friend 替换 member 函数
      • 将 non-member 函数与当前 class 声明在同一个命名空间内
    • Item 24 - 若所有参数皆需类型转换,请采用 non-member 函数
      • 只有当参数位于参数列内,这个参数才是隐式类型转换的合格者
    • Item 25 - 考虑写出一个不抛出异常的 swap 函数
      • 全特化版本,与 STL 保持一致性,这样 Widget 对象就可以正常的调用 swap 了
      class Widget {
      public:
          void swap(Widget& other) {
              using std::swap;  
              swap(pImpl, other.pImpl);
          }
      private:
          WidgetImpl *pImpl;
      };
      namespace std {  // std
          template <>
          void swap<Widget> (Widget& a, Widget& b) { a.swap(b); }
      }
      
      • 偏特化一个 function template 时,惯用手法是简单的为它添加一个重载版本
      namespace WidgetFpp {  // 这里不是添加到 std 中了
          class WidgetImpl {};
          class Widget {};
          template<typename T>
          void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); }
      }
      
      • 一般经验
        • 能使用 std::swap 就不去造轮子
        • pimpl 手法的,或类似的
          1. 提供一个 public swap,置换该类型的两个对象
          2. 在当前 class 或者 template 所在的命名空间内提供一个 non-member swap,并调用上述成员函数
          3. 如果当前是 class 而不是 class template,为class特化 std::swap,并调用 swap 成员函数
        • 在调用 swap 前,使用 using std::swap, 自动匹配合适的那个 swap 函数

    实现

    • Item 26 - 尽可能延后变量定义是的出现时间
      • 以“具明显意义之初值”将变量初始化,还可以附带说明变量的目的
    • Item 27 - 尽量少做转型动作
      • 书中对 Window::OnResize() 的分析,这里用过代码看结果,L18 这里转型后调用的OnResize()的*this是一个副本,L19 直接调用基类 OnResize能够得到正确的结果
      7 class Window {
      8   public:
      9     virtual void OnResize() {
      10         a = 3;
      11     }   
      12     int a;
      13 };  
      14 
      15 class SpWindow : public Window {
      16   public:
      17     void OnResize() override {  // C++ 11
      18         // static_cast<Window>(*this).OnResize();  // 这里 a = 0
      19         Window::OnResize();  // a = 3
      20     }   
      21 };
      
      • 谨慎使用 dynamic_cast
    • Item 28 - 避免返回handles指向对象内部成分
      • 书中的例子会造成 bitwise constness, 见 Item 3, 但是 clang 3.8 上这些代码已经不能编译通过了
    • Item 29 - 值得为“异常安全”花费精力
      • 异常安全的要求
        • 不泄露任何资源
        • 不允许数据败坏
      • 资源泄漏用对象管理,如锁
      • 单个函数的安全保证,copy-and-swap
      • 多个函数“连带效应”, 对每一个函数都实现安全保证
    • Item 30 - 了解 inline
      • inlinetemplate 通常被定义于头文件内。 是因为通常而言,inlining 和 template 是编译期的行为,编译器必须知道被调用函数的本体
      • 改变程序需要重新编译,而不能像普通的函数直接链接即可
    • Item 31 - 将文件间的编译关系降至最低
      • Handle class 和 Interface class 解除接口和实现之间的耦合关系,降低文件间的编译依赖
    • Item 32 - 避免遮掩继承而来的名称
      • derived class 继承了 base class 内的所有东西,实际上的运行方式是 derived class 作用域被嵌套在 base class 作用域内

    继承与面向对象设计

    • Item 33 - 确认 public 继承塑造出 is-a 关系

      • public 继承适用于 base class 对象上的每件事情也能够作用于 derived class 对象上
      • base class 和 derived class 中有相同的函数名称时,base class 中的函数就被覆盖了,virtual 函数也是这样,但是有运行时多态,但是普通函数当想继承的时候就出问题了
      • 在 derived class 加入 using base::func; 后相同参数和返回还是会覆盖 base class 中的对象
    • Item 34 - 区分接口继承和实现继承

      • C++ 的隐式规则太多,需要继承的东西干脆显示化,减少出错
    • Item 35 - 考虑 virtual 函数以及以外的选择

      • non-virtual interface, NVI 手法也是将真正需要的改变的部分分离出来放进一个函数内,而虚函数内先处理前置条件,再调用该函数
      7  class Game {
      8    public:
      9      int health() const {
      10         std::cout << "Game::health()
      ";
      11         int ret = doHealth();
      12         std::cout << "Game:: ret " << ret << '
      ';
      13         return ret;
      14     }
      15   private:
      16     virtual int doHealth() const {
      17         std::cout << "Game:: doHealth()
      ";
      18         return 1; 
      19     }   
      20 };  
      21 
      22 class LOL : public Game {
      23   private:
      24     virtual int doHealth() const {
      25         std::cout << "LOL doHealth()
      ";
      26         return 2; 
      27     }   
      28 };  
      29 
      30 int main() {
      31     LOL lol;
      32     lol.health();
      33 } 
      // 可以使用到 base class 默认的析构函数,参考 Item39,用 private 作种实现方式。
      // LOL 未定义虚函数的时候,结果为
      // Game::health()
      // Game:: doHealth()
      // Game:: ret 1
      
      // 定义虚函数后
      // Game::health()
      // LOL doHealth()
      // Game:: ret 2
      
      • strategy 策略使用函数指针替换虚函数,这样每个对象都可以更灵活的有自己的特定处理函数,和运行时可以改变函数
      7 class GameCharacter;
      8 class HealthCalcFunc {
      9   public:
      10     virtual int calc(const GameCharacter&) const {
      11         return 3; 
      12     }   
      13 };  
      14 
      15 HealthCalcFunc defaultHealthFunc;
      16 
      17 class GameCharacter {
      18   public:
      19     explicit GameCharacter(HealthCalcFunc* phcf = defaultHealthFunc) : pHealthCalc(phcf) {}
      20     int healthValue() const {
      21         return pHealthCalc->calc(*this);
      22     }   
      23     
      24   private:
      25     HealthCalcFunc* pHealthCalc;
      26 };  
      27 
      28 int main() {
      29     GameCharacter gc;
      30     gc.healthValue();
      31 } 
      
    • Item 36 - 绝对不重新定义继承而来的 non-virtual 函数

      • 继承而来的 non-virtual 函数是静态绑定,父类指针只能表现出父类对象的行为, 换而言之,这个在编译期就确定好了
    • Item 37 - 绝不重新定义继承而来的缺省参数值

      • 缺省参数值都是静态绑定的,应该覆盖的是 virtual 函数,而它为动态绑定的
      struct B { 
          virtual void mf(int i = 1) { std::cout << "B::mf " << i << "
      "; }
          // 不改变 virtual 函数的时候,可以使用 NVI 手法,实现一个外围函数,令子类调用它
      };
      struct D : public B { 
          void mf() { std::cout << "D::mf
      "; }
      };
      int main() {
          D d;
          B *pb = &d; 
          pb->mf();  // B::mf
      }
      
    • Item 38 - 通过复合 (composition) 塑造或根据某物实现出 has-a

      • 在应用域,复合意味着 has-a,在实现域复合意味着 is-implemented-in-terms-of
    • Item 39 - 谨慎使用 private 继承

      • private 继承作为一种实现技术,意味着 is-implemented-in-terms-of, base class 为实现细节
      • 当两个class不存在 is-a 关系时,其中一个需要访问另一个的 protected 成员,或者需要 重新定义 其一或者多个vitual函数,可以考虑private继承
    • Item 40 - 谨慎使用多重继承

      • 多重继承易造成歧义(ambiguity)
      • 干脆直接禁用了

    模板与泛型编程

    • Item 41 - 了解隐式接口和编译期多态
    • Item 42 - 了解 typename 的双重意义
      • 声明 template 参数时,typenameclass 没有区别
      • 利用 typename 标示嵌套从属类型名称(形如 C::const_iterator),但不得在 base class lists 或者 member initialization list 以他作为 base class 标示
    • Item 43 - 学习处理模板化基类内的名称
      • 模板的具现是在函数使用的时候
      • 编译器遇到 class template 继承的类时,并不知道该类的定义是什么
        • 所以只能通过 this 指针指向 base class 对象,具现base class
        • 在该定义域内声明需要访问的 base class 函数
        • 直接调用base class 中的代码
        struct CompanyA {
            void sendClear(const std::string& msg) {}
        };
        
        struct CompanyB {
            void sendClear(const std::string& msg) {}
        };
        
        struct MsgInfo {};
        
        template <typename Company>
        class MsgSend {
        public:
            void send(const MsgInfo& info) {
                std::string msg = "foo";
                Company c;
                c.sendClear(msg);
            }
        };
        
        template <typename Company>
        class LogMsg : public MsgSend<Company> {
        public:
            // using MsgSend<Company>::send;  // slove - way 2: tell complier, send() is defined in base class
            void sendMsg(const MsgInfo& info) {
                // send(info);  // base: cant pass complier
                // this->send(info);  // slove - way 1: could pass complier
                // MsgSend<Company>::send(info);  // slove - way 3: same as way 2
            }
        };
        
        int main() {
            MsgInfo mi;
            LogMsg<CompanyA> ma;
            ma.send(mi);
        }
        
    • Item 44 - 将与参数无关的代码抽离 template
      • 模板只有在使用时被具现,不同类型的会被具现出不同的代码,引起代码膨胀,有点儿类似宏的用法
      • 或者参数使用相同的二进制表述,如指针
    • Item 45 - 运用成员函数模板接受所有兼容类型
      • 模板具现化后的 base class 和 derived class 为两个独立的类了
      • member function template 成员函数模板可接受所有兼容类型参数,也就是泛化了构造函数
      • 为阻止编译器默认生成copy构造函数和重载=,在已经有泛化版本的情况下也须自己定义这些函数
    • Item 46 - 需要类型装换时须为模板定义非成员函数
      • 模板函数在进行实参类型推导的时候,不允许进行参数的隐式转换(隐式转换是发生在函数调用的时候),而类型推导的时候是先根据已经有的定义确定函数原型
      • 为了转换可以使用 friend感觉变复杂了
    • Item 47 - 使用 traits classes 表现类型信息
      • 整合重载技术,使得traits class 有可能在编译期对类型执行if...else测试
      • 使用 traits class
        • 实现一组重载函数,或者函数模板,作为实现部分
        • 建立一组驱动(控制)函数或者函数模板,调用以上重载函数
    • Item 48 - 认识模板元编程(编写模板程序在编译期执行的过程)

    一些新的知识点

    • pimpl, 以指针指向一个对象,内含真正数据
    • 模板全特化、偏特化的一个博客 https://blog.csdn.net/m_buddy/article/details/72973207
    • copy and swap, 在副本上做修改,直到修改成功时再进行写入,在陈硕的muduo书里面讲到的,用swap在临界区外释放资源
  • 相关阅读:
    利用Mono.Cecil动态修改程序集来破解商业组件(仅用于研究学习) 转 武胜
    探讨C语言中的回调函数
    BusyBox
    ISC的DHCP服务器
    exchange 2007 安装
    strcpy和memcpy的区别 | strcpy和strncpy的区别
    爱不是什么
    编译libnl时候的问题
    Linux中find常见用法示例
    ubuntu firefox flash 插件安装
  • 原文地址:https://www.cnblogs.com/shuqin/p/10224028.html
Copyright © 2011-2022 走看看