---恢复内容开始---
世界上有两种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++ 的四种类型转换方式:
- const_cast<T>( expression )
- dynamic_cast<T>( expression )
- reinterpret_cast<T>( expression )
- 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++ 的四种类型转换方式:
- const_cast<T>( expression )
- dynamic_cast<T>( expression )
- reinterpret_cast<T>( expression )
- 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.
来源:oschina
链接:https://my.oschina.net/u/4415205/blog/3787888