Loading

【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对象。
posted @ 2020-08-13 00:08  Parzulpan  阅读(158)  评论(0编辑  收藏  举报