Effective C++
Table of Contents
Chapter 1: Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
- C++ 发展多年以后其实更像如下的几个部分的结合体:
- C语言
- 面向对象的C语言: 构造析构,继承,封装,多肽
- Template: 就是generic programming
- STL: generic programming库
- 我们要根据自己当前出的区域,来决定用什么技术更高效, 比如:
- 对于inline类型来说(c语言), build-in类型pass-by-value通常比pass-by-reference 高效, 但是在OOC里面, 由于有构造和析构的存在, 我们就更倾向于使用 pass-by-reference-const.
- 在Template领域里面, 也是要传pass-by-reference-const
- 但是在STL领域里面pass-by-value再次适用, 因为迭代器和函数对象都是在指针上面塑造 出来的, 那就必须传递指针(指针就是value, 引用才是reference)
Item 2: Prefer const, enums and inlines to #defines
- #define的缺点:
- 用define定义以后,由于有预编译的存在, ASPECT_RATIO根本就没进入符号表,那么
出了错误,你也就不知道在哪里去改
#define ASPECT_RATIO 1.653 // Better Option const double AspectRation = 1.653
- 下面这个例子由于预编译的存在, STRING的内存被分配多次,如果用const替代的话,
只需要分配一次
#define STRING "abcdefg\n" const char string[] = "abcdefg"; printf(STRING); // Allocate memory for STRING printf(string); // Allocate memory for string, for the first and last time. //.... printf(STRING); // Allocate memory for STRING again printf(string); // No allocate memory here
- #define并不重视作用域, 除非#undef, 否则她一直有效, 这对于封装来说是个灾 难(不存在private define这一说), const可以解决这个问题
- #define实现宏函数, 问题非常多, 有时候加再多的括号也没办法解决:
#define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b) int a = 5, b = 0; CALL_WITH_MAX(++a, b); // a is incremented twice CALL_WITH_MAX(++a, b); // a is incremented once
- 用define定义以后,由于有预编译的存在, ASPECT_RATIO根本就没进入符号表,那么
出了错误,你也就不知道在哪里去改
Item 3: Use const whenever possible
- const 指针是非常容易错的部分, 总结起来就是
- const在*号左边,就是指针指向的东西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; //same as above char * const p = greeting; //const pointer, non-const data const char * const p = greeting; //const pointer, const data
- const在STL里面的应用如下:
std::vector<int> vec; const std::vector<int>::iterator iter = vec.begin(); // iter acts like a T* const std:vector<int>::const_iterator cIter = vec.begin(); // cIter acts like a const T*
- 如果一个成员变量被声明为const, 那么她就不能改变过其他成员变量, 下面例子中,
函数operator[]的返回值为const, 那么x[const成员变量]的结果就无法被赋值.
class TextBlock { public: const char& operator[](std::size_t position) const { //operator[] for return text[position]; //const object } char& operator[](std::size_t position) { //operator[] for return text[position]; // non-const object } private: std::string text; };
Item 4: Make sure that objects are initialized before they're used
- 如果是build-in类型的变量, 在定义的时候给一个初始化值, 因为不然的话, 其内容 是随机的
- 对于其他非build-in类型来说, 要调用ctor来初始化, 当然也有注意事项:
- 每个构造函数将所有成员初始化
- 初始化顺序和定义顺序相同
- 用member initialization list来进行初始化, 原因有:
- 有些时候有些值是只能初始化,而无法赋值的
- 调用成员初始化列表其实只是调用了一次copy ctor, 而default ctor在赋值的 话,等于除了default ctor又调用了一次operator=
Chapter 2: 构造析构赋值运算
Item 05: 了解C++默默编写并调用哪些函数
- 一个类,如果你没声明的话,编译器会默认给你创建三个函数:
- default ctor
- copy ctor
- copy assignment
- dtor
- 所有这些函数都是public, inline的
- 编译器产生的dtor是non-virtual的
- 如果某个base的类把copy assignment声明为private,那么derived类是不会生成 一个copy assignment的
Item 06: 若不想使用编译器生成的函数,就该明确拒绝
- 为了不让编译器暗自提供某些功能,可以将相应的成员函数声明为private,并且不予 实现
Item 07: 为多肽基类声明virtual析构函数
- 当derived 对象经由base 指针删除, 而base 带着一个non-virtual dtor, 那么其 结果未定义–通常情况下是derived成分没有被销毁
- 如果一个class里面哪怕只有一个virtual函数,那么它一定要有一个virtual的dtor, 如果一个virtual函数都没有,那么它没有想过继承这件事情,设置一个virtual dtor会 增大对象的大小
- 说到多肽, 我们可以有这么一种继承方法:动物类->鸟类->鸵鸟类, 其中鸟类和鸵鸟类 都可以实例化,因为有这种动物, 而动物类就不应该实例化. 为了防止这种类被实例化 c++中给他们了一个类型叫做"抽象基类(abstract base class)", 想让一个类成为 抽象基类很简单, 只需要在这个类里面定义一个纯虚函数即可
- 所谓纯虚函数,就是derived类里面必须要实现,而base类里面不需要实现的函数,其定义
语法比较不常见
class Base { virtual void pure_virtual_example() = 0; };
- 既然有一个纯虚函数就可以成为抽象基类可以了,而且既然是抽象基类肯定是要继承的
那么dtor肯定要是虚函数, 那么我们不妨"合二为一"把dtor设计成纯虚函数, 如下,
需要注意的是,纯虚dtor需要一个定义
#include <iostream> using namespace std; class Base { public: Base() { cout << "Base Ctor" << endl; } virtual ~Base() = 0; virtual void speak() = 0; }; Base::~Base() { cout << "Pure Virtual Dtor Need One Difinition!" << endl; } class De : public Base { public: De() { cout << "De Ctor" << endl; } virtual ~De() { cout << "De Dtor" << endl; } void speak() {} }; int main(int argc, char *argv[]) { De *dd = new De(); delete dd; return 0; }
Item 08: 别让异常逃离析构函数
- c++并不禁止析构函数突出异常,但是它并不鼓励你这么做
- 如果客户需要堆某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个 普通函数(而非在析构函数中)执行该操作
Item 09: 绝对不在构造和析构过程中调用virtual函数
- Base class构造期间, virtual函数不是virtual函数,只是base的一个函数, 其 绝对不可能到derived class阶层
Item 10: 令operator=返回一个reference to *this
- 赋值是可以写成连锁形式的, 其赋值顺序是才用右结合:
x = y = z = 15; // Following is the real order by default x = (y = (z=15));
- 为了实现赋值连锁, 赋值操作符函数必须返回一个reference指向操作符的左侧实参
class Widget { public: Widget& operator=(const Widget& rhs) { // ... return *this; } };
Item 11: 在operator=中处理"自我赋值"
- "自我赋值"发生在对象给自己赋值的时候,如果在这个期间有指针的话,那么事情就有可
能出现问题:
class Bitmap { //.. }; class Widget { private: Bitmap* pb; }; // Wrong Version!! what about if rhs is this Widget& Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; }
- 破解"自我赋值"难题的方法是"证同测试(identity test)"
Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; // identity test delete pb; pb = new Bitmap(*rhs.pb); return *this; }
- 上面这个版本依然有异常方面的麻烦, 如果new Bitmap出现了异常, 那么pb已经被
删除,现在是一个空指针.所以我们要调整前面的语句顺序: 先记录pb的地址,在new
Bitmap之后再删除这个指针
class Widget { Bitmap* pOrig = pb; // remember previous pb // if exception here, the next line delete pOrig will not execute pb = new Bitmap(*rhs.pb); delete pOrig; return *this; }
Item 12: 复制对象时勿忘其每一个成分
- OO编程会把内部都封装起来,而外界留下两个函数进行复制拷贝: copy ctor 和 copy assignment, 我们称之为copying函数
- copying函数应该确保赋值"对象内部所有的成员变量", 和 "所有base class成员"
class Customer { //... }; class PriorityCustomer : public Customer { public: PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority) {} PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { Customer::operator=(rhs); priority = rhs.priority; return *this; }
- 不要尝试某个copying函数取实现另一个copying函数, 应该将共同机能部分放进第 三个函数中(通常叫做init),由两个copying函数共同调用
Chapter 3: 资源管理
Item 13: 以对象管理资源:
- 对c++来说,所谓资源就是申请了就要释放的资源,不仅仅是内存,包括文件描述符,互斥 锁(mutex lock)都要申请了还给系统
- 假设我们有一个投资的类如下:
class Investment { //... };
- 我们通过工厂模式来返回某个Investment对象
// 返回指针,指向动态分配的内存(存储Investment对象), // 调用者有责任释放他们 Investment* createInvestment();
- 使用使用工厂模式返回的对象,我们可以用一下代码来释放它.
void f() { Investment* pInv = createInvestment(); //... delete pInv; }
- 这看起来妥当的方法却很有可能无法正确删除createInvestment返回的对象,因为//…
里面可能出现不正常的情况(无论如何,就是无法到达delete), 比如:
- 过早的return
- 抛出了异常
- 为了应对这种情况,c++设计了auto_ptr来管理资源
void f() { //会在对象离开作用域的时候, 自动调用析构函数保证资源释放 std::auto_ptr<Investment> pInv(createInvestment()); //... }
- 这个简单的例子, 精确的演绎了"以对象管理资源"的两个关键想法:
- 获得资源后立刻放入"管理对象(managing object)"里面
- "管理对象(managing object)"来确保资源被释放
- auto_ptr虽然实现了上面的两个关键想法,但是其为了防止"拷贝的时候,出现多个对象,
然后析构多次", 毅然而然的设计了一种特殊的"复制":一旦copy(或者copy assinment)
就把资源唯一的代理权给新的指针,自己变成null
// pInv1指向createInvestment的返回值 std::auto_ptr<Investment> pInv1(createInvestment()); //现在pInv2指向对象, pInv1为null std::auto_ptr<Investment> pInv2(pInv1); //现在pInv1指向对象, pInv2为null pInv1 = pInv2
- 由于起诡异的复制行为,而STL容器要求其元素发挥"正常的"复制行为,因此,这些容器里面 不能包含auto_ptr成员
- 一个复制行为正常的智能指针是tr1::shared_ptr, 它使用引用计数来持续追踪乖哦能用 多少对象指向某个资源.
- 不过可惜的是,shared_ptr也有很多缺点:无法判别循环引用(两个其实已经没有使用的对 象相互指向, 因而好像感觉都还在"被使用")
- 智能指针(包括auto_ptr和shared_ptr)还有一个特别大的缺点,他们的实现中,析构的时候
总是suppose用户使用的是单个成员,所以只用delete,而不是delete[],所以下面两个例子
都是错误的
// 馊主意,会用错误的delete形式 std::auto_ptr<std::string> aps(new std::string[10]); std::tr1::shared_ptr<int> spi(new int[1024]);
- 最后的最后,请大家注意的是createInvestment返回"未加工指针(raw pointer)", 简直 就是对资源泄漏的死亡邀请,因为调用者极易忘记释放资源.所以我们会在item18里面修改 createInvestment的借口
Item 14: 在资源管理类中小心coping行为
- 复制RAII(Resource Acquisition Is Initialization) 对象必须一并复制它所管理的 资源,所以资源的copying行为决定RAII对象的copying行为
- 普遍而常见的RAII copying行为是:抑制copying,使用引用计数法
Item 15: 在资源管理类中提供对原始资源的访问
- 条款13的例子中使用了智能指针来管理Investment, 但是由于改变了指针的类型(变成了智能
指针型), 所以需要一些转换,有两种方式:
- 显示转换, 智能指针提供的get函数
std::tr1::shared_ptr<Investment> pInv(createInvestment()); //declare int dayHeld(const Investment* pi); //usage int days = dayHeld(pInv.get());
- 隐式转换
- 显示转换, 智能指针提供的get函数
Item 16: 成对的使用new和delete时,要采用相同的形式
- 游戏规则很简单,如果调用new的时候使用了[],那么在对应的delete的时候,也要加上[]
- 如果typedef使用了的话, 那么delete的时候,要注意,不要被typedef的类型蒙蔽
typedef std::string AddressLine[4]; // new Addressline返回一个new string[4] std::string* pal = new AddressLine; //... delete pal; //Not defined delete []pal; //Good
Item 17: 以独立语句将newd对象置入智能指针
- 这里看似是讨论智能指针的问题,其实是讨论c++编译器"灵活性"的一种:参数执行顺序 的灵活性:一个函数的几个参数如果需要计算值作为参数,那么谁先谁后,是无法保证的 (java, c#可以保证)
- 比如我们有个例子,函数processWidget的两个参数pw, priority, 而我们想把函数
priority()的返回值作为priority的值. 某个new Widget的临时指针作为pw的值:
int priority(); //declaration for the function void processWidget(std::tr1::shared_ptr<Widget>, int priority); //usage fo the function processWidget(new Widget, priority());
- 上面的这种语法在编译的时候无法通过,因为tr1::shared_ptr的构造函数虽然是以
一个raw pointer为参数,但是这个构造函数被声明为explicit,所以是无法进行隐式
转换的, 显示转换然后调用的代码如下
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
- 上面看似挺好的代码,却隐藏着资源泄漏的危险, 原因就是我们上面说的c++编译器在
以什么顺序计算参数方面,是没有定数的. processWidget的参数要完成下面三件事:
- 调用priority()
- 执行new Widget, 生成一个临时的指针X
- 把临时指针X作为参数,调用tr1::shared_ptr构造函数
- 上面调用shared_ptr构造函数一定要在生成临时指针之后,但是调用priority(),可以
出现在第一步,第二步,或者最后一步,假设最终是以下面的这个顺序执行的:
- 执行new Widget, 生成一个临时的指针X
- 调用priority()
- 把临时指针X作为参数,调用tr1::shared_ptr构造函数
- 如果在执行priority()的时候抛出了异常, 那么临时指针X就会遗失,因为它尚未放入到 我们自动管理资源的shared_ptr内部.也就不可能被释放了.
- 解决的方法也很简单:使用分离的语句1,创建Widget. 2,讲Widget放入智能指针内部
std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
Chapter 4: 设计与声明
Item 18: 让接口容易被正确使用,不易被误用
- "促进正确使用"的办法包括接口的一致性,以及与内置类型行为的兼容
- "阻止误用"的办法包括建立新类型, 限制类型操作, 束缚对象值, 以及消除客户的资 源管理责任
Item 19: 设计class犹如设计type
- 在设计每个class的时候,回答以下问题:
- 新type对象应该如何创建和销毁
- 对象的初始化和对象的赋值操作有什么差别
- 新的type的对象如果被passed by value意味着什么 (copy构造函数用来定义一个 type的pass-by-value如何实现)
- 什么是新type的"合法值"
- 需要继承什么
- 新type需要什么样的转换
- 什么样的操作符和函数对次type而言是合理的
- 什么样的标准函数应该被驳回(声明为private)
- 函数该采用public,private还是protected
- 什么是新type的"未声明接口"
- 新的type有多一般化,如果一般化的够多,比如integer, string等都要一般化的化, 就要考虑generic编程了
- 你真的需要一个新的type么
Item 20: 宁以pass-by-reference-to-const替换pass-by-value
- pass-by-reference-to-const效率高的多, 因为没有任何构造或者析构函数的调用(没 有新的对象被创建)
- pass-by-reference-to-const不仅仅是效率高,而且可以避免对象切割(slicing)问题: 当一个derived对象以by value的方式传递到一个并被视为base对象, 那么base class的 拷贝构造函数会被调用而"造成此对象的行为像个derived class对象"的那些特质化的性质 全被切割掉了
- 不用pass-by-reference-to-const的唯一情况是1)内置类型2)STL的迭代器和函数对象
Item 21: 必须返回对象时, 别妄想返回其reference
- 说到返回值,在c++中是可以返回一个对象的.方法入下,其实这就是最正常的写法
inline const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.n * rhs.n, lhs.d * rhs.d); }
- 在Item20的影响下,可能会想到返回值"也用reference", 其实这是非常危险的, 因为reference
不是单独存在的,一个reference一定有一个"实体", 比如下面这个例子. 返回值reference其实
对应的是一个local的result对象, 一旦从函数返回,那么local的result就被销毁了.返回一个
已经销毁的,不存在的"实体"的reference是非常可怕的
const Rational& operator* (const Rational& lhs, const Rational& rhs) { //警告!糟糕的代码!! Rational result(lhs.n * rhs.n, lhs.d * rhs.d); return result; }
- 我们稍微改变一下,在heap里面创建一个对象,然后返回,例子如下.
const Rational& operator* (const Rational& lhs, const Rational& rhs) { Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d*); return *result; }
- 这个例子其实也是不行的,因为释放内存的责任是调用者的,那就有可能会因为无法找到heapd的句柄,
无法释放内存,而发生内存泄漏. 比如下面, 调用了两次operator*, 那么就要释放两次内存,但是
却没有办法让operator* 使用者进行delete调用, 因为没有合理的办法让他们取得operator*返
回的references背后隐藏的那个指针.
Rational w, x, y, z; w = x * y *z;
Item 22: 讲成员变量声明为private
- 切记将成员变量声明为private, 因为这样有很多好处:
- 可以实现更精确的访问控制:如果是public的话,谁都可以访问,但是private你却可以设置成: 只读,不准访问,读写访问,只写访问, 等等
- 一个变量声明为public那就意味着永远不可改变, 声明为private给了class作者有了更大的 实现弹性
- protected其实是历史一流, 和public一样,没有任何的封装性.
Item 23: 宁以non-memeber, non-friend替换member函数
- class的member函数和friend函数在访问private数据方面的权利是一样的,所以具有一样的, 所以封装效果是一样的.这里比较的是non-member, non-friend函数和member函数. 我们的 原则是:如果一个member函数可以用non-member,non-friend函数来替代,那么就替代
- 下面是一个webBroswerStuff的例子, 这个例子有三个member函数,其中clearBrowser其实
可以用一个non-member, non-friend函数代替,这样就是增加了class的封装性
class WebBrowser{ void clearCache(); void clearHistory(); void removeCookies(); void clearBrowser() { clearCache(); clearHistory(); removeCookies(); } };
- 用non-member, non-friend函数代替可以代替的member函数,并且使用一个namespace,是
c++常用的方法
namespace WebBrowserStuff{ class WebBrowser{ void clearCache(); void clearHistory(); void removeCookies(); }; void clearBrowser(WebBrowser& wb){ wb.clearCache(); wb.clearHistory(); wb.removeCookies(); } }
- 话说,namespace是STL里面组织代码的主要方式,因为namespace可以跨越不同的文件 (class不行), std命名空间分布在数十个头文件(<vector>, <algorithm>, <memory> 里面等等), 用户想用某个就include某一个头文件. (class就不行,class无法分割)
Item 24: 若所有参数皆需类型转换,请为此采用non-member函数
- 这个item其实是讲如何在c++中把事情做对,而不是怎样做更好. 比如,一开始把operator* 实现成成员变量的化,碰到result = 2 * oneHalf, 必然会编译器报错,然后修改也不迟.
- 错误的operator* 实现
class Rational { pulic: //... // NOT work for result = 2 * oneHalf const Rational operator*(const Rational& rhs) const; };
- 正确的operator*是一个non-member, 借用numberator()和denominator()函数读取
private值,而不是设计成有元函数
class Rational { //... }; const Rational operator*(lhs.numberator() * rhs.numberator(), lhs.denominator() * rhs.denominator()) { return Rational(lhs.numberator() * rhs.numberator(), lhs.denominator() * rhs.denominator()); }
Item 25: 考虑写出一个不抛出异常的swap函数
- 当std::swap对你的类型效率不高的时候,提供一个swap成员函数, 并确定这个函数不 抛出异常
- 如果你提供了一个member swap,也应该提供改一改non-member swap来调用前者,对于 classes(而不是templates), 也请特化std::swap
- 对自己特例化的swap调用方式是先using, 再调用swap,而且没有直接写std::swap,这
源于编译器对namespace的名字查找时, 遵循的一系列规则
class Widget { public: //... void swap(Widget& other) { using std::swap; swap(pImpl, other.pImpl); } //... }; namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { a.swap(b); } }
Chapter 5: 实现
Item 26: 尽可能延后变量定义式出现时间
- 在老的c语言规范里面(C89/90)里面, 变量只能定义在block开始的地方.注意!!不是 function开始的地方, 即便是C89/90, 把所有变量都定义在函数开始的地方是非常 不明智的, 因为这样会导致逻辑不清晰, 变量作用域模糊的代码
- 对于C++来说,变量"直到需要的时候"才定义,有更多的好处:
- 变量作用域更清晰, 下面的成本如下(但是方法B更清晰,作用域更准确):
- 方法A : 1个构造函数 + 1个析构函数 + n个赋值操作
- 方法B : n个构造函数 + n个析构函数
// 方法A, 定义于循环外 Widget w; for (int i = 0; i < n; ++i) { w = 取决于i的某个值; //... } // 方法B, 定义于循环内 for (int i = 0; i < n; ++i) { Widget w(取决于i的某个值); //... }
- 防止"定义了不使用"(由于异常的存在,当然异常现在不推荐使用)
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; //May NOT be used! }
- 直到真正需要的时候,就意味着大多数情况下这个变量已经有了一个靠谱的初始化
值,那么就可以用"直接在构造的时候指定初始值", 这效率比起"通过default构造
函数构造出一个对象,然后对它复制"效果好
std::string encryptPassword(const std::string& password) { // 检查长度 std::string encrypted(password); encrypt(encrypted); return encrypted; }
- 变量作用域更清晰, 下面的成本如下(但是方法B更清晰,作用域更准确):
Item 27: 尽量少做转型动作
- c语言风格的转型有以下两种(其实也只是小括号摆放位置不同而已),因此我们称之为"旧
式转型"(old-style casts)
(T)expression //将expression 转型为T T(expression) //将expression 转型为T
- c++有四种新型的转型:
- const_cast 将对象的常量性去除(cast away the constness)
- dynamic_cast 执行"安全向下转型" (safe downcasting), 也就是用来决定某对象 是否归属继承体系中的某个类型. 换句话说就是,你想在一个你认定了肯定是derived class 对象身上执行derived class操作函数, 但是你手上却只有一个"指向base" 的pointer或者reference,你只能依靠他们来处理,所以你想到了把它dynamic_cast 到"你认为的类型"
- reinterpret_cast意图执行低级转型, 由于取决于编译器,也即表示它不可移植
- static_cast用来强迫隐式转换(implicit conversions).比如将non-const对象 转换为const对象, 或者将int转换为double. 但是无法将const转换为non-const, 只有const_cast才能做到
- c++规则设计目的之一就是, 保证"类型错误"绝对不可能发生.理论上如果你"很干净"的 通过编译,就表示它并不企图在任何对象身上执行任何不安全,无意义的操作. 这是一个 极具价值的保证,不要轻率的放弃.
- c++的新型转型语法更受欢迎, 因为:
- 很容易在代码中被辨识出来(相比较老的类型转换就一个括号), 也就容易查找到问题
- 新型类型转换分成了四种,每种功能都限定在一个比较小的范围内,便于让编译器找到 问题
- 虽然c++的转型非常的好,但是有时候我们还是会使用"旧式转型",比如调用一个explicit
构造函数,把一个对象传递给一个函数的时候.很多时候这也是c++里面推荐的,唯一使用"旧
式转型"的情况(其实也可以使用新式转型):
class Widget { public: explicit Widget(int size); // ... }; void doSomeWork(const Widget& w); // 以一个int加上"旧式函数风格"的转型动作创建了一个Widget doSomeWork(Widget(15)); // 以一个int加上"新式C++风格"的转型动作创建了一个Widget doSomeWork(static_cast<Widget>(15));
- 转型不是简单的告诉编译器把某种类型视为另一种类型, 它往往会零编译器编译出"运行期间
执行的代码",比如下面的例子,由于底层机制的不同,把int转换成double,几乎肯定会产生一
些代码
int x, y; // ... double d = static_cast<double>(x) / y;
- 在继承体系中使用cast,也会产生"运行时代码", 因为c++支持多重继承,所以一个derived
对象可能拥有两个地址(Base1指向时候的地址,或者Base2指向时候的地址). 当然,有些实现
中单继承一会出现一个Derived的对象拥有两个地址的奇怪景象!
#include <iostream> using namespace std; class B1 { int i; }; class B2 { int i; }; class D : public B1, public B2 { int i; }; int main(int argc, char *argv[]) { D aD; B1* pb1 = &aD; B2* pb2 = &aD; cout << &aD << endl; ////////////////////////////////////////////////////// // There's no possible way for the B1 sub-object to // // have the same address as the B2 sub-object. // ////////////////////////////////////////////////////// cout << pb1 << endl; cout << pb2 << endl; return 0; } /////////////////////////////////////////////// // <==========running the test.out=========> // // // // 0x28fecc // // 0x28fecc // // 0x28fed0 // // // // <==========test.out ends here===========> // ///////////////////////////////////////////////
Item 28: 避免返回handles指向对象内部成分
- 假设我们要有一个"矩形"类, 其成员是两个点, 左上和右下.那么我们就有如下的类的
基础代码:
class Point { public: Point(int x, int y); // ... void setX(int newVal); void setY(int newVal); }; struct RectData { Point ulhc; // ulhc = "upper left-hand corner" Point lrhc; // lrhc = "lower right-hand corner" }; class Rectangle { // ... private: std::tr1::shared_ptr<RectData> pData; };
- 函数需要对外公开"左上"和"右下"的数值, 所以有下面两个函数.
class Rectangle { public: // ... Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } };
- 这两个函数可以通过编译,确是两个个错的函数,因为他们破坏了封装性!不是声明为了
const了么, 只有const对象可以调用这两个函数,为什么还破坏了封装性? 原因请看
下面的例子.虽然rec 是const的,不可更改,但是因为你返回了内部Point成员变量的
reference,可以利用这个reference来改动内部数据
Point coord1(0.0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); // rec is const, but rec.upperLeft() is Point&, and // can be changed rec.upperLeft().setX(50);
- 上面的例子是返回了一个reference,其实指针或者迭代器和reference的效果是一样 的, 它们三个都叫做handles(号码牌,用来取得某个对象)
- 上面的例子,稍加修改(返回const reference)就可以保证其封装性.
class Rectangle { public: // ... const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; } };
- 上面这种实现方法, 虽然勉强可以使用(operator[]就是这么实现的.),但深究起来,
还是会有dangling handles(空悬号码牌的问题), 比如下面的例子,GUIObject作
为一个参数传入,求它的"左上右下"矩形数据.(boundingBox(*pgo))整个这一部分
会产生一个临时的Rectangle对象tmp, 随后upperLeft,作用于temp身上, 返回一
个指向temp内部成分的reference(也就是temp的Point).这个语句过后, temp
就会被销毁,间接导致temp的Point也会析构, 也就是说pUpperLeft一开始就指向
了一块已经析构了的内存地址!
class GUIObject { // ... }; // 以 by value方式返回, 一定要声明为const, 可以防止不小心被'='赋值 const Rectangle boundingBox(const GUIObject& obj); // 用户可能会如下使用, 因为临时对象的产生,所以会导致dangling handles GUIObject* pgo; // ... const Points* pUpperLeft = &(boundingBox(*pgo).upperLeft());