Effective C++ 6 继承与面向对象设计 学习记录
条款32:确定public继承is-a的意义符合你的设计意图
Class Person;
Class Student : public Person{};
Void study(const Student &s);
Void eat(const Person &p);
Person p;
Student s;
eat(p);
eat(s);
Study(p);
Study(s);
Public继承代表的是is-a的关系。对于eat()函数,编译器可以将Student对象转型为Person对象,而对于study()函数而言,Person对象不一定是Student对象,因此反过来就行不通。如果想要程序编译通过,可能要进行强制类型转换。
合理的设计思维:
Public继承意味着父类对象上可以实施的任何操作,子类对象必须也可以实施。因为每个子类对象都包括一个父类对象。
条款33:避免继承而来的名称
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();
d.mf1(x);
d.mf2();
d.mf3();
d.mf3(x);
对于mf3()函数是子类non-virtual函数覆盖了父类non-virtual函数,条款36有详细的解释。这里我们主要讨论子类定义覆盖父类定义的现象。由于覆盖现象导致父类一些定义不再可见,从而导致错误。这与public继承is-a的意义相左。子类public继承与父类,却无法使用父类public成员函数。
合理的设计思维:
public继承会覆盖父类内的名称,有时这会违反public的意义。可以采用using显示申明需要可见的父类成员函数。
条款34:区分接口继承和实现继承
Class Shape
{
Public:
Virtual void draw()cosnt=0;
Virtual void error(const string &msg);
Int objectID() const
};
Class Rectangle: public Shape{};
Class Ellicpse: public Shape{};
首先需要明确的是接口继承与实现继承是不同的概念,pure virtual函数是可以有实现的。上述类定义中,对于pure virtual函数,当子类public继承的时候,要求一定要有自己的实现。因此,声明一个pure virtual函数的目的是为了继承接口;对于声明impure virtual函数的目的是为了继承接口和缺省实现;对于声明non-virtual函数的目的是为了继承接口和强制实现,不容更改定义。
合理的设计思维:
Public继承下,子类总是继承父类的接口。因此,是否需要继承实现、重新实现还是不容改变就决定了函数声明的方式,是pure virtual、impure virtual还是non-virtual。
条款35:考虑virtual函数以外的其他选择
1. 如条款37所述的NVI设计方法
案列见条款37,我们需要明白一点,NVI设计方法并不一定需要virtual函数为private。如果需要在外覆器wrapper中调用基类的对应兄弟,则可以将其声明为protected。Virtual析构函数的声明一定要是public,详细见条款7。NVI是template method设计模式的一个独特表现形式。
2. 用non-member函数替代
Class GameCharacter;
Int defaultHealthCalc(const GameCharacter &gc);
Class GameCaracter
{
Public:
Typedef int (*HealthCalcFunc)(const GameCaracter &);
Explicit GameCaracter(HealthCalcFunc hcf=defaultHealthCalc): healthFunc(hcf){}
Int healthValue()const
{
Return healthFunc(*this);
}
Private:
HealthCalcFunc healthFunc;
};
Class EvilBadGuy: public GameCharater
{
Public:
Explicit EvilBadGuy(HealthCalcFunc hcf=defualtHealthCalc): GameCharacter(hcf){}
};
Int loseHealthQuickly(const GameCharacter &);
Int loseHealthSlowly(const GameCharacter &);
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
此时,类中定义了一个指向函数的指针变量。对于同一类实体,现在可以调用不同健康计算函数,因而更加灵活。同时,如果在类中再定义一个可以设置指针变量的函数SetHealthCalcFunc,那么对象的健康计算方式可以在运行期进行改变。但这种设计方式也有缺点,比如健康计算函数需要使用到类中private或protected成员变量和成员函数,而此时并没有提供这些接口,那么就存在实现上的不便。此时,可以弱化类的封装,将这个健康计算函数声明为其friend函数,或者为能够使用类中成员变量或函数提供public接口。此种设计方法是strategy设计模式的简单应用。
3. 借助tr1::function完成strategy模式
可以借助templates,来实现上述借由函数指针实现的strategy模式。
Class GameCharacter;
Int defaultHealthCalc(const GameCharacter &gc);
Class GameCaracter
{
Public:
Typedef std::tr1::function<int (const GameCharacter &)> HealthCalcFunc;
Explicit GameCaracter(HealthCalcFunc hcf=defaultHealthCalc): healthFunc(hcf){}
Int healthValue()const
{
Return healthFunc(*this);
}
Private:
HealthCalcFunc healthFunc;
};
4. 经典strategy模式
Class GameCaracter;
Class HealthCalcFunc
{
Public:
Virtual int calc(const GameCharacter &gc) const
{
}
};
HealthCalcFunc defaultHealthCalc;
Class GameCaracter
{
Public:
Explicit GameCaracter(HealthCalcFunc *hcf=&defaultHealthCalc): healthFunc(hcf){}
Int healthValue()const
{
Return healthFunc->calc(*this);
}
Private:
HealthCalcFunc *healthFunc;
};
此时,可以通过为HealthGalcFunc类添加子类,并在覆盖calc()函数,就可以实现不同的健康计算方式。
条款36:绝不重新定义继承而来的non-virtual函数
Class B
{
Public:
Void mf();
};
Class D: public B
{
Public:
Void mf();
};
D继承B,且D覆盖了B的non-virtual成员函数。
D x;
B *pB=&x;
D *pD=&x;
pB->mf();
pD->mf();
对于non-virtual成员函数(此时静态类型和动态类型之分),编译器实施静态绑定。因此,对象D表现怎么得行为取决于指向该对象的指针类型,而非对象本身。
合理的设计思维:
1.非public继承(public继承是is-a的关系);
2.定义virtual成员函数。
因此,此条款对于条款7也是一个合理的解释。
条款37:绝不重新定义继承(virtual函数)而来的缺省参数值
Class Shape
{
Public:
Enum ShapColor{Red Green Blue};
Virtual void draw(ShapeColor color=Red) const=0;
};
Class Rectangle:Public Shape
{
Public:
Virtual void draw(ShapeColor color=Green) const=0;
};
Class Circle:Public Shape
{
Public:
Virtual void draw(ShapeColor color) const=0;
};
与virtual 函数不同,virtual 函数缺省参数值采用静态绑定,因此子类对象调用动态绑定的virtual函数时,缺省参数值却由父类指定。
Shape *ps=new Rectangle;
Ps->draw();
类Rectangle中虽然修改了缺省的参数值,但draw()的缺省参数值还是Red,并没有符合设计者的预期效果。
Circle c;
c.draw(Shape::Red);
Shape *ps=new Circle ;
Ps->draw();
类Circle中virtual函数声明与Shape不一致。当对象调用draw()时,静态绑定下的该函数不会从父类继承缺省参数值,因此需要显示提供。而以指针调用draw()时,动态绑定下的该函数会从父类继承缺省参数值。
合理的设计思维:NVI(no virtual interface)
Class Shape
{
Public:
Enum ShapColor{Red Green Blue};
Virtual void draw(ShapeColor color=Red) const
{
doDraw(color);
}
Private:
Virtual void doDraw(Shape color) const;
};
Class Rectangle:public Shape
{
Private:
Virtual void doDraw(Shape color) const;
};
此时,draw()是non-virtual成员函数。由条款36可知,其不应该被覆盖,缺省参数设置与成员函数virtual性再无关系。也可以看到父类、子类中doDraw()函数声明一致。
条款38:由聚合实现has-a 或 is-implemented-in-terms-of的类间关系
1. has-a:
Class Address;
Class PhoneNumber;
Class Person
{
Private:
Std::string name;
Address address;
PhoneNumber phonenumber;
};
2. is-implemented-in-terms-of
Template<class T>
Class Set
{
Public:
Bool member(const T& item) const; //行为
Void insert(const T& item);
Void remove(const T& item);
Std::size_t size() const;
Private:
Std::list<T> rep;
};
has-a 强调“拥有……”的概念;is-implemented-in-terms-of强调“由……实现”的概念,而又不是is-a的概念,必须重新定义一些行为。
合理的设计思维:
聚合(composition)的意义与public继承(is-a)完全不同。在设计类时,要明确类间关系,从而确定设计方法。
条款39:理解private继承
Private继承承载着聚合中is-implemented-in-terms-of的概念。
Class Person;
Class Student : private Person{};
Void eat(const Person &s);
Person p;
Student s;
eat(p);
eat(s);
对于public继承,我们知道它是is-a的关系。此例表明private继承不是is-a的关系,因为子类Student对象s作为参数调用eat()时,编译器并没有将s转换成Person对象,而public继承却会这么做。所以private继承只代表实现技术手段而非反应对象间的关系。
Private继承存在的必要性有三个因素:
1. protected和public成员
private继承会使父类所有数据成员的访问权限变为private,不可见性避免了使用者一些不恰当的访问操作。
2.继承并重新定义virtual函数,但又不是is-a的关系
此时,如果采用public继承,明显不合时宜。通过private继承将virtual函数变为private,避免使用者调用,因为向使用者提供这样的接口并不合适。如有需要可重新定义virtual函数,但必须声明为private,理由很显然。
3.EBO(空白基类最优化)
Class Empty{};
Class HoldsAnInt
{
Private:
Int x;
Empty e;
};
空类以为着这个类中没有virtual函数,没有non-static成员变量,也就意味着sizeof(Empty)=0;然而,实际情况是编译器至少会给这样的空类对象分配一个char型大小的空间。因此,对于4四节对齐的系统,sizeof(HoldsAnInt)=8。
Class HoldsAnInt : private Empty
{
Private:
Int x;
};
此时,HoldsAnInt private继承Empty,可以确定sizeof(HoldsAnInt )=sizeof(Int)。看以看到,编译器此时不会再为对象HoldsAnInt 分配一个char型大小的空间。和上面的相比,对象占用空间自然少。然而,空类本身就很特殊,所以不值得提倡。
合理的设计思维:
尽量使用聚合来表达is-implemented-in-terms-of这种概念。
条款40:多重继承
1.多重继承容易造成歧义
Class BorrowalbleItem
{
Public:
Void chechout();
};
Class ElectronicGadget
{
Private:
Bool checkout() const;
};
Class MP3Player: public BorrowableItem , public ElectronicGadget
{
};
MP3Player mp;
Mp.checkout();
Mp.BorrowableItem ::checkout();
此时,编译器根据最佳匹配原则寻找最佳函数版本,然而两个函数匹配程度一致,导致歧义。编译器匹配函数主要原则有函数名、参数、返回值和作用域,而此时两个函数只是访问权限的区别,因而需要指明具体的调用函数。
2.多重继承容易造成代码重复
Class File{};
Class InputFile : public File{};
Class OutputFile :public File{};
Class InputFile : virtual public File{};
Class OutputFile :virtual public File{};
Class IOFile :public InputFile, public OutputFile{};
此时,IOFile 多重继承InputFile和OutputFile,而两者都是继承自File。因此,File的构造函数会被调用两次,IOFile 对象会有两份File成员变量数据。这种情况下,C++引入了虚继承机制。虚继承机制下,多重继承时,子类对象中父类成员变量数据只会有一份。但为了达到这个目的,编译器会做很多工作,对象所占的内容空间也较非虚继承类对象大,变量访问速度也较慢。可以猜想,编译器的大部分工作在于判断和识别继承树中类的virtual性,以及virtual类成员初始化等工作上。
3. 多重继承可以实现public继承自某接口,private继承自某实现
Class IPerson
{
Virtual ~IPerson();
Virtual string name() const =0;
Virtual string birthDate() const=0;
};
Class DatabaseID{};
Class PersonInfo
{
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
{
Public:
explicit CPerson(DatabaseID pid):PersonInfo(pid){}
Virtual string name() const //继承某接口
{
Return PersonInfo::theName();//继承某实现
}
Virtual string birthDate() const
{
Return PersonInfo::theBirthDate();
}
Private:
Const char* valueDelimOpen() const
{
Return “”;
}
Const char* valueDelimClose() const
{
Return “”;
}
};
Public继承是is-a的关系,private继承是聚合的概念。合理的运用不同的继承方式,可以使得代码简洁、合理、易于维护。
合理的设计思维:
如无必要,尽量使用单继承方法,因为在使用多重继承时,必须考虑歧义和共同继承问题,同时还要考虑多重继承的效率问题。尽量避免虚基类带有任何数据成员。当然,多重继承也有其一定的优越性。上述3反应了在进行类设计时,合理安排多重继承的方式,可以使得类结构简洁且合理。