Effective C++ 读书笔记

匆匆过客 提交于 2020-05-08 10:18:52

---恢复内容开始---

  世界上有两种C++程序员,一种是读过Efective C++ 的,一种是没有读过的。

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

  • C。
  • Object-Oriented C++。
  • Template C++。
  • STL。

 

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

  1. class 专属常量:为了将常量的作用域限制于 class 内,必须让它成为 class 的一个成员(member);而为确保此常量至多只有一份实体,你必须让它成为一个 static 成员:

1 class GamePlayer {
2 private:
3   static const int NumTurns = 5;     // 常量声明式
4   int scores[NumTurns];              // 使用该常量
5   ...
6 };

 现在是 NumTurns 的声明式而非定义式。通常 C++ 要求你对所使用的任何东西提供定义式,但如果是 class 专属常量又是 static 且为整数类型(integral type, 例如 ints, chars, bools),则需特殊处理。只要不取它们的地址,便可以声明并使用它们而无须提供定义式。但如果要取其地址(或编译器要求),就必须另外提供定义式如下:

const int GamePlayer::NumTurns;    // 已在声明时获得初值,定义时不可以再设初值。

请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值,因此定义时不可以再设初值。

 2. 请记住

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

 

条款03:尽可能使用 const

 1. 如果关键字 const 出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

 2. STL 迭代器系以指针为根据塑模出来,所以迭代器的作用就像个 T* 指针。声明迭代器为 const 就像声明指针为 const 一样(即声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果希望迭代器所指的东西不可被改动(即希望 STL 模拟一个 const T* 指针),需要的是 const_iterator:

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();  // iter 的作用像个 T* const
*iter = 10;  // 没问题,改变 iter 所指物
++iter;      // 错误!iter 是 const
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;    // 错误!*cIter 是 const
++cIter;        // 没问题,改变 cIter。

 3. 请记住:

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

 

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

 1. 永远在使用对象之前先将它初始化。对于非内置类型,要确保每一个构造函数都将对象的每一个成员初始化。这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯薄的 class,其构造函数如下:

 1 class PhoneNumber { ... };
 2 class ABEntry {     // ABEntry = "Address Book Entry"
 3 public:
 4   ABEntry(const std::string& name, const std::string& address,
 5           const std::list<PhoneNumber>& phones);
 6 private:
 7   std::string theName;
 8   std::string theAddress;
 9   std::list<PhoneNumber> thePhones;
10   int numTimesConsulted;
11 };
12 ABEntry::ABEntry(const std::string& name, const std::string& address, 
13                  const std::list<PhoneNumber>& phones) {
14   theName = name;          // 这些都是赋值(assignments)
15   theAddress = address;    // 而非初始化(initializations)
16   thePhones = phones;
17   numTimesConsulted = 0;
18 }

该写法结果正确,但不是最佳写法。ABEntry 构造函数的一个较佳写法是,使用所谓的 member initialization list (成员初始化列表)替换赋值动作:

1 ABEntry::ABEntry(const std::string& name, const std::string& address,
2                  const std::list<PhoneNumber>& phones)
3     : theName(name),        // 现在,这些都是初始化
4       theAddress(address),
5       thePhones(phones),
6       numTimesConsulted(0) 
7 { }                         // 现在,构造函数本体不必有任何动作

 2. 如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值。为避免需要记住成员变量何时必须在成员初始化列表中初始化,何时不需要,最简单的做法就是:总是使用成员初始化列表。

 3. 成员初始化次序:1)base classes 早于其 derived classes;2)按照变量被声明的次序进行初始化。

 4. static 对象(其寿命从被构造出来直到程序结束为止),包括 global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为 static 的对象。函数内的 static 对象称为 local static 对象,其他 static 对象称为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们的析构函数会在 main() 结束时被自动调用。

 5. 请记住:

  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
  • 构造函数最好使用成员初始化列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始化列表列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。

 

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

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

 2. 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。

 

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

 1. 设计一个不能被拷贝构造和赋值构造的类:

class Uncopyable {
protected:
  Uncopyable() {}                  // 允许 derived 对象构造和析构
  ~Uncopyable() {}
private:
  Uncopyable(const Uncopyable&);   // 但阻止 copying
  Uncopyable& operator=(const Uncopyable&)
};

class HomeForSale : private Uncopyable {   // class 不再声明 
  ...                                      // copy 构造函数或
};                                         // copy assign. 操作符 

 2. 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。

 

条款07:为多态基类声明 virtual 析构函数

 1. 任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数;且只有当 class 内含至少一个 virtual 函数时,才为它声明 virtual 析构函数。

 2. 请记住:

  • polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性,就不该声明 virtual 析构函数。

 

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

 1. C++ 不喜欢析构函数吐出异常,会导致程序过早结束或出现不明确行为。

 2. 请记住:

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

 

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

 1. 在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class(比起当前执行构造函数和析构函数的那层)。

 

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

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

class Widget {
public:
  ...
  Widget& operator=(const Widget& rhs) {  // 返回类型是个 reference,
    ...                                   // 指向当前对象。
    return *this;                         // 返回左侧对象
  }
};

 

