zoukankan      html  css  js  c++  java
  • 【C++】《Effective C++》第一章

    第一章 让自己习惯C++

    C++是一个威力强大的语言,带着众多特性,但是在你可以驾驭其威力并有效运用其特性之前,你必须先习惯C++的办事方式。

    条款01:视C++为一个语言联邦

    如今的C++已经是个多重范型编程语言,它同时支持过程形式面向对象形式函数形式泛型形式元编程形式

    • 由于C++的能力使其成为一个无可匹敌的工具,可能会引发某些迷惑:所有“适当用法”似乎都有例外

    • 将其视为一个由相关语言组成的联邦而非当一语言。在其某个次语言中,各种守则与通例倾向简单、直观易懂、并且容易记住。主要的次语言有:

      • C
      • Object-Oriented C++
      • Template C++
      • STL
    • C++高效编程守则视情况而变化,取决于你使用C++哪一部分。

    条款02:尽量以const,enum,inline替换#define

    通俗的讲,让编译器替换预处理器比较好,因为#define不被视为语言的一部分。

    const替换#define

    #define ASPECT_RATIO 1.653
    
    # 替换成
    
    const double AspectRatio = 1.653;
    
    • 这样做的理由:

      • 调试的需要:#define的记号会被预处理器移走,记号名称肯尼没进入记号表内。因此当#define的宏名称获得一个编译错误时,可能会引起困惑,浪费大量时间追踪错误。而AspectRatio肯定会被编译器看到。
      • 更小的代码量:对浮点数而言,使用常量可能比使用#define导致较小量的代码,因为预处理"盲目地将ASPECT_RATIO替换成1.653",这可能导致目标代码出现多份1.653
    • 常量替换的两种特殊情况:

      • 定义常量指针时,由于常量定义式通常被放在头文件内,因此有必要将指针(注意不是指针所指之物)声明为const

        const char* const authorName = "Scott Meyers";
        
        # 更好的做法:string对象比char*更和时宜
        
        const std::string authorName("Scott Meyers");
        
      • class专属常量需要声明在class内部,并且被class使用。而为确保此常量至多只有一份实体,必须让它成为一个static成员。

        // 通常定义在头文件
        class GamePlayer {
        private:
          static const in NumTurns = 5; // 常量声明式
          int scores[NumTurns]; // 使用该常量
        };
        
        const int GamePlayer::NumTurns; // 某些编译器可能不支持类内初始化,因此需要在类外设初值
        

    enum替换#define

    某些编译器可能不支持类内初始化,可改用所谓的"the enum hack"的补偿做法。因为一个属于枚举类型的数值当作int被使用。

    class GamePlayer {
    private:
      enum { NumTurns = 5};
      int scores[NumTurns];
    };
    
    • enum hack的行为某方面所比较像#define而不像const
    • enum hack更加实用,它是模板元编程的基础技术。

    inline替换#define

    #define实现宏看起来像函数,并且不会导致函数调用带来的开销,但是可能引发错误,且可读性降低。

    #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
    
    int a = 5, b = 0;
    CALL_WITH_MAX(++a, b);  // a被累加两次
    CALL_WITH_MAX(++a, b+10); // a被累加一次
    
    • 使用inline函数可以避免上面的问题,而且inline还能实现一个“class内的private inline函数”。
    template<typename T>
    inline void callWithMax(const T& a, const T& b) {
      f(a > b ? a: b);
    }
    

    请记住

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

    条款03:尽可能使用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; // const pointer, non-const data
    const char* const p = greeting; // const pointer, const data
    
    • 如何区分上面,主要看const的位置:
      • 如果关键字const出现在星号左边,表示被指物是常量。
      • 如果关键字const出现在星号右边,表示指针本身是常量。
      • 如果关键字const出现在星号两边,表示被指物和指针本身都是常量。

    const修饰函数

    • 修饰参数时,和修饰一般变量相同。
    • 修饰返回值时,可以降低因客户错误而造成的意外,且又不至于放弃安全性和高效性。
    class Rational {};
    Rational a, b, c;
    
    if(a * b = c) {  // 做一个比较操作,但是客户用法错误
      // ...
    }
    
    // 如果a和b都是内置类型,上面的用法当然错误,所以为了避免这种情况,应该:
    
    const Rational::Rational operator* (const Rational& lhs, const Rational& rhs);
    

    const修饰成员函数

    • 这样做有两个好处:
      • 增强可读性,使得接口容易被理解,知道哪个函数可以改动对象内容而哪一个不行。
      • const修饰的成员函数可以作用于const对象,可以改善程序效率(根本办法是以pass by reference-to-const方式传递对象)。
    • 一个需要注意的问题:C++对常量性的定义是二进制位常量性(bitwise constness),即const成员函数不应该修改对象的任何成员变量。因此,如果成员变量是一个指针,那么不修改指针所指之物,则符合bitwise constness。但是不从bitwise constness的角度,也算是修改了对象。
    class CTextBlock {
    public:
      char& operator[](std::size_t pos) const { // `bitwise constness`声明,但其实不恰当
        return pText[pos];
    private:
      char* pText;
      }
    };
    
    const CTextBlock cctb("Hello");
    char* pc = &cctb[0];
    
    *pc = 'J';  // cctb此时为"Jello"
    
    • 解决这个问题的方法是使用mutable,它能释放掉non-static成员变量的bitwise constness约束。
    mutable std::size_t textLength;
    mutable bool lengthIsValid;
    

    constnon-const成员函数中避免重复

    class TextBlock {
    public:
    
      const char& operator[](std::pos) const {
        // ...
        return text[pos];
      }
    
      char& operator[](std::size_t pos) {
        // ...
        return text[pos];
      }
    
    private:
      std::string text;
    };
    
    // 可以发现上面两个函数实现的功能大同小异,但是有很多重复
    // 改进:做一个转型动作
    class TextBlock {
    public:
    
      const char& operator[](std::pos) const {
        // ...
        return text[pos];
      }
    
      char& operator[](std::size_t pos) { // 只调用const operator[]
       return const_cast<char&>(
         static_cast<const TextBlock&>(*this)[pos];
       )
      }
    
    private:
      std::string text;
    };
    
    • 以上转型动作实际上做了两个操作:
      • *this添加const(使得接下来能调用const operator[]版本)。
      • const operator[]的返回值中移除const
    • 注意: const成员函数承诺绝不会改变其对象的逻辑逻辑状态,non-const成员函数则没有这种承诺。

    请记住

    • 将某个东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
    • 编译器强制实施二进制常量性(bitwise constness),但你编写程序时应该使用"概念上的常量性"(conceptual constness)。
    • constnon-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

    条款04:确定对象被使用前以先被初始化

    • 读取未初始化对象的后果: 它会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可以让你的程序终止运行。更可能的情况是读入一些半随机bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多不愉快的调试过程。

    按对象的类型划分

    • 对于内置类型的对象,永远在使用前初始化。
    • 对于类类型的对象,初始化责任落在构造函数身上,即确保每一个构造函数都将对象的每一个成员初始化。
    • 重要: 由于类类型成员的初始化动作发生在构造函数本体之前,所以构造函数使用成员初始化列表替换赋值动作更好。因为赋值动作首先会调用default构造函数,然后再调用copy assignment操作符,而使用成员初始化列表之后只调用一次copy构造函数,显然更加高效。
    class PhoneNumber {
      //...
    };
    
    class ABEntry {
    public:
      ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
    
    private:
      std::string theName;
      std::string theAddress;
      std::list<PhoneNumber> thePhones;
      int numTimesConsulted;
    };
    
    ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) {
      theName = name; // 这些都是赋值而非初始化
      theAddress = address;
      thePhones = phones;
      numTimesConsulted = 0;
    }
    
    // 改进:使用成员初始化列表
    ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones):theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) { }
    

    按对象的作用域与生命周期划分

    • non-local static对象:C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。
      • global对象
      • 定义于namespace作用内的对象
      • classes内、file作用域内被声明为static的对象
    • local static对象:函数内的local static对象会在“该函数被调用期间、首次遇到该对象的定义式”时被初始化。
      • 函数内被声明为static的对象
    • 因此,如果一个non-local static对象的初始化依赖于另外一个non-local static对象的初始化,那么可能造成错误。
    class FileSystem {  // 来自你的程序库
    public:
      std::size_t numDisks() const;
    
    };
    extern FileSystem tfs;  // 预备给客户使用的对象
    
    class Directory { // 由程序库客户建立
    public:
      Directory(params);
    
    };
    Directory::Directory(params) {
      std::size_t disks = tfs.numDisks; // 使用tfs对象
    }
    
    Directory tempDir(params); // 为临时文件而作出的目录
    
    • 以上的程序存在的一个问题: 除非tfstempDir之前先被初始化,否则tempDir的构造函数会用到尚为初始化的tfs解决方法:local static对象代替non-local static对象(参考单例模式的常见实现方法)。
    class FileSystem {  // 来自你的程序库
    public:
      std::size_t numDisks() const;
    
    };
    
    // 用这个函数代替tfs对象,定义并初始化一个local static对象,返回一个reference指向上述对象
    FileSystem& tfs() {
      static FileSystem fs;
      return fs;
    }
    
    class Directory { // 由程序库客户建立
    public:
      Directory(params);
    
    };
    Directory::Directory(params) {
      std::size_t disks = tfs().numDisks; // 使用tfs()
    }
    
    // 用这个函数代替tempDir对象
    Directory& tempDir() {
      static Directory td;
      return td;
    }
    

    请记住

    • 为内置型对象进行手工初始化,因为C++保证初始化它们。
    • 构造函数最好使用成员初始列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
    • 为免除"跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。
  • 相关阅读:
    ARM的反汇编工具
    Windows上使用Objectivec和Cocoa
    linux 统计代码行数
    oracle数据库基本操作
    thinkphp 带检索参数分页
    Object 转为List
    MvvM datagrid多行选中绑定
    mvvm Dev12.1 GridControl 导出
    ASP.NET MVC入门,好文共享
    [职场、征人、面试](呛)你到底要不要换工作? Part (II) 诚实,最难堪的状态,却最有价值的对策
  • 原文地址:https://www.cnblogs.com/parzulpan/p/13493905.html
Copyright © 2011-2022 走看看