C++学习笔记(16)——类的继承(续)

在《类的继承》中详细总结了类的公有继承的相关知识,在大多数的工程中,这种继承都可以满足编程需求,或者说,这是我们常见的继承方式。实际上,除了这种常见的类的公有继承,类还有其他继承方式:私有继承,保护继承,多重继承等。本篇笔记将继续总结类的继承关系,但是本篇笔记的内容可能在实际编程中应用较少,但老生常谈的是为了见多不怪。

一、私有继承

类似于公有继承格式,私有继承即使用关键字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,越简单越可靠的思维也是无可厚非的。这些复杂的规则应该是高级编程专家,或是在开发应用库时应该考虑的方法。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bjtuwayne

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值