五.实现
大多数情况下,适当提出拟的类定义以及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长建置时间。
条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并为被使用,仍需耗费这些成本,所以应该尽量避免这种情形。
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted1;
if (password.length() < MinimumPasswordLength)
{
throw logic_error(“Password is too short”); //注意:可能抛出异常
}
string encrypted2;
…
return encrypted;
}
如上代码,encrypted在2处定义是个不错的选择,因为如果抛出异常,那么encrypted的构造和析构可是做了无用功啊!
还有一点要注意:“通过默认构造函数构造出一个对象然后对它赋值”比“直接在构造函数时制定初值”效率差。
“尽可能延后”的真正意义应该是:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
//方法A:定义循环外
Widget w;
for (int i = 0; i < n; ++i)
{
w = some value dependent on i;
...
} //1个构造函数+1个析构函数+n个赋值操作;
//方法B:定义循环外
for (int i = 0; i < n; ++i)
{
Widget w(some value dependent on i);
...
} //n个构造函数+n个析构函数
除非:1.你知道赋值成本比“构造+析构”成本低;2.你正在处理代码中效率高度敏感的部分,否则应该使用方法B。
请记住:
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27:尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
C风格的转型动作看起来像这样:
(T)expression //将expression转型为T
函数风格的转型动作看起来像这样:
T(expression) //将expression转型为T
C++还提供四种新式转型:
const_cast:通常被用来将对象的常量性转除;即去掉const。
dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
static_cast:用来强迫隐式转换,例如将non-const转型为const,int转型为double等等。
尽量使用新式转型:
它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程。
各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
请记住:
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
条款28:避免返回handls指向对象内部成分
struct RectData
{
Point ulhc;
Point lrhc;
};
class Rectangle
{
public:
...
Point& upperLeft() const { return pData->ulhc; }1//const只对函数内进行保护,函数返回后呢??
Point& lowerRight() const { return pData->lrhc; }2 //const只对函数内进行保护,函数返回后呢??
private:
std::tr1::shared_ptr<RectData> pData;
...
};
1,2两函数都返回引用,指向private内部数据,调用者于是可通过这些引用更改内部数据!这严重破坏了数据的封装性,对私有成员进行直接操作?太不可思意了!
const Point& upperLeft() const { return pData->ulhc; }3
const Point& lowerRight() const { return pData->lrhc; }4
或者将1,2改为3,4,这就限制了客户的“涂改权”,只有“读取权”。
但终究“返回一个handle代表对象内部成分”总是危险的。特别是将返回的指针或引用赋值给其它指针或引用,那么久造成了“悬空”。
不要令成员函数返回一个指针指向“访问级别较低的”成员。
请记住:
避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的
请记住:
异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外
Inline函数,多棒的点子!它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码。
inline造成的代码膨胀会导致额外的换页行为,降低指令的高速缓存的命中率,从而带来效率损失。
如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式钱加上关键字inline。
Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道哦啊它长什么样子。
Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
一个表面上看似inline的函数是否真实inline,取决于你的建置环境,主要取决于编译器。
有的时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。
对程序开发而言,将上述所有考虑牢记在心很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。
这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。
请记住:
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
不要只因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件间的编译依存关系降至最低
连串编译依存关系会对许多项目造成难以形容的灾难
以“声明的依存性”替换“定义的依存性”
如果使用对象引用或者对象指针能够完成任务,就尽量不要使用对象。
如果能够,尽量以class声明替换class定义式
请记住:
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。
程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
六.继承与面向对象设计
如果你了解C++各种特性的意义,你会发现,你对OOP的看法改变了。它不再是一项用来划分语言特性的仪典,而是可以让你通过它说出你对软件系统的想法。一旦你知道该通过它说些什么,移转至C++世界也就不再是可怕的高要求了。
条款32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。
如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)
好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
请记住:
“public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。
条款33:避免遮掩继承而来的名称
C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否是相同或不同的类型,并不重要。即,只要名称相同就覆盖基类相应的成员,不管是类型,参数个数,都无关紧要。派生类的作用域嵌套在基类的作用域内。
C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。
如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式,引入基类的成员函数。
请记住:
derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。
条款34:区分接口继承和实现继承
纯虚函数的目的:为了让派生类只继承函数接口。我们也可以为纯虚函数定义,但是只能通过明确指出其类名称才行。
非纯虚函数的目的:为了让派生类继承这个函数的接口和缺省的实现
非纯虚函数也会引起一些问题,派生类应该重新定义继承而来的非纯虚函数,但是忘记了调用了缺省行为,那么就会导致问题的出现。这种方法可以避免。在父类中定义两个类,一个是纯虚函数,一个默认行为的函数。就是讲非纯虚函数一分为二了。但是上述的解决方法也存在着问题。我们利用纯虚函数必须在派生类重新声明,但是它们也可以拥有自己的实现。
非虚函数表现的是不变性。这种函数的目的在于令派生类继承函数的接口以及一份强制性实现
上面说明3种函数对于派生类的作用和意义
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。
成员函数的接口总是会被继承。
声明一个纯虚函数的目的是为了让派生类只继承函数接口。
声明一个虚函数的目的是让派生类继承该函数的接口和缺省实现。
声明一个非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。
请记住:
接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
pure virtual函数只具体制定接口继承。
简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
non-virtual函数具体制定接口继承以及强制性实现继承。
条款35:考虑virtual函数以外的其它选择
利用non-virtual interface手法实现模板设计模式
令客户通过public non-virtual成员函数调用私有的虚函数。这种方式的优点就是non-virtual成员函数确保了虚函数被调用前设定好适当的场景,在虚函数调用之后整理场景。
利用函数指针实现策略模式
不同人物类型计算健康指标的函数不同,在构造函数中传入计算健康值函数。这种方法是策略设计模式简单应用。这种方法非成员函数无法访问类中的非公共成员了
请记住:
virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
条款36:绝不重新定义继承而来的non-virtual函数
如果在派生类中定义了与父类一样的函数名,会带来一些问题
clss A{
void mf();
...
};
class B :public A{
void mf();
...
};
B b;
A *pa=&b;
B *pb=&b;
pa->mf();
pb->mf();
上面两个对象调用mf函数的结果会不一致。因为非虚函数都是静态绑定的。
请记住:
绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
虚函数是静态绑定的,缺省的参数值时静态绑定的。
如果重新定义在派生类虚函数的缺省参数值,在调用一个定义在派生类的虚函数,却使用了基类为它所指向的缺省参数值
如果在派生类中重复给虚函数提供缺省参数值,会带来代码重复问题,这种重复又带有相依性,一旦基类的缺省值改变了,派生类也必须改变,不然就会存在问题。
对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数系动态绑定(dynamically bound),而缺省参数值确实静态绑定(statically bound)。意思是你可能会在“调用一个定义于派生类内的虚函数”的同时,却使用基类为它所指定的缺省参数值。
请记住:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
条款38:通过符合塑模出has-a或“根据某物实现出”
复合有两个意义:第一是有一个(has a),第二是根据某物实现出。当复合发生在应用域的对象之间,表现出has a的关系,当它发生在实现域内就表现出根据某物实现出的关系。
比较麻烦是区分is a和根据某物实现出这两种关系。复用在根据某物实现出,是找到了一个结构,可以对其进行改造实现自己的目的。
例如:set的实现可以复合已经实现了的链表结构
template<typename T> class Set:public list<T>
{...};
list对象为真的时候对于set对象不一定为真,比如说list中可以存在重复的元素。这两个类并不是is a的关系,公共继承并不适合,正确的做法如下:
template<typename T> class Set
{
public:
member(const T& item)const;
void insert(const T& item);
void remove();
...
private:
list<T> rep;
};
请记住:
复合(composition)的意义和public继承完全不同。
在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。
条款39:明智而审慎地使用private继承
在私有继承中,编译器不会把派生类对象自动转化为基类对象。私有继承而来的所有成员对派生类而言都是私有的。私有继承意味着根据某物实现出,不是像public是is a的关系。私有继承只是一种实现技术。
条款40:明智而审慎地使用多继承
多继承会带来多歧义的问题,可能多个基类存在名字相同的函数,而在派生类中调用时这个函数时,就存在了问题。要解决这个问题,就必须要指明调用的是那个基类的函数
钻石型多继承。
对于虚继承的建议:非必要不要使用虚继承。如果必须使用虚继承,那么尽量避免在虚基类中放置数据,这样就不用担心类中数据初始化的问题了。
多继承有正当用途,其中当涉及到公共继承某个接口类和私有继承某个协助实现的类的两个组合。