effective C++ 3rd 笔记(三)
时间:2011-04-14 来源:Atela
条款26: 尽可能延后变量定义式的出现时间
//方案A和B哪个比较好? //方案A Widget w; for (int i = 0; i < n; ++i) { w=...; } //方案B for (int i = 0; i < n; ++i) { Widget w(...); }
方案A:1次构造,1次析构,n次赋值
方案B:n次构造,n次析构
除非你知道(1)赋值成本小于构造+析构 (2)你正在处理代码中效率高度敏感的部分
否则应该选择方案B
条款27:尽量少做转型动作
(T)expression //C风格 T(expression) //函数风格 //C++新式转型 const_cast(expression) //常量性移除,唯一有此能力的转型 dynamic_cast(expression) //安全向下转型,唯一无法由旧时语法执行的动作 reinterpret_cast(expression)//低级转型,实际动作可能取决于编译器,如int*转为int,条款50 static_cast(expression) //强迫隐式转型,non-const转const,int转double,void*转typed指针,pointer to base转pointer to derived //最好唯一使用旧时转型的时机: 调用explicit构造函数将对象传给函数 class Widget { public: explicit Widget(int size); }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); doSomeWork(static_cast(15)); // create Widget from int with C++-style cast
转型不是告诉编译把某种类型视为另一种类型! 任何一个类型转换往往真的另编译器编译出运行期执行的码
int x, y; double d = static_cast(x)/y; //用浮点除法 Dereved d; Base *pb = &d; //Derived* 转为Base* //两个指针指可能不同,可能会有个偏移量offset在运行期被施于Derived指针上,用以取得正确的Base*
单一对象(如一个Derived对象),可能拥有一个以上的地址(如以Base*指向的地址和以Derived*指向的地址)。
因此,避免做出对象在C++中如何布局的稼穑,更不改以此假设为基础执行任何转型动作。如将对象地址转为char*,然后在上面进行指针算术,几乎总会未定义。
class Window { public: virtual void onResize() { ... } ... }; class SpecialWindow: public Window { public: virtual void onResize() { static_cast(*this).onResize(); //error,在当前对象的base class成分的副本上调用Window::onResize() ... // 当前对象上执行SpecialWindow专属动作 } }; //解决之道 class SpecialWindow: public Window { public: virtual void onResize() { Window::onResize(); // call Window::onResize ... // on *this } ... };
dynamic_cast通常是你想在一个你认定为derived class对象上执行derived class操作函数,但你手上只有base*或引用。
可使用类型安全容器,或将virtual函数往继承体系上方移动,替换dynamic_cast转型。 不要使用连串dynamic_cast。
条款28: 避免返回handles指向对象内部成分
避免返回handles(包括引用,指针,迭代器)指向内部数据。遵守这个条款可以增加封装性,帮助const成员函数像个const,并将发生悬吊号码牌dangling handles的可能性降至最低。
但不意味绝对不可以让成员函数返回handles。如operator[]允许你摘采string,vector的个别元素,这些数据会随着容器的销毁而销毁。而这样的函数只是例外。
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 pData; }; class Rectangle { public: Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } }; Point coord1(0, 0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); rec.upperLeft().setX(50); //rec是const,upperLeft是const函数,但内部数据被更改了 //解决方案,向外传递内部数据引用的成员函数申明为const class Rectangle { public: const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; } } //但依然有问题,可能导致空悬的号码牌dangling handles: handles所指东西(的所属对象)不复存在 //考虑下例 class GUIObject { ... }; const Rectangle boundingBox(const GUIObject& obj); GUIObject *pgo; const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft()); //boundingBox返回一个const Rectangle临时对象temp,然后upperLeft返回temp的一个const Point引用 //然后另pUpperLeft指向那个Point //但是当这个语句结束后,临时对象temp被销毁,导致temp内的Points析构,pUpperLeft成为悬垂指针
条款29: 为异常安全而努力是值得的。
以对象管理资源管理资源解决资源泄漏,然后根据“现实可施作”条件下挑选3个异常安全保证中的最强烈等级,只有在你的函数调用了传统代码,才别无选择的设为无任何保证。
class PrettyMenu { public: void changeBackground(std::istream& imgSrc); private: Mutex mutex; Image *bgImage; int imageChanges; }; void PrettyMenu::changeBackground(std::istream& imgSrc) lock(&mutex); delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); unlock(&mutex); }
异常安全的两个条件:当异常抛出时
- 不泄露任何资源: new Image(imgSrc)抛出异常,unlock(&mutex); 就不会执行了
- 不允许数据败坏: new Image(imgSrc)抛异常, bgImage就指向已被删除的对象,imageChanges也已被增加
//避免资源泄露,条款13,14,以对象管理资源 void PrettyMenu::changeBackground(std::istream& imgSrc){ Lock ml(&mutex); delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); }
异常安全函数提供三个保证:
- 基本承诺: 如果异常被抛出,程序内任何事物仍然保持在有效状态下。程序有可能处于任何状态--只要该状态合法。
- 强烈保证: 如抛出异常,程序状态不改变。如果函数成功,就是完全成功;函数是不程序会回复到调用函数之前的状态。
- 不抛掷(nothrow)保证: 承诺绝不抛出异常,因为它们总是能为完成它们原先承诺的功能
int doSomething() throw(); //空白的异常明细,如果抛出异常,则会调用unexpected(),其中调用terminate(),可用set_expected()设置默认函数
class PrettyMenu { ... std::tr1::shared_ptr<Image> bgImage; ... }; void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); bgImage.reset(new Image(imgSrc));//delete在reset内部被调用,没进入reset则不会调用,提供强烈保证 ++imageChanges; }
美中不足的imgSrc:如果Image构造函数抛出异常,有可能输入流input stream的读取记号read marker(如读取指针)已被移动,而这样的搬移对程序其余部分是一种可见的状态改变。再读imgSrc就从先前的后面开始读取。
所以changeBackground在解决这个问题前只提供基本的异常安全保证。
强烈保证另一种方案: copy and swap 创建副本,对副本操作,若有异常,原始对象不变,若成功再在不抛出异常的swap中交换
//pimpl idiom手法:将隶属于对象的数据从原对象放进另一对象内,然后赋予原对象一个指针指向那个所谓的实现对象。条款31 struct PMImpl { std::tr1::shared_ptr<Image> bgImage; int imageChanges; }; class PrettyMenu { private: Mutex mutex; std::tr1::shared_ptr pImpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // see Item 25 Lock ml(&mutex); // acquire the mutex std::tr1::shared_ptr // copy obj. data pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc)); // modify the copy ++pNew->imageChanges; swap(pImpl, pNew); // swap the new } // release the mutex
copy and swap 不保证整个函数有强烈的异常安全。
void someFunc() { ... //对local状态做一份副本 f1(); f2(); ... //将修改后的状态置换回来 }
如果f1或f2异常安全性比强烈保证低,则someFunc很难获得强烈安全。
如果f1或f2都是强烈异常安全的,情况并不就此好转。如果f1圆满结束,程序状态有所改变。随后f2异常,程序状态和someFunc被调用前不会相同。
copy and swap的创建副本可能耗用无法供应的时间和空间,所以”强烈保证”并非在任何时刻都显得实际。所以有时候基本保证是个绝对通情达理的选择。
条款30:通彻了解inlining的里里外外
inline函数: 对此函数的每一个调用都以函数本体替换之。 因此可能增加目标码object code的大小,在一台内存有限的机器上,过度热衷inline会造成程序体积过大,即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而立的效率损失。
而如果inline函数本体很小,编译器针对函数本体所产出的码可能针对函数调用所产出的码更小,可导致较小的目标码和较高的指令高速缓存装置击中率。
inline只是对编译器的一个申请,不是强制命令。
隐式声明inline: 将函数定义于class定义式内。 显示声明: 在函数定义式前加inline。
inline在大多数C++程序中是编译期行为。
如果你在写一个template而你认为所有根据次template具现出来的函数都应该为inline,则将此template声明为inline。否则不声明。
大多数编译期拒绝太过复杂(循环或递归)的函数inlining,而所有virtual函数调用(除非是最平淡无奇的)也都会使inlining落空(运行期行为)。
如果程序要取某个inline函数的地址或指针调用函数,编译器通常不对该函数实施inlining。
构造函数和析构函数往往是inlining的糟糕候选人。
当你使用new时,动态创建的对象被其构造函数自动初始化;当你使用delete时,析构函数被调用;
当你创建一个对象,其每一个base class及每一个成员变量都会被自动构造;当你销毁一个对象,反向程序的析构行为会自动发生;
如果有个异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。
Derived::Derived(){ //空白Derived构造函数的观念性实现 Base::Base(); // initialize Base part try { dm1.std::string::string(); } // try to construct dm1 catch (...) { // if it throws, Base::~Base(); // destroy base class part and throw; // propagate the exception } try { dm2.std::string::string(); } // try to construct dm2 catch(...) { // if it throws, dm1.std::string::~string(); // destroy dm1, Base::~Base(); // destroy base class part, and throw; // propagate the exception } try { dm3.std::string::string(); } // construct dm3 catch(...) { // if it throws, dm2.std::string::~string(); // destroy dm2, dm1.std::string::~string(); // destroy dm1, Base::~Base(); // destroy base class part, and throw; // propagate the exception } }
inline函数改变后,所有用到该函数的程序都要重新编译,而non-inline函数则只需重新连接就好。
大部分调试器对inline函数调试束手无策。
条款31: 将文件间的编译依存关系降至最低。
Person的文件和它的头文件之间建立了编译依赖关系。如果任一个辅助类(即string, Date,Address和Country)改变了它的实现,或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。
解决方法: 用前置声明替换#include头文件, 声明的类型只能被使用为引用或指针,或在函数的声明中。
//方案一: handle class,pimpl idiom //person.h #ifndef PERSON_H #define PERSON_H #include // 不能前置声明,因为std::string是个basic_string类型的typedef #include // for tr1::shared_ptr; see below class PersonImpl; // 前置声明 class Date; // 前置声明 class Address; // 前置声明 class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: // ptr to implementation; std::tr1::shared_ptr pImpl; // see Item 13 for info on }; // std::tr1::shared_ptr #endif //person.cpp #include "Person.h" // we're implementing the Person class, #include "PersonImpl.h" // we must also #include PersonImpl's class Person::Person(const std::string& name, const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name, birthday, addr)) { } std::string Person::name() const { return pImpl->name(); } std::string Person::birthDate() const { return pImpl->birthDate(); } //PersonImpl.h #ifndef PERSONIMPL_H #define PERSONIMPL_H #include "Date.h" #include "Address.h" class PersonImpl { public: PersonImpl(const std::string& name, const Date& birthday, const Address& addr):theName(name),theBirthDate(birthday),theAddress(addr){} std::string name() const { return theName; } std::string birthDate()const { return theBirthDate.toString();} private: std::string theName; // implementation detail Date theBirthDate; // implementation detail Address theAddress; // implementation detail }; #endif //Date.h #ifndef DATE_H #define DATE_H class Date { public: Date(int m, int d, int y):month(m),day(d),year(y){ } std::string toString() const{ char buff[20];sprintf(buff,"today is %d %d %d",month,day,year);return buff;} private: int month; int day; int year; }; #endif
设想下如果为Date.h被修改,包含它的PersonImpl.h就需要重新编译,person.cpp要重新编译,而person.h不变,所以外部程序包含person.h不需要重新编译。
而如果PersonImpl增加删减一个成员变量,构造函数就要改变,person.cpp就需要重新编译,如果因此person.h的构造函数及因增减的成员相关的成员函数变化了,即person本身的接口变了,那么包含person.h的所有文件都要重新编译。但是需要使用Person类的类也可以在改类头文件中使用前置声明, 然后只需重新编译该类,而包含该类头文件的其他类就不需要重新编译了。
所以本条款就是把编译相关的文件局部于一定的的文件层次中。
另外可以为声明式和定义式提供不同的头文件: 文件需要保持一致性,如<iosfwd>包含iostream的各组件的声明式,其定义则分布在<sstream><streambuf><fstream><iostream>
//方案二: Interface class //Person.h #ifndef PERSON_H #define PERSON_H #include // standard library components // shouldn't be forward-declared #include // for tr1::shared_ptr; see below class Date; // forward decls of classes used in class Address; // Person interface class Person { public: static std::tr1::shared_ptr // return a tr1::shared_ptr to a new create(const std::string& name, // Person initialized with the const Date& birthday); // why a tr1::shared_ptr is returned virtual std::string name() const = 0; virtual std::string birthDate() const = 0; }; #endif //Person.cpp #include "Person.h" #include "RealPerson.h" std::tr1::shared_ptr Person::create(const std::string& name, const Date& birthday){ return std::tr1::shared_ptr(new RealPerson(name, birthday)); } //RealPerson.h #ifndef REALPERSON_H #define REALPERSON_H #include "Date.h" class Address; class RealPerson: public Person { public: RealPerson(const std::string& name, const Date& birthday) : theName(name), theBirthDate(birthday) {} virtual ~RealPerson() {} std::string name() const {return theName;} std::string birthDate() const{return theBirthDate.toString();} private: std::string theName; Date theBirthDate; }; #endif
因为Person是抽象类不能创建对象实例,而在现实中,Person::create可能会创建不同的类型的derived class对象,取决于额外参数值, 读自文件或数据库的数据,环境变量等。
而无论handle class还是 interface class 都必然会使你在运行期付出额外的空间和时间代价。
条款32: 确定你的public 继承塑膜出is-a关系
public继承意味is-a, 适用于base classes身上的每一件事一定也适用于derived classes,因为每一个derived classes也都是一个base class对象。
class Bird { public: virtual void fly(); // birds can fly }; class Penguin:public Bird { // penguins are birds };// 企鹅会飞吗??? /*********************************************************/ class Bird { ... // no fly function is declared }; class FlyingBird: public Bird { public: virtual void fly(); }; class Penguin: public Bird { ... // no fly function is declared }; /*********************************************************/ void error(const std::string& msg); // defined elsewhere class Penguin: public Bird { public: virtual void fly() { error("Attempt to make a penguin fly!");} }; //不是说企鹅不会飞,而其实是在说企鹅会飞,但尝试那么做是一种错误。。
当然如果设计的类无关乎会不会飞,就最好了。。。
class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; // return current values virtual int width() const; }; void makeBigger(Rectangle& r) //增加矩形面积,高不变,宽增加 { int oldHeight = r.height(); r.setWidth(r.width() + 10); assert(r.height() == oldHeight); } class Square: public Rectangle {...}; Square s; assert(s.width() == s.height()); // this must be true for all squares makeBigger(s); //正方形能高不变只增加宽吗,这样还是正方形吗???? assert(s.width() == s.height()); // this must still be true for all squares
条款33: 避免遮掩继承而来的名称
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没人希望如此
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数
class Base { private: int x; public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(); void mf3(); void mf3(double); }; class Derived: public Base { public: virtual void mf1(); void mf3(); void mf4(); }; Derived d; int x; d.mf1(); // fine, calls Derived::mf1 d.mf1(x); // error! Derived::mf1 hides Base::mf1 d.mf2(); // fine, calls Base::mf2 d.mf3(); // fine, calls Derived::mf3 d.mf3(x); // error! Derived::mf3 hides Base::mf3 //遮掩名称,和类型无关 void Derived::mf4(){ mf2();} //查找顺序local->Derived->Base->Base 的namespace-> global
可以在Derived class内用using声明被遮掩的Base class名称。
class Derived: public Base { public: using Base::mf1;// 让Base class内名为mf1的所有东西都在Derived作用域内可见 virtual void mf1(); void mf3(); void mf4(); };
如果不想继承base class所有函数,只让部分函数可见
转交函数forwaring function
class Base { public: virtual void mf1() = 0; virtual void mf1(int); }; class Derived: private Base { //因为public继承意味is-a关系,不能遮掩Base public: virtual void mf1() { Base::mf1(); } //转交函数, 隐式inline }; Derived d; int x; d.mf1(); // fine, calls Derived::mf1 d.mf1(x); // error! Base::mf1() is hidden
条款34: 区分接口继承和实现继承
- 成员函数的接口总是会被继承。public继承意味is-a,所以某个函数可以用于base class身上,也一定可以用于它的derived classes。
- 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口
- 声明impure virtual(非纯虚)函数的目的,是为了让derived classes继承该函数的接口和缺省实现。
- 声明non-virtual函数的目的是为了让derived classes继承函数的接口及一份强制性实现。
//关于impure virtual的缺省实现 class Airport { ... }; class Airplane { public: virtual void fly(const Airport& destination); }; void Airplane::fly(const Airport& destination) { 缺省代码,将飞机飞往指定的目的地 } class ModelA: public Airplane { ... }; ///继承Airplane::fly缺省行为 class ModelB: public Airplane { ... };///继承Airplane::fly缺省行为 class ModelC: public Airplane { //未声明fly函数 }; Airport PDX(...); Airplane *pa = new ModelC; pa->fly(PDX); // calls Airplane::fly!
切断vitural函数接口和其缺省实现的连接
class Airplane { public: virtual void fly(const Airport& destination) = 0; //声明为纯虚函数 protected: void defaultFly(const Airport& destination); }; void Airplane::defaultFly(const Airport& destination) { 缺省行为,将飞机飞至指定的目的地 } class ModelA: public Airplane { public: virtual void fly(const Airport& destination){ defaultFly(destination); } }; class ModelB: public Airplane { public: virtual void fly(const Airport& destination){ defaultFly(destination); } }; class ModelC: public Airplane { public: virtual void fly(const Airport& destination); //Airplane中的纯虚函数迫使必须提供自己的fly版本 }; void ModelC::fly(const Airport& destination) { 将C型飞机飞至指定的目的地 }
有人担心fly和defaultFly过度雷同的名称而引起的class命名空间污染问题。。
class Airplane { public: virtual void fly(const Airport& destination) = 0;//声明为纯虚函数,让derived classes必须重写 }; void Airplane::fly(const Airport& destination) { //提供缺省行为,只能被derived classes显示调用 缺省行为 //而原先defaultFly是protected,现在为public } class ModelA: public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } }; class ModelB: public Airplane { public: virtual void fly(const Airport& destination){ Airplane::fly(destination); } }; class ModelC: public Airplane { public: virtual void fly(const Airport& destination); }; void ModelC::fly(const Airport& destination) { 自定义行为 }
non-virtual函数: 不变性invariant凌驾其特异性specialization
条款35: 考虑virtual函数以外的其他选择
- 借由Non-Virtual Interface手法实现Template Method设计模式
//NVI手法:增加一个外层函数,里面调用虚函数,可增加事前事后处理工作。其实还是使用virtual class GameCharacter { public: int healthValue() const { //增加一层外覆器wrapper ... //做一些事前工作,设定场景 int retVal = doHealthValue(); // do the real work ... //事后清理 return retVal; } private: virtual int doHealthValue() const { ... } };
NVI手法其实没有必要让virtual一定是private,可以为protected(如derived class调用base class里同名函数),可以为public(如virtual析构函数)
- 借由Function Pointers 实现Strategy设计模式:
优点:每个对象可以有自己的健康计算函数和可在运行期改变计算函数; 缺点:可能降低封装性 (不能访问non-public成员),需要声明friend或提供public访问接口
class GameCharacter; // forward declaration int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef int (*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) :healthFunc(hcf){} int healthValue() const { return healthFunc(*this); } private: HealthCalcFunc healthFunc; }; class EvilBadGuy: public GameCharacter { //同一人物的不同实体可以有不同计算函数,比起virtual函数 public: explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) :GameCharacter(hcf) { ... } }; int loseHealthQuickly(const GameCharacter&); // health calculation int loseHealthSlowly(const GameCharacter&); // funcs with different EvilBadGuy ebg1(loseHealthQuickly); EvilBadGuy ebg2(loseHealthSlowly);
- 借由tr1::function完成Strategy设计模式
class GameCharacter; // as before int defaultHealthCalc(const GameCharacter& gc); // as before class GameCharacter { public: //HealthCalcFunc为任何可调用物,如函数指针、函数对象;接受可隐式转为const GameCharacter&的形参,返回兼容int的类型 typedef std::tr1::function HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf){} int healthValue() const { return healthFunc(*this);} private: HealthCalcFunc healthFunc; }; short calcHealth(const GameCharacter&); struct HealthCalculator { int operator()(const GameCharacter&) const { ... } }; class GameLevel { public: float health(const GameCharacter&) const; }; class EvilBadGuy: public GameCharacter { ... }; class EyeCandyCharacter: public GameCharacter { ... }; EvilBadGuy ebg1(calcHealth); EyeCandyCharacter ecc1(HealthCalculator()); //使用函数对象 GameLevel currentLevel; EvilBadGuy ebg2( //使用某个类的成员函数 std::tr1::bind(&GameLevel::health, currentLevel, _1) ); //GameLevel::health实际上有两个形参,一个为隐含this指针,-1为占位符,为bind返回的函数对象调用时候的第几个形参
- 古典的Strategy设计模式 : 将计算函数做成一个分离的继承体系中的virtual成员函数
class GameCharacter; class HealthCalcFunc { public: virtual int calc(const GameCharacter& gc) const { ... } }; HealthCalcFunc defaultHealthCalc; class GameCharacter { public: explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc) : pHealthCalc(phcf) {} int healthValue() const { return pHealthCalc->calc(*this);} private: HealthCalcFunc *pHealthCalc; }; //可为HealthCalcFunc继承体系添加derived class来增加健康计算方法
条款36: 绝不重新定义继承而来的non-virtual函数 : public 继承is-a关系,non-virtual不变性凌驾特异性条款34
条款37: 绝不重新定义继承而来的缺省参数值
//virtual是动态绑定,默认参数是静态绑定,所以看静态类型 class Shape { public: enum ShapeColor { Red, Green, Blue }; virtual void draw(ShapeColor color = Red) const = 0; }; class Rectangle: public Shape { public: virtual void draw(ShapeColor color = Green) const; }; class Circle: public Shape { public: virtual void draw(ShapeColor color) const; //动态绑定下才会从base继承缺省参数值 }; Shape *pr = new Rectangle; // static type = Shape* pr->draw(); // calls Rectangle::draw(Shape::Red)!!!!
解决方案: NVI手法,条款35
class Shape { public: enum ShapeColor { Red, Green, Blue }; void draw(ShapeColor color = Red) const { doDraw(color); } private: virtual void doDraw(ShapeColor color) const = 0; // the actual work is }; // done in this func class Rectangle: public Shape { public: private: virtual void doDraw(ShapeColor color) const; //无须指定缺省参数值 };
条款38: 通过复合塑模出has-a或“根据某物实现出”。Model “has-a” or ”is-implemented-in-terms-of”through composition.
当复合(composition)发生于应用域内的对象之间,表现出has-a关系;当发生于实现域内则是表现出is-impemented-in-terms-of关系。
复合即为包含。应用域:人、汽车等实物。 实现域:缓冲区、互斥器、查找树等等。
条款39: 明智而审慎地使用private继承。
- private继承意味is-implemented-in-terms-of.它通常比复合的级别低. 但是当derived class需要访问protected base class成员,或需要重新定义继承而来的virtual函数时,才合理。
- 和复合不同,private继承可以造成empty base最优化
class Person { ... }; class Student: private Person { ... }; // inheritance is now private void eat(const Person& p); // anyone can eat void study(const Student& s); // only students study Person p; // p is a Person Student s; // s is a Student eat(p); // fine, p is a Person eat(s); // error! a Student isn't a Person //private继承,编译器不会自动将一个derived class 对象转换为一个base class对象
private继承意味is-implemented-in-terms-of, 与条款38的复合在应用域的表现一样。
尽可能使用复合, 必要时才使用private继承。合适才是必要?当protected成员或virtual函数牵扯进来时。还有一种激进情况,当空间方面的厉害关系足以踢翻private继承的支柱时。
设计一个Widget类,通过复用Timer类来周期性审查每个成员函数的被调用次数。并重写Timer的虚函数。
class Timer { public: explicit Timer(int tickFrequency); virtual void onTick() const; // automatically called for each tick ... }; //为了让Widget重新定义Timer的virtual,必须继承Timer //但public继承在此并不适用,is-a关系,而且Widget调用OnTick容易造成接口误用,违反条款18 //所以private继承 class Widget: private Timer { private: virtual void onTick() const; // look at Widget usage data, etc. ... }; //但private继承绝非必要,可以使用public继承加复合 class Widget { private: class WidgetTimer: public Timer { public: virtual void onTick() const; ... }; WidgetTimer timer; ... };
为什么愿意选择public继承加复合而非private继承?
1)如果你想从Widget派生出其他类,而同时想阻止其derived class重新定义OnTick,但无论什么继承派生类都可以重写虚函数。
但WidgetTimer是private成员,Widget的derived class将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数
2)你可能会想要将Widget的编译依存性降至最低。如果Widget继承Timer,当Widget编译时,Timer定义必须可见。
如果WidgetTimer移到Widget之外而Widget内含指针指向一个WidgetTimer,则不需要include。
激进情况: 如果你的class不到任何数据,没有non-static成员变量,没有virtual函数(会有vptr),没有virtual base class。而C++裁定凡是独立对象都必须有非零大小。
class Empty {}; class HoldsAnInt { private: int x; Empty e; }; // sizeof(HoldsAnInt) > sizeof(int); //C++官方勒令安插一个char到空对象。然而齐位需求alignment,可能被扩充到int class HoldsAnInt: private Empty { private: int x; }; //sizeof(HoldsAnInt) == sizeof(int) //EBO(empty base optimization)空白基类最优化,EBO一般只在单一继承下才可行
条款40: 明智而审慎地使用多重继承。
- 多重继承比单一继承复杂。可能导致新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化(赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。public继承某个Interface class,private继承某个协助实现的class
//多重继承,virtual base class class File { ... }; class InputFile: virtual public File { ... }; class OutputFile: virtual public File { ... }; class IOFile: public InputFile, public OutputFile { ... };
virtual继承的classes所产生对象往往体积更大,访问virtual base classes 的成员变量时速度更慢。
而且virtual bsae class的初始化责任是由继承体系中的最底层class负责的。
因此:1)非必要不要使用virtual继承 2)如必须使用virtual base classes,尽可能避免在其中放置数据。
现在要以IPerson的指针和引用来编写程序,并用factory function创建对象。
现在要写个CPerson类,继承并重写IPerson的pure virtual函数的实现。
我们可以自己写这个实现,但我们已经找到一个PersonInfo可以提供CPerson所需要的实现内容。
is-implemented-in-terms有两种方式,复用和private继承,但本例CPerson要重新定义valueDelimOpen和valueDelimClose,所以这里用private继承,当然也可以复合+继承。
//public继承某接口,private继承自某实现 class IPerson { public: virtual ~IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; }; class DatabaseID { ... }; class PersonInfo { public: explicit PersonInfo(DatabaseID pid); virtual ~PersonInfo(); virtual const char * theName() const; virtual const char * theBirthDate() const; virtual const char * valueDelimOpen() const; virtual const char * valueDelimClose() const; }; class CPerson: public IPerson, private PersonInfo { // note use of MI public: explicit CPerson( DatabaseID pid): PersonInfo(pid) {} virtual std::string name() const // implementations { return PersonInfo::theName(); } // of the required IPerson member virtual std::string birthDate() const // functions { return PersonInfo::theBirthDate(); } private: // redefinitions of const char * valueDelimOpen() const { return ""; } // inherited virtual const char * valueDelimClose() const { return ""; } // delimiter }; // functions