条款11:在 operator= 中处理“自我赋值”

 1. “自我赋值”要具备“自我赋值安全性”和“异常安全性”。下面的代码就不具备“自我赋值安全性”,如果 *this 和 rhs 是同一个对象,就会出现问题。

class Bitmap { ... };
class Widget {
  ...
private:
  Bitmap* pb;
};

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

为阻止这种错误,需要检验是否是“自我赋值”:

Widget& Widget::operator=(const Widget& rhs) {
  if (this == &rhs) return *this;      // 如果是自我赋值,就不做任何事

  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

但上述代码存在异常方面的麻烦,如果“new Bitmap”导致异常,Widget 最终会持有一个指针指向一块被删除的 Bitmap。同时具备两种安全性的代码如下:

Widget& Widget::operator=(const Widget& rhs) {
  Bitmap* pOrig = pb;        // 记住原先的 pb
  pb = new Bitmap(*rhs.pb);  // 令 pb 指向 *pb 的一个副本
  delete pOrig;              // 删除原先的 pb
  return *this;
}

另一个替代方案是,使用所谓的 copy and swap 技术:

class Widget {
  ...
  void swap(Widget& rhs);  // 交换 *this 和 rhs 的数据
  ...
};

Widget& Widget::operator=(const Widget& rhs) {
  Widget temp(rhs);
  swap(temp);
  return *this;
}

 

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

 1. 编写拷贝构造函数时,要确保(1)复制所有 local 成员变量,(2)调用所有 base classes 内的适当的 copying 函数。

 

条款13:以对象管理资源(智能指针)

 1. 考虑以下类:

class Investment { ... };        // “投资类型”继承体系中的 root class

Investment* createInvestment();  // 返回指针,指向 Investment 继承体系内
                                 // 的动态分配对象。调用者有责任删除它。

createInvestment 的调用者使用了函数返回的对象后,需要手动释放它。现在考虑有个 f 函数履行了这个责任:

void f() {
  Investment* pInv = createInvestment(); // 调用 factory 函数
  ...
  delete pInv;                           // 释放 pInv 所指对象
}

但有可能“...”区域内的语句抛出异常或提前返回,delete 将不会被调用从而发生内存泄漏。为确保 createInvestment 返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开 f,该对象的析构函数会自动释放资源,这就是“智能指针”的思想:

void f() {
  std::auto_ptr<Investment> pInv(createInvestment());
  ...                     // 调用 factory 函数
}                         // 一如以往地使用 pInv,经由 auto_ptr 的析构函数自动删除 pInv

使用 auto_ptr 时有个问题,auto_ptr 同时只能指向一个对象,否则对象会被删除一次以上。auto_ptr 有一个性质:若通过拷贝构造或赋值构造复制时,它们会变成 null,复制所得的指针将取得资源的唯一拥有权。

std::auto_ptr<Investment> pInv1(createInvestment()); // pInv 指向 createInvestment 返回值
std::auto_ptr<Investment> pInv2(pInv1);              // 现在 pInv2 指向对象,pInv1 被设为 null
pInv1 = pInv2;                                       // 现在 pInv1 指向对象,pInv2 被设为 null

因此,STL 容器要求其元素发挥“正常的”复制行为,这些容器容不得 auto_ptr,而要使用另一种智能指针 shared_ptr:

void f() {
  ...
  std::shared_ptr<Investment> pInv1(createInvestment());
  std::shared_ptr<Investment> pInv2(pInv1);
  pInv1 = pInv2;
}

 

条款14:在资源管理类中小心 copying 行为

 1. 假如我们使用 C API 函数处理类型为 Mutex 的互斥器对象,共有 lock 和 unlock 两函数可用:

void lock(Mutex* pm);    // 锁定 pm 所指的互斥器
void unlock(Mutex* pm);  // 将互斥器解除锁定

为确保不会忘记将一个被锁住的 Mutex 解锁,你可能会希望建立一个 class 用来管理机锁。这样的 class 的基本结构由 RAII 守则支配,也就是“资源在构造期间获得,在析构期间释放”:

class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm) {
    lock(mutexPtr);     // 获得资源
  }
  ~Lock() {
    unlock(mutexPtr);  // 释放资源
  }
private:
  Mutex* mutexPtr;
};

// 客户对 Lock 的用法符合 RAII 方式:
Mutex m;         // 定义你需要的互斥器
...
{                // 建立一个区块用来定义 critical section
  Lock m1(&m);   // 锁定互斥器
  ...            // 执行 critical section 内的操作
}                // 在区块最末尾,自动解除互斥器锁定

当 RAII 对象被复制时,大多数时候会选择以下两种可能:

  1)禁止复制。此时应将 copying 操作声明为 private,对 Lock 而言看起来是这样:

class Lock : private Uncopyable {  // 禁止复制。见条款6.
public:
  ...
};

  2)对底层资源祭出“引用计数法”。

class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { // 以某个 Mutex 初始化 shared_ptr
    lock(mutexPtr.get());                           // 并以 unlock 函数为删除器
  }
private:
  std::shared_ptr<Mutex> mutexPtr;                  // 使用 shared_ptr 替换 raw pointer
};

 

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

 1. 将 RAII class 对象(如 std::shared_ptr)转换为其所内含之原始资源,有两种做法:显式转换和隐式转换。

 

