effective C++(四)

声明一个纯虚函数的目的是为了让派生类只继承函数接口,但是基类声明了纯虚函数也可以给它提供一份实现代码,c++并不会发生怨言,但调用他的唯一途径是调用时明确指出其类的名称。

声明简朴的非纯虚函数的目的,是让派生类继承该函数的接口和缺省实现,为了避免错误的使用缺省实现,可以这样实现

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 ModelC:public Airplane{
 public:
 virtual void fly(const Airport& destination);
 ..
};
void ModelC::fly(const Airplane& destination)
{
 自己的行为
}

如果成员函数是个非虚函数,意味着它并不打算在派生类中有不同行为

替换虚函数的三种方法:一令客户通过public non-virtual成员函数间接调用私有虚函数,被称为NVI手法,它的优点在于可以在执行主要代码前后做一些事前工作和事后工作,事实上此手法下的虚函数不一定为私有,要视情况而定

class GameCharacter{
 public:
 int healthValue() const
 {
   ...                   //做一些事前准备工作
   int reVal=doHealthValue();
   ...                   //做一些事后准备工作
   return retVal;
 }
 ...
 private:
  virtual int doHealthValue() const
  {
    ...
   }
};

另外一种手法是称为strategy分解表现形式,是通过运用函数指针替换虚函数,优点是每个对象可各自拥有自己的计算函数,和可在运行期改变计算函数的优点,不过缺点为可能必须降低类的封装性。

class GameCharacter;
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;
};

最后一种方法是使用函数对象,下面的签名代表的函数是接受一个reference指向const GameCharacter,并返回int。这个trl::function类型,产生的对象可以持有保存任何与此签名式兼容的可调用物,所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为Int.

...如上
typedef std::trl::function<int (const GameCharacter&)>HealthCalcFunc;
...同上

这使得一下函数都可被调用

short calcHealth(const GameCharacter&);
struct HealthCalculator{
 int operator()(const GameCharacter&)const
 { ... }
};
class GameLevel{
 public:
 float health(const GameCharacter&) const;
 ...
};
class EvilBadGuy::public GameCharacter{
 ...
};
EvilBadGuy ebg1(calcHealth);
EvilBadGuy ebg2(HealthCalculator());
GameLevel currentLevel;
EvilBadGuy ebg3(std::trl::bind(&GameLevel::health,currentLevel,_1)
);

GameLevel::health宣称自己接受一个参数,但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是那个this所指,如果我们使用GameLevel::health作为计算函数,我们必须以某种方式转换它,使它不再接受两个参数,用bind,它指出ebg3的计算函数应该总是以currentLevel作为GameLevel对象。

任何情况下都不该重新定义一个继承而来的非虚函数,否则会发生如下问题:

class B{
 public:
 void mf()
};
class D:public B
{
 public:
 void mf();       //遮掩了B::mf;
};
D x;
B* PB=&x;
D* PD=&x;
PB->mf();         //调用B::mf
PD->mf();         //调用D::mf

原因在于,非虚函数时静态绑定的,由于PB被声明为一个pointer-to b,通过PB调用的非虚函数永远是B所定义的版本,即使PB所指为B派生的对象。如如果mf为虚函数,则不论PD还是PB所指都为D::mf因为PB和PD真正指的都是一个D类型的对象。

对象所谓的静态类型,就是它在程序中被声明时所采用的类型。动态类型则是指目前所指对象的类型,也就是说,动态类型可以表现出一个对象将会有声明行为。虚函数时动态绑定而来的,意思是调用一个虚函数,究竟调用哪一份代码,取决于动态类型

class Shape{
 public:
 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;//这样写当调用此函数要指定参数值,因为静态绑定下这个函数并不从其基类继承缺省参数值,
 ...                                         当若以指针或引用调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其基类继承缺省参数值
};
Shape* pc=nnew Circle;            //静态类型都为Shape
Shape* pr=new Rectangle;
pc->draw(Shape::Red);       //调用Circle::draw(Shape::Red)
pr->drwa();                //本来Rectangle::draw的缺省参数为GREEN,但由于pr的静态类型是Shape*,所以调用的缺省参数为Shape的

