Atela

导航

effective C++ 3rd 笔记(三)

条款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

posted on 2011-04-14 20:43  Atela  阅读(520)  评论(0编辑  收藏  举报