条款16:成对使用 new 和 delete 时要采取相同形式

 1. 如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 []。如果在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []。

 

条款17:以独立语句将 newed 对象置入智能指针

 1. 假如有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的 Widget 上进行某个带有优先权的处理:

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

如果调用 processWidget 时用以下语句:

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

上述调用可能泄漏资源,正确的写法是:使用分离语句,分别写出(1)创建 Widget,(2)将它置入智能指针中,然后再把智能指针传给 processWidget:

std::shared_ptr<Widget> pw(new Widget); // 在单独语句中以智能指针存储 newed 所得对象
processWidget(pw, priority());          // 这个调用动作不会造成泄漏

 2. 以独立语句将 newed 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

 

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

std::shared_ptr<Investment> createInvestment() {
  std::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
                                     getRidOfInvestment);
  retVal = ...;  // 令 retVal 指向正确对象
  return retVal;
}

 

条款19:设计 class 犹如设计 type

 

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

 1. 对于用户自定义类型来说,传值方式会导致额外的拷贝构造和析构函数发生,因此应尽量用 pass by reference-to-const 方式。以 by reference 方式传递参数也可以避免 slicing (对象切割) 问题。当一个派生类以传值方式传递并被视为一个基类对象,基类的拷贝构造函数会被调用,而派生类的部分全被切割掉了。

class Window {
public:
  ...
  std::string name() const;     // 返回窗口名称
  virtual void display() const; // 显式窗口和其内容
};

class WindowWithScrollBars : public Window {
public:
  ...
  virtual void display() const;
};

现在假设写个函数打印窗口名称,然后显式该窗口。下面是错误示范:

void printNameAndDisplay(Window w)  { // 不正确,参数可能被切割
  std::cout << w.name();
  w.display();
}

当调用上述函数并给它一个 WindowWithScrollBars 对象是:

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

以上在 printNameAndDisplay 内调用 diaplay 调用的总是 Window::display,而不是 WindowWithScrollBars::display。

解决切割问题的办法,就是以 by-reference-to-const 的方式传递 w:

void printNameAndDisplay(const Window& w) {
  std::cout << w.name();
  w.display();
}

现在,传进来的窗口是什么类型,w 就表现出那种类型。

 2. 请记住

  • 尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。
  • 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对他们而言,pass-by-value 往往比较适当。

 

条款21:必须返回对象时,别妄想返回其引用

 1. 不要返回指针或引用指向一个 local stack 对象,也不要返回引用指向一个 heap-allocated 对象,或返回指针或引用指向一个 local static 对象而有可能同时需要多个这样的对象。

 

条款22:将成员变量声明为 private

 1. 将成员变量声明为 private,以函数取得或设定其值,可以实现出“不准访问”、“只读访问”、“读写访问”和“只写访问”:

class AccessLevels {
public:
  int getReadOnly() const { return readOnly; }
  void setReadWrite(int value) { readWrite = value; }
  int getReadWrite() const { return readWrite; }
  void setWriteOnly(int value) { writeOnly = value; }
private:
  int noAccess;   // 对此 int 无任何访问动作
  int readOnly;   // 对此 int 做只读访问
  int readWrite;  // 对此 int 做读写访问
  int writeOnly;  // 对此 int 做只写访问
};

 2. protected 并不比 public 更具封装性。

 

条款23:宁以 non-member、non-friend 替换 member 函数

 1. 如以下 WebBrowser 类提供三种方法:清除下载元素高速缓存区、清除访问过的 RUL 的历史记录、以及移除系统中的所有 cookies:

class WebBrowser {
public:
  ...
  void clearCache();
  void clearHistory();
  void removeCookies();
};

如果想一整个执行所有这些动作,因此 WebBrowser 可提供这样一个成员函数:

class WebBrowser {
public:
  ...
  void clearEverything();  // 调用 clearCache, clearHistory 和 removeCookies
};

这一机能也可由一个非成员函数调用适当的成员函数而提供出来:

