C++笔记---面向对象编程之继承关系的讨论

一、前言

面向对象编程(OOP)是C++联邦中一个非常重要的部分,最近在学习effective C++的时候正好学习到这部分,所以就想按照自己的逻辑对其做一个总结,方便学习回忆。而继承是C++三大特性(继承、多态、封装)之一,其中涵盖的知识点比较多,比如说:继承可以是单一继承或者多重继承,继承关系可以是public、private或者protected,成员函数可以是pure virtual、impure virtual或者non-virtual等等。

在经过学习之后发现自己对于继承的理解方式改变了,因为之前只知道去记住继承的各种知识点,类似于死记硬背,对其背后的本质原因不太理解,这里说的本质原因并不是底层的实现,而是每种继承方式的相关知识点分别对应着如何的设计理念,也就是我设计的软件产品决定了我采用什么样的继承方法:比如public继承使用于什么场景,成员函数设计为virtual又有什么深刻含义等等。

二、为什么需要继承

我觉得任何一个特性都有其背后的需求,否则这个特性就没啥用那还设计出来干啥,所以先弄懂为什么需要某个特性有利于形成顺畅的思考逻辑。继承的本质就是我们希望能够重用别人的类来实现我们自己的类,这是一种代码复用的重要的手段,比如说现在有一个类是进行画圆的class DrawCircle,现在我想画各种颜色的圆,比如说画红色圆class DrawRedCircle,那么我最简单的方法是继承DrawCircle,然后在其基础上增加个颜色就可以了,而不是需要从头实现个完整的画红色圆的类,这样不但造成代码冗余,软件的设计还没有层次感,后续维护和扩展比较麻烦。所以继承是方便我们依赖已有的类快速实现自己的类的重要方法。
那下一步自然而然的我就考虑如果我设计自己的类该考虑啥?最基本的类包括继承来的和自己需要增加的:对于继承来的,既然有public、private、protected三种方式我该选择哪一种呢?对于自己的东西,为了控制其权限,我该用public、private、protected哪种修饰符呢?按照一开始的说法,选择哪种继承关系和访问限定符是由实际的场景需求决定的,所以还是回到一开始我们了解问题的本质:不同的特性选择其实就代表了对于软件系统的想法!,所以本节的内容就重点讲一下各种继承方式和访问限定的基本用法(毕竟熟悉一下才能正确选择)以及各种继承方式分别对应的内在的设计理念。主要内容如下:

  • public、private、protected用在访问限定和继承关系的特点
  • 继承方式的内在理念
    • 什么时候我们需要public继承
    • 什么时候我们需要private继承
  • 讨论一下复合的含义

1、 public、private、protected用在访问限定和继承关系的特点

我们在实现一个class的时候,经常会遇到对成员的访问限定的设置:这个成员变量和成员函数该设置为public还是private啊?还会遇到继承方式的设置:是该public继承还是private继承啊?这一小节我们先不纠结到底该怎么选,我们先看看不同的选择有哪些特点(这里先不考虑友元)
对于访问限定我们将区域分为3个:自己类内,自己派生的类,外界。这里大体遵循的原则是:private修饰的成员只有类内的成分可以访问,protected修饰的成员可以放松条件,自己(public)派生的类内的成分可以访问,而public最不隐秘了,外界的成分也可以访问,这里外界的成员指的是可以通过类实例化的对象访问public修饰的成员,我们以实际代码看一下:

class Base{
public:
    int _a;
    void display(){
		std::cout<<_a<<" "<<_b<<" "<<_c<<std::endl;
	}
protected:
    int _b;
private:
    int _c;
};
Base base;
std::cout<<base._a<<std:endl; //正确
std::cout<<base._b<<std:endl; //错误
std::cout<<base._c<<std:endl; //错误

对于上面的_a_b_c,类内部的成分指的是类内的函数,比如说display()函数,它有着最高的访问权限,即使是private修饰的_c也能访问,毕竟是类内部的成分;而外界访问不到private指的是代码最后两行,不能通过实例化的对象访问类中非public的成员,所以最后两行是错误的。那么对于protected修饰的_b呢,其实他的作用是为了让Base的派生类内部能访问得到_b,而外界访问不到,这个涉及到继承,看下面代码:

class Derive : public Base{

}

我们假设Derivepublic继承自Base,那么Derive内部和外界对于Base部分的成员访问权限如何呢?我我们先把关系表放在下面然后分析:

类成员/继承方式public继承protected 继承private 继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