原因在于draw是个虚函数,而它有个缺省参数值在派生类中被重新定义了,我们也绝对不要重新定义一个继承而来的缺省参数,因为缺省参数值都是静态绑定,而虚函数却是动态绑定的,为了避免重复代码,可使用NVI手法来处理

class Shape{
 public:
 void draw(ShapeColor color=Red) const
{
  doDraw(color);
}
 ...
private:
 virtual void doDraw(ShapeColor color)const=0;
};
class Rectangle:public Shape{
 private:
 virtual void doDraw(ShapeColor color)const;        //不需指定缺省参数了,巧妙的避开了
 ..
};

程序中对象其实相当于你所塑造的世界中的某些事物,例如人,汽车等等,这样的对象属于应用域,其他对象则纯粹是实现细节上的人工制品,像缓冲区,互斥器,等等这些对象属于你的软件的实现域,当复合(类中含有类)发生于应用域内对象之间,变现出has-a的关系,当它发生于实现域则是表现出is-implemented-in-terms-of的关系(根据某物实现之),当两个类之间并非is-a的关系,但它们又需要彼此的相互功能时,可以使用复合。

类之间的继承关系是private,编译器不会自动将一个派生类对象转换为一个基类对象,私有继承纯粹只是一种实现技术,如果D以私有继承形式继承B,意思是D对象根据B对象实现而得,再没有其他意义了。

尽可能使用复合,必要时才使用私有继承,大概有两种情况,第一种是当类之间不存在is-a关系,但是其中一个需要访问另一个的保护成员,或需要重新定义其一或多个虚函数,第二种是你所处理的类不带任何数据,你需要对它造成最优化的情况。

看一个复合代替私有继承的情况

class Widget:private Timer{
 private:
 virtual void onTick() const;        //查看widget的数据
..
};
class Widget{
 private:
 class WigdetTimer:public Timer{
 public:
 virtual void onTick() const;
 ...
 };
 WidgetTimer timer;
 ..
};

优点,WidgetTimer是Widget内部的一个私有成员并且继承Timer,Widget的派生类将无法取用WidgetTimer,因此无法继承它或重新定义它的虚函数,这是C++的阻止派生类重新定义虚函数的能力表现方法!

对于第二种情况

class Empty{}    //没有数据,其对象应该不适用任何内存
class HoldsAnInt{
 private:
 int x;
 Empty e;
};

我们会发现HoldAnInt的大小大于一个整型,在多数编译器中sizeof(Empty)=1,因为面对大小为零之独立对象,通常会给它一个char到空对象内,但是如果我们用私有继承,就可以确保它。

class HoldsAnInt:private Empty{
 private:
 int x;
};

几乎可以确定HoldAnInt的大小等于一个整型,这是所谓的EBO,空白基类最优化。

使用多重继承的时候一定要避免造成命名的歧义性

class BorrowableItem{
 public:
 void checkOut();
 ..
};
class ElectronicGadget{
 private:
 bool checkOut() const;
 ...
};
class Mp3Player:public BorrowableItem,public ElectronicGadget
{...};
Mp3Player mp;
mp.checkOut();//造成歧义,到底该调用哪个checkOut函数     可以如此:mp.BorrowableItem::checkOut()

当这种情况需要使用虚继承

class File{...};
class InputFile:virtual public File{...};
class OutputFile:virtual public File{...};
class IOFile:public InputFile,public OutputFile{...};

从正确行为来看,Public继承总是应该是virutal,但你得为virtual继承付出代价,支配虚基类初始化的规则比起非虚基类的情况远为复杂且不直观,虚基类的初始化责任是由继承体系中的最底层类负责的,这暗示类若派生自虚基类而需要初始化,必须认知其虚基类,不论那些基类距离多远,当一个新的派生类加入体系中,它必须承担其虚基类不论直接或间接,的初始化责任。因此,非必要不要使用虚继承,其次必须使用时,尽可能避免在其中放置数据。

多种继承的确有正当用途,其中一个情节涉及public继承某个interface class和private继承某个协助实现的类的两相组合

显式接口,在源码中明确可见的,通常由函数的签名式也就是函数名称,参数类型和返回类型构成。