void clearBrowser(WebBrowser& wb) {
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

上述两种方法,非成员函数版本更好,因为它具有更大的封装性。

 2. 在 C++ 中,比较自然的方法是让 clearBrowser 成为一个非成员函数并且位于 WebBrowser 所在的同一个 namespace 内:

namespace WebBrowserStuff {
  class WebBrowser { .. };
  void clearBrowser(WebBrowser& wb);
  ...
}

将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。

// 头文件 "webbrowser.h" - 这个头文件针对 class WebBrowser 自身
// 及 WebBrowser 核心机能。
namespace WebBrowserStuff {
class WebBrowser { ... };
  ...           // 核心机能,例如几乎所有客户都需要的
}               // 非成员函数

// 头文件 "webbrowserbookmarks.h"
namespace WebBrowserStuff {
  ...           // 与书签相关的便利函数
}

// 头文件 “webbrowsercookies.h”
namespace WebBrowserStuff {
  ...          // 与 cookie 相关的便利函数
}

 

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

 1. 考虑如下 Rational class:

class Rational {
public:
  Rational(int numerator = 0,    // 构造函数刻意不为 explicit;
           int denominator = 1); // 允许 int-to-Rational 隐式转换。
  int numerator() const;
  int denominator() const;
private:
  ...
};

如果将 operator* 写成 Rational 成员函数的写法:

class Rational {
public:
  ...
  const Rational operator* (const Rational& rhs) const;
};

对于如下调用:

Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // 可以通过编译
result = 2 * oneHalf;          // 错误

第一个调用中,因为是 non-explicit 构造函数,2被隐式转换成了 Rational 对象,第二个调用无法通过编译。结论是,只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参数者。被调用成员所隶属的那个对象(即 this 对象)不是隐式转换的合格参与者。这就是为什么第一次调用可通过编译,第二次调用则否。

正确的写法应是:

class Rational {
  ...
};

const Rational operator*(const Rational& lhs,
                         const Rational& rhs) {
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

 2. 如果需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那个这个函数必须是 non-member。

 

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

 1. 

 

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

 1. 考虑下面函数,它计算通行密码的加密版本而后返回,如果密码太短,函数会丢出一个异常,类型为 logic_error。

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;  // 置入变量 encrypted 内
}

对象 encrypted 在此函数中并非完全未被使用,但如果有个异常被丢出,它就真的没被使用。也就是说如果函数 encryptPassword 丢出异常,你仍得付出 encrypted 的构造成本和析构成本,所以最好延后 encrypted 的定义式,直到确实需要它:

std::string encryptPassword(const std::string& password) {
  using namespace std;
  if (password.length() < MinimumPasswordLength) {
    throw logic_error("Password is too short");
  }
  string encrypted(password);  // 通过 copy 构造函数定义并初始化
  encrypt(encrypted);
  return encrypted;
}

 

条款27:尽量少做转型动作

 1. c++ 的四种类型转换方式:

  1. const_cast<T>( expression )
  2. dynamic_cast<T>( expression )
  3. reinterpret_cast<T>( expression )
  4. static_cast<T>( expression )

各有不同的目的:

  • const_cast 通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的 C++-style 转型操作符。
  • dynamic_cast 主要用来执行“安全向下转型”,也就是用来决定某对象是否归属体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
  • reinterpret_cast 意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
  • static_cast 用来强迫隐式转换,例如将 non-const 对象转为 const 对象,或将 int 转为 double 等等。它也可以用来执行上述多种转换的反向转换,例如将 void* 指针转为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无法将 const 转为 non-const——这个只有 const_cast 才能办到。
class Widget {
public:
  explicit Widget(int size);
};
void doSomeWork(const Widget& w);
doSomeWork(static_cast<Widget>(15));

  2. 例如许多应用框架都要求派生类内的 virtual 函数代码的第一个动作就先调用基类的对应函数。假设我们有个 Window 基类和一个 SpecialWindow 派生类,两者都定义了 virtual 函数 onResize。进一步假设 SpecialWindow 的 onResize 函数被要求首先调用 Window 的 onResize。下面是实现方式之一,它看起来对,但实际上错:

class Window {                             // base class
public:
  virtual void onResize() { ... }          // base onResize 实现代码
  ...
};

class SpecialWindow : public Window {      // derived class
public:
  virtual void onResize() {                // derived onResize 实现代码
    static_cast<Window>(*this).onResize(); // 将 *this 转型为 Window,然后调用其 onResize;
                                           // 这不可行!
    ...                                    // 这里进行 SpecialWindow 专属行为。
  }
  ...
};

上述代码中并非在当前对象身上调用 Window::onResize 之后又在该对象身上执行 SpecialWindow 专属动作。它是在“当前对象之基类成分”的副本上调用 Window::onResize,然后在当前对象身上执行 SpecialWindow 专属动作。如果 Window::onResize 修改了对象内容,当前对象其实没被改动,改动的是副本。正确的写法为:

class SpecialWindow : public Window {
public:
  virtual void onResize() {
    Window::onResize();  // 调用 Window::onResize 作用于 *this 身上
    ...
  }
};

 3. 如果想在派生类对象上执行派生类操作函数,但你的手上只有指向基类的指针或引用,只能靠它们来处理对象。有两个一般性做法可以避免这个问题:

  1)使用容器并在其中存储直接指向派生类对象的指针(通常是智能指针),如此便消除了“通过基类接口处理对象”的需要。假设先前的 Window/SpecialWindow 继承体系中只有 SpecialWindow 才支持闪烁效果,不要这么做:

class Window {
pubic:
  virtual ~Window() {}
};

class SpecialWindow : public Window {
public:
  void blink();
  ...
};

typedef std::vector<std::shared_ptr<Window> > VPW;
VPW winPtrs;
std::shared_ptr<Window> sp(new SpecialWindow);
winPtrs.push_back(sp);
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++) { // 不希望使用 dynamic_cast if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink(); }

(注意,dynamic_cast 仅工作于多态类,多态类是至少具有一个虚函数的类,析构函数也算。dynamic_cast 中,只需要源类型具有多态性,编译就能通过。如果目标类型不具多态性,dynamic_cast 将返回空指针。)

应该改为这样做:

class Window { ... }
class SpecialWindow : public Window {
public:
  void blink();
  ...
};

typedef std::vector<std::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++)
  (*iter)->blink();

  2)使用虚函数。

class Window {
public:
  virtual void blink() {}
  ...
};

class SpecialWindow : public Window {
public:
  virtual void blink() { ... }
};

typedef std::vector<std::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++)
  (*iter)->blink();

 

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

 1. 

