一、确定public继承塑造出is-a关系
public继承意味着”is - a”,适用于base classes身上每一件事都适用于derived classes。
若derived class与base class有差别,但语义上derived class确实归于base class,则考虑改善设计,举例:
1
2
3
4
5
6class Brid{
virtual void fly() = 0;
}
class Penguin : public Brid{
....
}企鹅确实属于鸟类,但企鹅并不会飞,这时有两种选择:
一是选择不给
Brid
写fly()
函数,将Brid
派生为飞鸟和不会飞的鸟,再让企鹅去继承不会飞的鸟类,这样在编译期写Penguin.fly()
就会报错。二是选择使
Penguin
重写fly()
,使得若是有控制流进入这个函数就报错,注意,这并不是说企鹅不能飞,而是,企鹅能飞,但那么做是一种错误。两种方式都可以解决问题,一般而言更推荐第一种,因为好的接口应防止无效代码通过编译!
public继承下,绝不重新定义继承来的non-virtual函数,因为是is - a关系
二、避免遮掩继承而来的名称
根据作用域准则,C / C++ 程序在寻找一个变量 / 函数时,总是先在local作用域中寻找,找不到再去包含local的,更大作用域中寻找,依次类推,若是直到global作用域还找不到匹配的变量 / 函数,就会报错。
基于1,当我们在子类中重写出一个和父类同名的接口时,父类的对应接口会被覆盖掉,即使是父类的重载接口也一样会被覆盖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Base{
public:
virtual void mf1();
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;
d.mf1(); // ok,调用的是Derived的mf1
d.mf1(10); // error,Derived::mf1遮掩了Base::mf1
d.mf2(); //ok,调用的是Base类的mf2
d.mf3(); //ok,调用Derived中的mf3
d.mf3(10.0) //error,Derived::mf3遮掩了Base::mf3当子类中重写了父类的接口,却又想调用父类接口时,有两种方法:
使用using使父类接口暴露在子类中,但需注意的是,这会使所有父类同名重载接口暴露:
1
2
3
4
5
6
7
8
9
10class Derived : public Base{
public:
using Base::mf1;
using Base::mf3;
virtual void mf1(); //覆写
void mf3();
void mf4();
};
d.mf3(); //ok,使用Derived接口(虽然Base的mf3()也暴露了,但这里遵循作用域准则,先小后大
d.mf3(1); //ok,使用Base的mf3(double)接口当你只想使得某个特定的接口或者部分接口暴露时,使用一个转发函数:
1
2
3
4
5
6class Derived : private Base{
public:
void mf3(){
Base::mf3();
}
};
三、区分接口继承和实现继承
接口继承和实现继承不同,public继承下,derived类总是继承base类的接口
pure virtual函数只具体指定接口继承(派生类必须继承这个接口)
impure virtual函数指定接口继承,并提供一份缺省实现继承
non-virtual函数同时指定接口和强制的实现继承,一般认为它不应该被重新定义
tips : pure-virtual函数可以被实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Shape{
public:
void func() = 0;
};
class Rectangle : public Shape{
...
};
void Shape::func(){
...
}
Shape* ps = new Shape;
Shape* ps1 = new Rectangle;
ps -> func(); //error
ps1 -> Shape::func(); //ok
四、考虑virtual函数外的其他选择
在RPG游戏中,我们需要计算一个角色扣血情况,由于有不同的角色与NPC,所以我们可以很自然地想到将扣血函数在一个基类中写为虚函数:
1 | class GameCharater{ |
而除了虚函数以外,有一些手段能达到与虚函数相仿的目的:
藉由Non - Virtual Interface实现Template Method 模式
它通过一个public non-virtual成员函数包裹较低访问性的virtual函数,其中virtual函数的访问级应为private或protected,否则没有意义
1
2
3
4
5
6
7
8
9
10class GameCharater{
public:
int healthValue() const{
... // 前置操作
int retVal = doHealthValue();
... // 后置操作
}
private:
virtual doHealthValue() const;
} ;基类采用NVI手法,在子类中重新定义virtual函数即可(调用包含virtual函数的普通成员函数是多态)。
藉由function pointers实现的Strategy模式(策略模式)
将virtual函数替换成函数指针成员变量,这是Strategy模式的一种表现形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class GameCharater;
int defaultHealthCalc(const GameCharater& gc);
class GameCharater{
public:
using HealthCalcFunc = int(*)(const GameCharacter&); //使用using定义函数指针
typedef int(HealthCalcFunc *)(const GameCharacter&); //使用typedef定义函数指针
explicit GameCharater(HealthCalcFunc hcf = defaultHealthCalc):
healthFunc(hcf){}
int healthValue() const{
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};使用这种方式有两个好处:
一是可以使同一个人物对象可以有不同的接口一样的函数
二是可以使调用函数在运行期变更
但它的缺点也比较明显,因为本该被成员解决的问题需要被外部函数解决,这无疑降低了类的封装性
藉由
std::function
实现的Strategy模式(策略模式)在作者写这本书时,function还在tr1中,如今在CXX1x中已成为std中的成员,
std::function
与function pointer效果类似,但它提供了封装和对不同函数等效物的支持,它可以以函数对象,函数,成员函数等作为参数,从而具备了更惊人的弹性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class GameCharater;
int defaultHealthCalc(const GameCharater& gc);
class GameCharater{
public:
using HealthCalcFunc = std::function<int(const GameCharater&)>;
explicit GameCharater(HealthCalcFunc hcf = defaultHealthCalc):
healthFunc(hcf){}
int healthValue() const{
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
//不同返回值的函数
short calcHealth(const GameCharacter&);
//函数对象
struct HEalthCalculator{
int operator() (const GameCharater&) const{
...
}
};
//类中public函数
class GameLevel{
public:
float health(const GameCharater&) const;
};
//上述三者都可以充当一个类函数指针传入function:
GameCharater gc1(calcHealth);
GameCharater gc2(HEalthCalculator());
GameCharater gc2(std::bind(&GameLevel::health, std::placeholders::_1))古典Strategy模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class GameCharater;
class HealthCalcFunc{
public:
virtual int calc(const GameCharater& gc);
};
HealthCalcFunc defaultHealthCalc;
class GameCharater{
public:
explicit GameCharater(HealthCalcFunc* hcf = &defaultHealthCalc):
healthFunc(hcf){}
int healthValue() const{
return healthFunc(*this);
}
private:
HealthCalcFunc* healthFunc;
};
五、绝不重新定义继承来的参数缺省值
所谓静态类型,是指变量在声明时所采用的类型,在编译期就被决定好,而动态类型,是指当前所指的对象的类型,直到运行期才决定好:
1 | class Shape{ |
在派生类中重新定义继承而来的缺省参数值是静态绑定!所以当你这样定义,在运行期往往会出现出人意料的结果:
1 | class Circle : public Shape{ |
而且,在派生类中重写一遍参数缺省值也是不可以的,这意味着你修改基类的缺省值时,所有子类的缺省值也得被改一遍。
六、通过复合塑造出“has - a”
- 复合意味着某个类的成员是另一个类,而public继承则意味着某个类是父类的特例
- 在应用域,复合意味着“has - a”,即有一个其它类型的成员对象,在实现域,复合意味着“根据某物实现出”
- 要注意区分“is - a”和根据某物实现出,比如,你想使用
std::list
来构建一个自己的myset
,如果使用public继承,语义和行为上就是“myset
是std::list
的一个特例,但这是不正确的,事实上,我们想要的是“根据某物实现出”,即将std::list
写为myset
的一个成员。
七、明智而谨慎地使用private继承
为什么私有代表了has-a关系
首先明确一点,public继承之所以代表is - a关系,是因为父类向外界提供的一切接口(public),子类都能代为提供,故而子类表现得像是一个特殊的父类;而使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。因此私有继承提供的特性与复合相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
何时使用私有继承,何时使用复合
由于既可以使用复合,也可以使用私有继承来建立has-a关系。大多数c++程序员倾向于前者。不过私有继承所提供的特性确实比包含多。例如,假设需要访问has - a关系类的护成员,由于复合关系不在继承体系中,所以不能访问,而private继承就可以。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。空白基类最优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//使用复合
class Empty{
//没有成员,只有函数
};
class Util_1{
int x;
Empty e;
};
sizeof(Util_1) == (1 + 4) != sizeof(int);
//使用继承
class Util_2 : private Empty{
int x;
}
sizeof(Util_2) == 4 == sizeof(int)
八、明智而谨慎地使用多重继承
C++支持多重继承,基本继承方式:
1
2
3
4
5
6
7
8
9
10
11class Base1{
public:
void func();
}
class Base2{
public:
void func();
}
class Derived : public Base1, public Base2{
...
}当继承而来的多个父类中有同名可访问成员时,会出现歧义,解决歧义的方法是说明它在哪个作用域中,例:
1
2Derived d;
d.Base1::func();多重继承使用时会出现菱形继承的问题
菱形继承最经典的例子就是C++中
stream
的例子了,这里以文件流来举例:1
2
3
4
5
6
7class File{
public:
int* file_handle;
}
class InputFile : public File;
class OutputFile : public File;
class IOFile : public InputFile , public OutputFile;如代码所示,这个继承体系是一个菱形继承,假设
IOFile
对象想访问File
对象的成员file_handle
,那么它该访问的是从InputFile
而来的还是OutputFile
而来的成员呢,这又引发了歧义。编译器的一种可能的处理方法是,给两条路径来的成员都申请一块内存!也就是说,这种情况下,IOFile
中有两个file_handle
成员,那么一个IOFile
竟然等效于两个File
!这是不对的:1
2
3
4IOFile fd;
std::cout << &fd -> InputFile::file_handle << std::endl;
std::cout << &fd -> OutputFile::file_handle << std::endl;
//手动试一下会发现两者内存位置不同所以,我们需要虚继承来解决这个问题:
1
2
3
4
5
6
7class File{
public:
int* file_handle;
}
class InputFile : virtual public File;
class OutputFile : virtual public File;
class IOFile : public InputFile , public OutputFile;而进行虚继承,必会付出虚函数表指针的空间代价和查找虚函数的时间代价,是否使用虚继承及虚函数应看具体情况