通过上表可以知道,如果基类成员是private,无论什么继承方式派生类都是不可见的,因此Derive类是不能访问到Base::_c的,由于是public继承,因此Base::_aDerive中依然是public,也就是外界也可以访问得到,Base::_bDerive中依然是protected,可以在Derive类内和其派生类内部访问,外界访问不到;假设Derive是以private的方式继承自Base,那么Base::_aBase::_cDerive中就是private的了,只能在Derive内部访问的到,而Base::_c依然是不可见的。

可以看到:继承方式其实改变的是基类成员变量的访问限定,这是表象,重点是改变了基类成员的访问限定对应的设计理念是啥?这是下一节重点总结的。

这一部分其实可以当个手册来用,如果忘记了就随手过来翻翻,因为这里对继承方式所产生的访问限定进行了总结,类似于前面说的死记硬背,并不涉及到设计理念,但是这是基础,因为要设计产品的时候总不能连基本的特性都不知道吧?就好比想写一篇文章,我也得首先知道每个字怎么写,但是写文章的重点不是写“字”,而是表现你的想法,字记不住可以去字典查,这也是将这一部分作为手册的原因,会用就行。

2、继承方式的内在理念

这个部分主要是讲public继承和private继承的,前面我们已经知道两种继承的特点的,也就是对成员变量的限定作用,但是对于软件设计更重要的是我们如何根据实际情况选择适当的继承方式呢?这就需要我们对两种继承方式有一个更本质的认识才行。

什么时候我们需要public继承

这里首先把结果说出来:public继承是一种“is-a”的关系,该怎么理解呢?

假设Derive类public的方式继承自Base类,那么就说明Derive也是一种Base,对于Base所有的操作对于Derive同样适用,比如:Base是人,Derive是学生,学生当然也是人,所有人的行为比如吃饭、睡觉、呼吸等学生也可以,但是学生的学习行为就不能普遍适合于人,所以当使用public继承的时候,首先看看派生类是不是一种基类,也就是说基类的行为派生类也都有,那这样的话根据这个关系可以总结一下public继承的特点:

  • 一个函数以Base类对象作为参数,那么也可以以Derive类对象作为参数
  • Base类的普通成员函数在Derive类中不应该被重写了

第一点不难理解,因为Derive也是一种Base,那我对Base的操作肯定可以用在Derive上,这里有一个内在的编译解释,那就是:当面对public继承的时候,编译器可以将一个Derive对象转为Base对象,这也是为什么需要Base类对象作为参数的时候同样也可以将Derive作为参数。第二点的意思是,既然Base中定义了普通函数,那么意思是这个普通函数就是Base类的特征,Derive既然也是一种Base,还有啥必要重新定义了?就好像人这个类定义了吃饭的动作,学生这个类就不需要定义吃饭了,因为学生也是人,吃饭难道还能吃出花来?要是重新定义吃饭,那么就违背public继承是"is-a"的原则了。

因为本篇博客不讲具体的实现细节,重点是讲述对不同继承方式的认知,所以我们以举例子为主。所以啥时候使用public继承?那就是Derive类也是一种Base类的时候!!!而且要结合具体的开发需求来定。这个观点看似很好理解,但是实际使用时却容易分辨不清到底如何才是相同的属性?

比如鸟作为Base,企鹅作为Derive,因为企鹅是一种鸟,所以企鹅以public继承自鸟应该没问题吧?看似是没问题的,但是当我们为鸟类写一个fly函数的时候,因为fly是鸟的天性,企鹅也是一种鸟,那么企鹅也可以调用fly了?现实告诉我们这是错误的,因为企鹅不会飞!那问题出在哪里了呢?
首先是我们对于鸟的认知问题:鸟都会飞吗?不是啊,企鹅就不会啊,所以将fly作为鸟的方法是不对的,因为Base类的方法反映的是Base类的基本特征,也是其通过public方式派生出的Derive类的特征,现在企鹅并不会飞,所以用飞作为Base特征是不合适的,那什么时候合适呢?如果保证鸟派生出来的都是燕子、杜鹃、麻雀等派生类,那么fly作为鸟的特征就很合理了啊。所以这个例子告诉我们在设计Base类的时候需要结合你想派生出什么样的Derive类,这决定了你需要用什么特性作为Base类的方法,而不是死钻牛角尖:鸟就不都会飞嘛!可是如果我派生的鸟都是燕子、杜鹃、麻雀等,那我使用fly作为Base类的特性有什么不行呢?这也验证了一个原则:程序的设计必须要结合实际的环境