---恢复内容结束---

  世界上有两种C++程序员,一种是读过Efective C++ 的,一种是没有读过的。

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

  • C。
  • Object-Oriented C++。
  • Template C++。
  • STL。

 

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

  1. class 专属常量:为了将常量的作用域限制于 class 内,必须让它成为 class 的一个成员(member);而为确保此常量至多只有一份实体,你必须让它成为一个 static 成员:

1 class GamePlayer {
2 private:
3   static const int NumTurns = 5;     // 常量声明式
4   int scores[NumTurns];              // 使用该常量
5   ...
6 };

 现在是 NumTurns 的声明式而非定义式。通常 C++ 要求你对所使用的任何东西提供定义式,但如果是 class 专属常量又是 static 且为整数类型(integral type, 例如 ints, chars, bools),则需特殊处理。只要不取它们的地址,便可以声明并使用它们而无须提供定义式。但如果要取其地址(或编译器要求),就必须另外提供定义式如下:

const int GamePlayer::NumTurns;    // 已在声明时获得初值,定义时不可以再设初值。

请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值,因此定义时不可以再设初值。

 2. 请记住

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

 

条款03:尽可能使用 const

 1. 如果关键字 const 出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

 2. STL 迭代器系以指针为根据塑模出来,所以迭代器的作用就像个 T* 指针。声明迭代器为 const 就像声明指针为 const 一样(即声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果希望迭代器所指的东西不可被改动(即希望 STL 模拟一个 const T* 指针),需要的是 const_iterator:

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();  // iter 的作用像个 T* const
*iter = 10;  // 没问题,改变 iter 所指物
++iter;      // 错误!iter 是 const
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;    // 错误!*cIter 是 const
++cIter;        // 没问题,改变 cIter。

 3. 请记住:

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

 

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

 1. 永远在使用对象之前先将它初始化。对于非内置类型,要确保每一个构造函数都将对象的每一个成员初始化。这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯薄的 class,其构造函数如下:

 1 class PhoneNumber { ... };
 2 class ABEntry {     // ABEntry = "Address Book Entry"
 3 public:
 4   ABEntry(const std::string& name, const std::string& address,
 5           const std::list<PhoneNumber>& phones);
 6 private:
 7   std::string theName;
 8   std::string theAddress;
 9   std::list<PhoneNumber> thePhones;
10   int numTimesConsulted;
11 };
12 ABEntry::ABEntry(const std::string& name, const std::string& address, 
13                  const std::list<PhoneNumber>& phones) {
14   theName = name;          // 这些都是赋值(assignments)
15   theAddress = address;    // 而非初始化(initializations)
16   thePhones = phones;
17   numTimesConsulted = 0;
18 }

该写法结果正确,但不是最佳写法。ABEntry 构造函数的一个较佳写法是,使用所谓的 member initialization list (成员初始化列表)替换赋值动作:

1 ABEntry::ABEntry(const std::string& name, const std::string& address,
2                  const std::list<PhoneNumber>& phones)
3     : theName(name),        // 现在,这些都是初始化
4       theAddress(address),
5       thePhones(phones),
6       numTimesConsulted(0) 
7 { }                         // 现在,构造函数本体不必有任何动作

 2. 如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值。为避免需要记住成员变量何时必须在成员初始化列表中初始化,何时不需要,最简单的做法就是:总是使用成员初始化列表。

 3. 成员初始化次序:1)base classes 早于其 derived classes;2)按照变量被声明的次序进行初始化。

 4. static 对象(其寿命从被构造出来直到程序结束为止),包括 global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为 static 的对象。函数内的 static 对象称为 local static 对象,其他 static 对象称为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们的析构函数会在 main() 结束时被自动调用。

 5. 请记住:

  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
  • 构造函数最好使用成员初始化列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始化列表列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。

 

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

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

 2. 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。

 

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

 1. 设计一个不能被拷贝构造和赋值构造的类:

class Uncopyable {
protected:
  Uncopyable() {}                  // 允许 derived 对象构造和析构
  ~Uncopyable() {}
private:
  Uncopyable(const Uncopyable&);   // 但阻止 copying
  Uncopyable& operator=(const Uncopyable&)
};

class HomeForSale : private Uncopyable {   // class 不再声明 
  ...                                      // copy 构造函数或
};                                         // copy assign. 操作符 

 2. 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。

 

条款07:为多态基类声明 virtual 析构函数

 1. 任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数;且只有当 class 内含至少一个 virtual 函数时,才为它声明 virtual 析构函数。

 2. 请记住:

  • polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性,就不该声明 virtual 析构函数。

 

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

 1. C++ 不喜欢析构函数吐出异常,会导致程序过早结束或出现不明确行为。

 2. 请记住:

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

 

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

 1. 在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class(比起当前执行构造函数和析构函数的那层)。

 

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

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

class Widget {
public:
  ...
  Widget& operator=(const Widget& rhs) {  // 返回类型是个 reference,
    ...                                   // 指向当前对象。
    return *this;                         // 返回左侧对象
  }
};

 

条款11:在 operator= 中处理“自我赋值”

 1. “自我赋值”要具备“自我赋值安全性”和“异常安全性”。下面的代码就不具备“自我赋值安全性”,如果 *this 和 rhs 是同一个对象,就会出现问题。

