在《类的继承》中详细总结了类的公有继承的相关知识,在大多数的工程中,这种继承都可以满足编程需求,或者说,这是我们常见的继承方式。实际上,除了这种常见的类的公有继承,类还有其他继承方式:私有继承,保护继承,多重继承等。本篇笔记将继续总结类的继承关系,但是本篇笔记的内容可能在实际编程中应用较少,但老生常谈的是为了见多不怪。
一、私有继承
类似于公有继承格式,私有继承即使用关键字private来表达:
class A:private B
{
};
使用私有继承,基类B的公有成员和保护成员都将成为派生类的私有成员,也即不能通过A来调用B中的任何接口。B中的公有接口和变量只可以在A的成员函数中被访问。简单的总结就是A从B获得实现方法但不获得接口,这种不完全继承的关系被称为has-a的关系。
那么如何访问基类中的公有方法呢?只要用类名+作用域解析符即可:
int A::FuncA()
{
return B::FuncB(); //访问基类中的公有方法
}
-
二、保护继承
保护继承即将上述private换成protected。使用保护继承,基类的公有成员和保护成员都将成为派生类的保护成员。和私有派生一样,基类的非私有接口在派生类中也是可用的,但在继承层次结构之外是不可用的。和私有继承的区别在于当该派生类再派生出一个类的时候,使用私有继承,第三代将不能使用基类的接口,而使用保护继承,第三代可以使用基类的方法,因为在第二代中他们是受保护的,受保护的特性就是派生类可用。
class A
{
public:
void FuncA(){};
};
class B:protected A
{
};
class C:public B
{
public:
void FuncC(){FuncA();};//可行
};
若将上述B的继承改为private,则编译会报错:
error C2247: “A::FuncA”不可访问,因为“B”使用“private”从“A”继承
结合公有继承,总结继承方式成员访问权限如下表:
特征 |
公有继承 |
保护继承 |
私有继承 |
公有成员 |
变为派生类的公有成员 |
变为派生类的保护成员 |
变为派生类的私有成员 |
保护成员 |
变为派生类的保护成员 |
变为派生类的保护成员 |
变为派生类的私有成员 |
私有成员 |
只能通过基类的接口访问 |
只能通过基类的接口访问 |
只能通过基类的接口访问 |
三、包含
除了用私有继承和保护继承来表达has-a关系,还可以通过包含来实现。它的形式非常易于理解,就像在类的私有成员部分声明一个普通类型变量一样(类本身就是一种数据类型)。
class D
{
private:
A a;
};
如此,在D类具体实现中,便可以用A类的对象a来调用A中的非私有接口了。如:
class D
{
private:
A a;
public:
void funcD(){a.FuncA();};
};
但在D类之外,使用D的对象d是无法访问到FuncA的,正是“继承了基类的方法,而没有继承接口”。这与私有继承表达的意思相同。
大多数C++程序员倾向于用包含来建立has-a的关系。因为它易于理解和实现,而私有继承会带来更多的问题。但是如果新类要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
四、多重继承
使用多个基类的继承被称为多重继承(MI)。表达形式如下:
class F:public E1, public E2
{
};
必须使用public关键字来限定每个基类。否则,缺省的情况下,编译器会默认为是private。
多重继承带来的两个问题:
1.接口二义性问题
由于F继承于E1和E2,若E1和E2中都有一个接口func,那么用F的对象调用func时,编译器将不知道要调用哪个。
class E1
{
public:
void func(){};
};
class E2
{
public:
void func(){};
};
class F:public E1, public E2
{
};
在main中用F的对象调用func:
F f;
f.func();
则编译器会报错:
error C2385: 对“func”的访问不明确.
可以用作用域解析符来澄清编程者的意图:
F f;
f.E1::func();
但更好的做法是在F中重新定义func,并指出要使用哪个func:
class F:public E1, public E2
{
public:
void func(){E1::func();E2::func();};
};
2.当间接基类祖先相同时带来的多副本二义性
假如上述E1 E2都继承于A,即他们有相同的基类祖先A,当用户创建F的对象时,它将创建两个A副本,那在使用时到底该使用哪个副本将出现问题。而且也会出现上述访问接口的二义性问题。
class E1:public A
{
public:
void func(){};
};
class E2:public A
{
public:
void func(){};
};
class F:public E1, public E2
{
public:
void func(){E1::func();E2::func();};
};
在main中用基类指针去访问F的对象:A *a = &f;编译器将报错:
error C2594: “初始化”: 从“F *”到“A *”的转换不明确
通过E1或E2的强制转换可以编译通过:(可以试一下,通过A的强制转换时不可以的,很好理解,这样还是没能解决二义性问题)
A *a = (E1*)&f;
但是这样做的问题是,f确实存在两个A的副本,浪费资源不说,但是可能还会造成其他二义性,另外,从逻辑上讲,f应具有基类A的某些确定属性,而不是两份属性。
为了解决这个问题,C++引进了虚基类的技术。
五、虚基类
虚基类使得从多个类(他们的基类相同)派生出的对象只继承一个基类对象。虚基类的表达方式为:用virtual修饰基类(不是修饰基类本身,而是修饰间接基类的继承方式),且virtual的位置可以随意。
class E1:virtual public A
{
public:
void func(){};
};
class E2:public virtual A
{
public:
void func(){};
};
这样,A *a = &f;将不再报错,f只有A对象的一个副本。 这样的直接好处是可以用A来实现多态。
这里有个新的问题,这个问题出在构造函数,加入编写如下的构造函数:
#include <iostream>
using namespace std;
class A
{
private:
int ma;
public:
A(int a=0){ma = a;};
void FuncA(){cout<<"A.a = "<<ma<<endl;};
};
class E1:virtual public A
{
public:
E1(int a=0, int e1=0):A(a){};
void func(){};
};
class E2:public virtual A
{
public:
E2(int a=0, int e2=0):A(a){};
void func(){};
};
class F:public E1, public E2
{
public:
F(int a=0, int e1=0, int e2=0):E1(a, e1),E2(a,e2){};
void func(){E1::func();E2::func();};
};
按照之前介绍的单继承,F类的构造函数可以把a值传递到E1和E2,进而传递到A中,但是如果真的这么传递,A即包含了两个副本。虚基类既然消除了这种创建多个副本的情况,既然不会允许这种信息传递。也即上述代码无法将F的a传递到A中。如在main中编写:
F f(1,2,3);
A *a = &f;
a->FuncA();
运行,打印的信息是:A.a = 0
那么如何把1传到A呢(从继承的角度来说,必须这么做),需要在F的构造函数处显式的调用A的构造函数来完成:
F(int a=0, int e1=0, int e2=0):A(a),E1(a, e1),E2(a,e2){};
注意,对于虚基类,必须这么做;但是对于非虚基类,这样做是非法的。总之,在祖先相同时,使用多重继承,必须引入虚基类,并修改构造函数初始化列表的规则。
呼应前言部分,我们看到了私有继承,保护继承和多重继承的一些别扭的地方,所以这些方法或许并不常见,也不建议初学者设计这么复杂的继承关系。单向的公有继承可以满足我们大多的使用需求。在实际工程应用中,也应避免这种原理性问题带来的bug,越简单越可靠的思维也是无可厚非的。这些复杂的规则应该是高级编程专家,或是在开发应用库时应该考虑的方法。