effective C++还举了一个矩形和正方形的例子:在我们的认知中正方形也是一种矩形这是正确的,所以正方形以public继承的方式继承矩形是很合理的。但是考虑一种情况,假设我们在矩形类中定义一个改变矩形宽的函数,这并不会影响矩形的长,但是在正方形类中调用改变矩形宽的函数,其实也改变了正方形的长,因为对于正方形来说有一个限定条件就是正方形的长和宽必须是相等的。所以我们在决定使用public继承关系时,必须要保证对于Base类的所有操作都适合于Derive类,而矩形和正方形的关系并不符合,因为矩形“可以独立修改宽而不影响长”却并不适合于正方形,除非保证在程序设计中不触发这个操作。

什么时候我们需要private继承

如果我们将public继承举的人和学生的例子改为private继承,那就变成了:人可以吃饭,学生可以学习,但是如果对象是学生的话,学生是不可以吃饭的!!!由此可见,private继承并不是“is-a”的关系,而这种现象在编译层面的解释是:当private继承的时候,编译器是不可以将Derive类转换为Base类型的,这也就是上面举的例子中,如果是private继承的话,学生不能吃饭。
private继承的内在理念是"implemented-in-terms-of",也就是根据某物实现出,这说明如果Deriveprivate方式继承自Base,那么就可以得到以下认知:

  • Derive并不是一种Base,对Base的一些操作对Derive并不能适用
  • 对于Derive而言,仅仅是利用了Base的某些方法来构造出自己,是一种“利用”

Derive仅仅是利用了Base类的某些特性,不必重复造轮子了,其实私有继承是没有特别多的设计层面上的含义的,因为根据前面的继承特点可以知道,如果继承方式是private继承的话,那么Base中的private修饰的成员在Derive中不可见,并且本来能被外界访问的public成员在Derive中也变成了private成员,而且又由于private继承的时候,编译器不能将Derive类转换为Base类,所以综上得知,Base中的东西是不能被Derive实例化的对象所访问的,Base类仅仅起到了协助构造的作用,Derive类需要的一般是Base中方法的实现,而不是继承自Base的接口。所以private继承使用的比较少,更何况如果仅仅是利用Base的特性,我们还可以使用复合的构造方式。

3、讨论一下复合的含义

上面我们讨论了public继承和private继承的设计理念的问题,其实本质上继承还是在利用现有的东西提高代码复用,减少从头造轮子,这也是继承的重要特性,继承方式的不同决定了派生类是在保持本性(“is-a”)的基础上增加了功能还是利用(“implemented-in-terms-of”)Base来实现自己,但总归而言这都是提高代码复用,那么除了继承还有没有其他这样利用其他类来构造本类的方法呢?当然有,这里就来讨论一下复合。
复合:当一种类型的对象内含有另外一种对象,就构成了复合。也就是说,B类的对象作为了A类对象的成员。

class B{...};
class A{
...
private:
	B b;
...
};

其实上述复合的形式我们经常遇到,知识没有深入的研究其内在的设计理念。比如我们肯定写过有string类型的成员变量的类,其实就符合上述形式,因为string也是一个类。我们最普遍的认知是b作为A的一个成员,也就是bA的一个属性,比如说如果A是Person的话,那B可能是身高,电话等等,但是B并不是一类A,因为身高和Person并不是一个类型,所以public继承的“is-a”并不适用与这里,在这里应该是“has-a”的意义,这也是我们经常使用的,也最符合我们的认知。但是复合还有一种意义,那就是“implemented-in-terms-of”,和private继承是一样的!在这里A是利用的B中的某个特性,而A的外界代码是访问不到b的,这就和private继承的时候,派生类外界代码也是访问不到基类的(private继承的特点);

那这样其实就存在两种类结合的模式可以实现“implemented-in-terms-of”:private继承和复合,那我们到底选择哪一种呢?这里我们后面需要单独讨论一下,因为这一篇主要是从理念上理解不同继承方式的区别,所以暂时不总结太多细节的东西了。

三、总结

这里我们讨论了三种类之间的内在关系:“is-a”、“has-a”、“implemented-in-terms-of”,其实在多数的继承中使用的还是public继承,即派生类和基类是一个类型的,派生类是基类的特化;如果我们通过分析明确两个类并不是“is-a”的关系,那就需要考虑另外两个了,如果关系符合“implemented-in-terms-of”至于我们是使用复合还是private,本质上都能达到要求,后面针对具体的问题分析哪个最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值