class Bitmap { ... };
class Widget {
  ...
private:
  Bitmap* pb;
};

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

为阻止这种错误,需要检验是否是“自我赋值”:

Widget& Widget::operator=(const Widget& rhs) {
  if (this == &rhs) return *this;      // 如果是自我赋值,就不做任何事

  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

但上述代码存在异常方面的麻烦,如果“new Bitmap”导致异常,Widget 最终会持有一个指针指向一块被删除的 Bitmap。同时具备两种安全性的代码如下:

Widget& Widget::operator=(const Widget& rhs) {
  Bitmap* pOrig = pb;        // 记住原先的 pb
  pb = new Bitmap(*rhs.pb);  // 令 pb 指向 *pb 的一个副本
  delete pOrig;              // 删除原先的 pb
  return *this;
}

另一个替代方案是,使用所谓的 copy and swap 技术:

class Widget {
  ...
  void swap(Widget& rhs);  // 交换 *this 和 rhs 的数据
  ...
};

Widget& Widget::operator=(const Widget& rhs) {
  Widget temp(rhs);
  swap(temp);
  return *this;
}

 

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

 1. 编写拷贝构造函数时,要确保(1)复制所有 local 成员变量,(2)调用所有 base classes 内的适当的 copying 函数。

 

条款13:以对象管理资源(智能指针)

 1. 考虑以下类:

class Investment { ... };        // “投资类型”继承体系中的 root class

Investment* createInvestment();  // 返回指针,指向 Investment 继承体系内
                                 // 的动态分配对象。调用者有责任删除它。

createInvestment 的调用者使用了函数返回的对象后,需要手动释放它。现在考虑有个 f 函数履行了这个责任:

void f() {
  Investment* pInv = createInvestment(); // 调用 factory 函数
  ...
  delete pInv;                           // 释放 pInv 所指对象
}

但有可能“...”区域内的语句抛出异常或提前返回,delete 将不会被调用从而发生内存泄漏。为确保 createInvestment 返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开 f,该对象的析构函数会自动释放资源,这就是“智能指针”的思想:

void f() {
  std::auto_ptr<Investment> pInv(createInvestment());
  ...                     // 调用 factory 函数
}                         // 一如以往地使用 pInv,经由 auto_ptr 的析构函数自动删除 pInv

使用 auto_ptr 时有个问题,auto_ptr 同时只能指向一个对象,否则对象会被删除一次以上。auto_ptr 有一个性质:若通过拷贝构造或赋值构造复制时,它们会变成 null,复制所得的指针将取得资源的唯一拥有权。

std::auto_ptr<Investment> pInv1(createInvestment()); // pInv 指向 createInvestment 返回值
std::auto_ptr<Investment> pInv2(pInv1);              // 现在 pInv2 指向对象,pInv1 被设为 null
pInv1 = pInv2;                                       // 现在 pInv1 指向对象,pInv2 被设为 null

因此,STL 容器要求其元素发挥“正常的”复制行为,这些容器容不得 auto_ptr,而要使用另一种智能指针 shared_ptr:

void f() {
  ...
  std::shared_ptr<Investment> pInv1(createInvestment());
  std::shared_ptr<Investment> pInv2(pInv1);
  pInv1 = pInv2;
}

 

条款14:在资源管理类中小心 copying 行为

 1. 假如我们使用 C API 函数处理类型为 Mutex 的互斥器对象,共有 lock 和 unlock 两函数可用:

void lock(Mutex* pm);    // 锁定 pm 所指的互斥器
void unlock(Mutex* pm);  // 将互斥器解除锁定

为确保不会忘记将一个被锁住的 Mutex 解锁,你可能会希望建立一个 class 用来管理机锁。这样的 class 的基本结构由 RAII 守则支配,也就是“资源在构造期间获得,在析构期间释放”:

class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm) {
    lock(mutexPtr);     // 获得资源
  }
  ~Lock() {
    unlock(mutexPtr);  // 释放资源
  }
private:
  Mutex* mutexPtr;
};

// 客户对 Lock 的用法符合 RAII 方式:
Mutex m;         // 定义你需要的互斥器
...
{                // 建立一个区块用来定义 critical section
  Lock m1(&m);   // 锁定互斥器
  ...            // 执行 critical section 内的操作
}                // 在区块最末尾,自动解除互斥器锁定

当 RAII 对象被复制时,大多数时候会选择以下两种可能:

  1)禁止复制。此时应将 copying 操作声明为 private,对 Lock 而言看起来是这样:

class Lock : private Uncopyable {  // 禁止复制。见条款6.
public:
  ...
};

  2)对底层资源祭出“引用计数法”。

class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { // 以某个 Mutex 初始化 shared_ptr
    lock(mutexPtr.get());                           // 并以 unlock 函数为删除器
  }
private:
  std::shared_ptr<Mutex> mutexPtr;                  // 使用 shared_ptr 替换 raw pointer
};

 

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

 1. 将 RAII class 对象(如 std::shared_ptr)转换为其所内含之原始资源,有两种做法:显式转换和隐式转换。

 

条款16:成对使用 new 和 delete 时要采取相同形式

 1. 如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 []。如果在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []。

 

