Effective C++ 43,44
最后更新于:2022-04-01 15:50:00
43.明智地使用多继承。
多继承带来了极大的复杂性。最基本的一条就是二义性。
当派生类为多继承时,其多个基类有同名的成员时,就会出现二义性。通常要明确其使用哪个成员的。显式地限制修饰成员不仅很笨拙,而且会带来限制。当显式地用一个类名来修饰一个虚函数时,函数就会被固定,而不再具有虚拟的特性。对于虚函数,若两个基类拥有一个同名同参的虚函数,当派生类没有重新定义虚函数时(可以只声明),直接调用这个同名函数会出二义性错误,需要指明其类。而当派生类中重新定义了这个函数,这是不可能的,因为一个类只允许有唯一同参同名的函数(其实不对,函数体声不声明const,还是不同的,一个用于正常对象,一个用于const对象)。而对于派生类中重新定义虚函数,其实是在派生类中重新创建了一个函数,在这里要重新定义两个虚函数是不行的,因为重定义一个函数就是在创建一个函数,而一个函数里不能有两个同名同参的函数。
当不修改虚函数时,只要用指明基类的方式去调用基类的函数即可,当需要重新定义一个虚函数时,即对于派生类只保留一个虚函数时,不用关心其保留哪个虚函数,只要正常的重新声明定义即可。
而当需要重新定义多个虚函数,且派生类要用到这多个虚函数时,一种所谓的巧妙的方法解决二义性,就是在存在二义性的两个基类下再进行派生,在这两个派生类中给其定义各自的新名字,而函数体为内联的调用基类的函数。而多重继承的派生类多重继承与这两个中间类,就将两个原本冲突的基类函数变成了两个不冲突的基类函数。仅仅为重新定义一个虚函数,而不得不引入新的类。
~~~
class A{
public:
virtual void fun(){cout<<"A"<<endl;}
};
class B{
public:
virtual void fun(){cout<<"B"<<endl;}
};
class AuxA:public A{
public:
virtual void Afun() = 0;
virtual void fun(){return Afun();}
};
class AuxB:public B{
public:
virtual void Bfun() = 0;
virtual void fun(){return Bfun();}
};
class C:public AuxA,public AuxB{
public:
virtual void Afun(){cout<<"A in C"<<endl;}
virtual void Bfun(){cout<<"B in C"<<endl;}
};
class D:public A,public B{
};
int main(){
D* d = new D();//对于正常的情况,
A* aaa =d;//当其使用基类的指针时,
aaa->fun();//就会调用基类的虚函数,而不会发生冲突
B* bbb = d;//完全没有体现到多态
bbb->fun();
//d->fun();//而直接调用会二义性。
C* c =new C();
//c->fun();//改名后原来的这个函数还是二义性的。
A* aa = c;
aa->fun();//输出 A in C
B* bb = c;//输出 B in C
bb->fun();
AuxA *a = c;
AuxB * b = c;
a->fun();//输出 A in C
b->fun();//输出 B in C
~~~
这样就同个改名而在派生类中重新定义了两个基类的同名函数。
而除了二义性,还经常碰到的问题就是菱形继承,也就是一个基类被继承多次,但是否应该保存多个拷贝的问题。一般来说都是只拥有一个这样的基类,即将其声明为虚基类。
但这样也是有问题的,首先程序开发这设计一个基类A派生了多个基类BC,但是在其定义BC时无法知道以后是否有人会多继承BC,而后人想要修改BC的定义使其虚继承于类A又是很难做到的,一般ABC都是只读的库函数,而D由库的用户开发。另一方面,如果A声明为BC的虚基类,这在大部分情况下会给用户带来空间和时间上的额外消耗。
而对于虚基类,若A为非虚基类,则D的对象在内存中的分配通常占用连续的内存单元,而若A为虚基类,会包含一个指针指向虚基类数据成员的函数单元。这里对于A派生BC,而D继承与B和C,则D的内存中有两个A,而如果是虚基类,D中有两个指向A的指针。
所以考虑这些,进行高效的类设计时,若涉及到MI 多继承,作为库的设计者就要具有超凡的远见。
向虚基类传递构造函数参数。对于单继承中,派生类在成员初始化列表中对基类传递参数,由于是单继承的,这些参数可以逐层的向上传递。但虚基类的构造函数就不同了,它的参数由继承结构中最底层的派生类的成员初始化列表来指定,如果有新类增加到继承结构中,可能要修改执行初始化的类。避免这个问题的办法是消除对虚基类传递构造函数参数的需要,最简单的就是java中的解决方法,即避免在虚基类中放入数据成员,java中的虚基类 接口禁止包含数据。
虚函数的优先度。当虚函数在多重继承中涉及到虚基类时,会有优先度的问题,仍然以ABCD为例,A中有虚函数void fun(),C中重定义了fun,但B和D中没有,调用D的指针fun时,若A不是虚基类,则是正常的情况,发生二义性错误。但是当A是虚基类时,就可以说C中重定义的fun的优先度高于最初的A中的定义也是B中的fun,则此时会无二义性的解析为调用 C::fun()函数。
当对于原 B类继承于A类,现今需要新加入一个新类C继承与A,但是你发现其与B类用许多相似的地方,但C又不是一个B,所以你决定让C 由B实现,便让C私有继承于B,同时继承A,同时修改了一下B中的虚函数,实现了多继承。而另一种做法是将BC的共同点放在一个新的类D中,改变继承结构,使D继承于A,而BC继承于D,这样就只有单继承了。表面上看来,多继承没有添加一个新的类,没有改变原有的继承结构,它只是在原有的类B的基础上增加了一些新的虚函数,这样看似增加了大量的功能,而只增加了一点点复杂性。但事实让,引入多继承就会带来许多麻烦。
MI是复杂的,但也是有用的,需要明智的去使用。
44.说你想说的,理解你想说的。
简单来说,理解面向对象构件在c++中的含义,而不是只去记忆c++的语言规则。对c++理解越深,越能清晰的考虑问题。