隐式接口由有效表达式组成

template<typename T>
void doProcessing(T& w)
{
 if(w.size()>10&& w!=someNastyWidget){
 T temp(w);
 temp.normalize();
 temp.swap(w);
 }
}

从上可以看出w的类型T好像必须支持size,normalize,swap,copy构造函数,不等比较,这些便是T必须支持的一组隐式接口,凡是涉及w的任何函数调用,有可能造成模板具现化,使这些调用得以成功,这样的具现行为发生在编译器,以不同的template参数具现化function templates会导致调用不同的函数,这便是所谓的编译器多态。

隐式接口仅仅是有一组有效表达式构成,表达式自身可能看起来很复杂,但它们要求的约束条件一般而言相当直接又明确,加诸于template参数身上的隐式接口就像加诸于class对象身上的显式接口一样真实,而且两者都在编译器完成检查,你无法再template中使用不支持template所要求之隐式接口的对象

template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在类内呈嵌套状,我们称它为嵌套从属名称,像基本内置类型并不依赖于任何template参数的名称,这样的名称是非从属名称。

C++有个规则,如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是一个类型,除非你告诉它是,所以在缺省情况下,嵌套从属名称不是类型。typename只被用来验明嵌套从属类型名称,其他的不能添加

template<typename C>
void print2nd(const C& container)
{
 C::const_iterator* x;        //编译器会以为是相乘!
 ...
}
void print2nd(const C& container)
{
 if(container.size()>=2){
   C::const_iterator iter(container,begin());       //无法通过编译,iter不是一个类型
 ...
void print2nd(const C& container)
{
 if(container.size()>2){
  typename C::const_iterator iter(container.begin());      //正确写法
 ..
}
}

typename作为嵌套从属类型名称的前缀词的例外是不可以出现在基类列表内的嵌套从属类型名称之前,也不可以在成员初值列中作为基类的修饰符。如:

template<typename T>
class Derived:public Base<T>::Nested{      //base class list中不允许出现typename
 public:
 explicit Derived(int x)
 :Base<T>::Nested(x)                    //初值列中不允许出现
  {
    typename Base<T>::Nested temp;
     ...
   }
   ...
};

声明template参数时,前缀关键字class 和typename是一样效果的,typedef 和typename可以一起使用

处理模板化基类内的名称

template<typename Company>
class MsgSender{
 public:
 ...
 void sendClear(const MsgInfo& info)
 {
   ...
 }
};
template<typename Company>
class LoggingMsgSender:public MsgSender<company>{
 public:
 ...
 void sendClearMsg(const MsgInfo& info)
 {
  sendClear(info);               //调用基类函数,无法通过编译
 }
 ...
};

当编译器遭遇class template loggingMsgSender定义式时,并不知道它继承什么样的class,当然它继承的是MsgSender<company>,但其中的company是个template参数,不到后来当loggingMsgSender被具现化,无法确切知道它是什么。而不知道company是什么,就无法知道 它是否有个sendclear函数。

更确切的是,假设有个函数不需要这个函数,则需要对其有一个特化版的MsgSender template,这是所谓的模板全特化

template<>
class MsgSender<CompanyZ>
{
 public:
 ...
 void sendSecret(const MsgInfo& info)
 {...}
};

这就是为什么C++拒绝这个调用的原因,它知道基类模板有可能被特化,而那个特化版本可能不提供和一般性template相同的接口,因此它往往拒绝在模板化基类内寻找继承而来的名称。解决方法有三种

template<typenname Company>
class LoggingMsgSender:public MsgSender<Company>
{
 public:
 1.using MsgSender<Company>::sendClear;
 ...  
 void sendClearMsg(const MsgInfo& info)
 {
   2.this->sendClear(info);
   3.MsgSender<Company>::sendClear(info);  //最不满意的一个揭发,如果调用的是虚函数,会关闭virtual绑定行为
 }
...
};
将于参数无关的代码抽离template,template生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系,因非类型模板参数而造成的膨胀,往往可以消除,做法是以函数参数或class成员变量替换参数

猜你喜欢

转载自blog.csdn.net/weixin_38893389/article/details/79529742