条款17:以独立语句将 newed 对象置入智能指针

 1. 假如有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的 Widget 上进行某个带有优先权的处理:

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

如果调用 processWidget 时用以下语句:

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

上述调用可能泄漏资源,正确的写法是:使用分离语句,分别写出(1)创建 Widget,(2)将它置入智能指针中,然后再把智能指针传给 processWidget:

std::shared_ptr<Widget> pw(new Widget); // 在单独语句中以智能指针存储 newed 所得对象
processWidget(pw, priority());          // 这个调用动作不会造成泄漏

 2. 以独立语句将 newed 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

 

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

std::shared_ptr<Investment> createInvestment() {
  std::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
                                     getRidOfInvestment);
  retVal = ...;  // 令 retVal 指向正确对象
  return retVal;
}

 

条款19:设计 class 犹如设计 type

 

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

 1. 对于用户自定义类型来说,传值方式会导致额外的拷贝构造和析构函数发生,因此应尽量用 pass by reference-to-const 方式。以 by reference 方式传递参数也可以避免 slicing (对象切割) 问题。当一个派生类以传值方式传递并被视为一个基类对象,基类的拷贝构造函数会被调用,而派生类的部分全被切割掉了。

class Window {
public:
  ...
  std::string name() const;     // 返回窗口名称
  virtual void display() const; // 显式窗口和其内容
};

class WindowWithScrollBars : public Window {
public:
  ...
  virtual void display() const;
};

现在假设写个函数打印窗口名称,然后显式该窗口。下面是错误示范:

void printNameAndDisplay(Window w)  { // 不正确,参数可能被切割
  std::cout << w.name();
  w.display();
}

当调用上述函数并给它一个 WindowWithScrollBars 对象是:

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

以上在 printNameAndDisplay 内调用 diaplay 调用的总是 Window::display,而不是 WindowWithScrollBars::display。

解决切割问题的办法,就是以 by-reference-to-const 的方式传递 w:

void printNameAndDisplay(const Window& w) {
  std::cout << w.name();
  w.display();
}

现在,传进来的窗口是什么类型,w 就表现出那种类型。

 2. 请记住

  • 尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。
  • 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对他们而言,pass-by-value 往往比较适当。

 

条款21:必须返回对象时,别妄想返回其引用

 1. 不要返回指针或引用指向一个 local stack 对象,也不要返回引用指向一个 heap-allocated 对象,或返回指针或引用指向一个 local static 对象而有可能同时需要多个这样的对象。

 

条款22:将成员变量声明为 private

 1. 将成员变量声明为 private,以函数取得或设定其值,可以实现出“不准访问”、“只读访问”、“读写访问”和“只写访问”:

class AccessLevels {
public:
  int getReadOnly() const { return readOnly; }
  void setReadWrite(int value) { readWrite = value; }
  int getReadWrite() const { return readWrite; }
  void setWriteOnly(int value) { writeOnly = value; }
private:
  int noAccess;   // 对此 int 无任何访问动作
  int readOnly;   // 对此 int 做只读访问
  int readWrite;  // 对此 int 做读写访问
  int writeOnly;  // 对此 int 做只写访问
};

 2. protected 并不比 public 更具封装性。

 

条款23:宁以 non-member、non-friend 替换 member 函数

 1. 如以下 WebBrowser 类提供三种方法:清除下载元素高速缓存区、清除访问过的 RUL 的历史记录、以及移除系统中的所有 cookies:

class WebBrowser {
public:
  ...
  void clearCache();
  void clearHistory();
  void removeCookies();
};

如果想一整个执行所有这些动作,因此 WebBrowser 可提供这样一个成员函数:

class WebBrowser {
public:
  ...
  void clearEverything();  // 调用 clearCache, clearHistory 和 removeCookies
};

这一机能也可由一个非成员函数调用适当的成员函数而提供出来:

void clearBrowser(WebBrowser& wb) {
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

上述两种方法,非成员函数版本更好,因为它具有更大的封装性。

 2. 在 C++ 中,比较自然的方法是让 clearBrowser 成为一个非成员函数并且位于 WebBrowser 所在的同一个 namespace 内:

namespace WebBrowserStuff {
  class WebBrowser { .. };
  void clearBrowser(WebBrowser& wb);
  ...
}

将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。

// 头文件 "webbrowser.h" - 这个头文件针对 class WebBrowser 自身
// 及 WebBrowser 核心机能。
namespace WebBrowserStuff {
class WebBrowser { ... };
  ...           // 核心机能,例如几乎所有客户都需要的
}               // 非成员函数

// 头文件 "webbrowserbookmarks.h"
namespace WebBrowserStuff {
  ...           // 与书签相关的便利函数
}

// 头文件 “webbrowsercookies.h”
namespace WebBrowserStuff {
  ...          // 与 cookie 相关的便利函数
}

 

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

 1. 考虑如下 Rational class:

class Rational {
public:
  Rational(int numerator = 0,    // 构造函数刻意不为 explicit;
           int denominator = 1); // 允许 int-to-Rational 隐式转换。
  int numerator() const;
  int denominator() const;
private:
  ...
};

如果将 operator* 写成 Rational 成员函数的写法:

class Rational {
public:
  ...
  const Rational operator* (const Rational& rhs) const;
};

对于如下调用:

Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // 可以通过编译
result = 2 * oneHalf;          // 错误

第一个调用中,因为是 non-explicit 构造函数,2被隐式转换成了 Rational 对象,第二个调用无法通过编译。结论是,只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参数者。被调用成员所隶属的那个对象(即 this 对象)不是隐式转换的合格参与者。这就是为什么第一次调用可通过编译,第二次调用则否。

正确的写法应是:

class Rational {
  ...
};

const Rational operator*(const Rational& lhs,
                         const Rational& rhs) {
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

 2. 如果需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那个这个函数必须是 non-member。

 

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

 1. 

 

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

 1. 考虑下面函数,它计算通行密码的加密版本而后返回,如果密码太短,函数会丢出一个异常,类型为 logic_error。

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;  // 置入变量 encrypted 内
}

对象 encrypted 在此函数中并非完全未被使用,但如果有个异常被丢出,它就真的没被使用。也就是说如果函数 encryptPassword 丢出异常,你仍得付出 encrypted 的构造成本和析构成本,所以最好延后 encrypted 的定义式,直到确实需要它:

std::string encryptPassword(const std::string& password) {
  using namespace std;
  if (password.length() < MinimumPasswordLength) {
    throw logic_error("Password is too short");
  }
  string encrypted(password);  // 通过 copy 构造函数定义并初始化
  encrypt(encrypted);
  return encrypted;
}

 

条款27:尽量少做转型动作

 1. c++ 的四种类型转换方式:

  1. const_cast<T>( expression )
  2. dynamic_cast<T>( expression )
  3. reinterpret_cast<T>( expression )
  4. static_cast<T>( expression )

各有不同的目的:

  • const_cast 通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的 C++-style 转型操作符。
  • dynamic_cast 主要用来执行“安全向下转型”,也就是用来决定某对象是否归属体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
  • reinterpret_cast 意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
  • static_cast 用来强迫隐式转换,例如将 non-const 对象转为 const 对象,或将 int 转为 double 等等。它也可以用来执行上述多种转换的反向转换,例如将 void* 指针转为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无法将 const 转为 non-const——这个只有 const_cast 才能办到。
class Widget {
public:
  explicit Widget(int size);
};
void doSomeWork(const Widget& w);
doSomeWork(static_cast<Widget>(15));

  2. 例如许多应用框架都要求派生类内的 virtual 函数代码的第一个动作就先调用基类的对应函数。假设我们有个 Window 基类和一个 SpecialWindow 派生类,两者都定义了 virtual 函数 onResize。进一步假设 SpecialWindow 的 onResize 函数被要求首先调用 Window 的 onResize。下面是实现方式之一,它看起来对,但实际上错:

class Window {                             // base class
public:
  virtual void onResize() { ... }          // base onResize 实现代码
  ...
};

class SpecialWindow : public Window {      // derived class
public:
  virtual void onResize() {                // derived onResize 实现代码
    static_cast<Window>(*this).onResize(); // 将 *this 转型为 Window,然后调用其 onResize;
                                           // 这不可行!
    ...                                    // 这里进行 SpecialWindow 专属行为。
  }
  ...
};

上述代码中并非在当前对象身上调用 Window::onResize 之后又在该对象身上执行 SpecialWindow 专属动作。它是在“当前对象之基类成分”的副本上调用 Window::onResize,然后在当前对象身上执行 SpecialWindow 专属动作。如果 Window::onResize 修改了对象内容,当前对象其实没被改动,改动的是副本。正确的写法为:

class SpecialWindow : public Window {
public:
  virtual void onResize() {
    Window::onResize();  // 调用 Window::onResize 作用于 *this 身上
    ...
  }
};

 3. 如果想在派生类对象上执行派生类操作函数,但你的手上只有指向基类的指针或引用,只能靠它们来处理对象。有两个一般性做法可以避免这个问题:

  1)使用容器并在其中存储直接指向派生类对象的指针(通常是智能指针),如此便消除了“通过基类接口处理对象”的需要。假设先前的 Window/SpecialWindow 继承体系中只有 SpecialWindow 才支持闪烁效果,不要这么做:

class Window {
pubic:
  virtual ~Window() {}
};

class SpecialWindow : public Window {
public:
  void blink();
  ...
};

typedef std::vector<std::shared_ptr<Window> > VPW;
VPW winPtrs;
std::shared_ptr<Window> sp(new SpecialWindow);
winPtrs.push_back(sp);
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++) { // 不希望使用 dynamic_cast if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink(); }

(注意,dynamic_cast 仅工作于多态类,多态类是至少具有一个虚函数的类,析构函数也算。dynamic_cast 中,只需要源类型具有多态性,编译就能通过。如果目标类型不具多态性,dynamic_cast 将返回空指针。)

应该改为这样做:

class Window { ... }
class SpecialWindow : public Window {
public:
  void blink();
  ...
};

typedef std::vector<std::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++)
  (*iter)->blink();

  2)使用虚函数。

class Window {
public:
  virtual void blink() {}
  ...
};

class SpecialWindow : public Window {
public:
  virtual void blink() { ... }
};

typedef std::vector<std::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); iter++)
  (*iter)->blink();

 

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

 1. 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!