【C++】继承与面向对象设计

news2024/12/22 19:06:54

目录

一、确保public继承塑模出is-a关系

二、避免隐藏继承而来的名称

三、区分接口继承和实现继承

四、考虑virtual函数以外的其他选择

五、不要重新定义继承而来的non-virtual函数

六、不要重新定义继承而来的缺省参数

七、尽量使用复合塑模出has-a

总结


一、确保public继承塑模出is-a关系

还是先以一个简单的例子来说明

class Person
{
    //…………
};

class Student : public Person
{
    //…………
};

根据生活经验我们知道,每一个学生都是人,但是并不是每一个人是学生,这就是所谓的is-a的关系。我们可以预期,对人可以成立的每一件事对学生也是成立的,学生是一种特殊的人。

所以,任何函数如果参数是Person类型(或者是Person类型的指针或者引用),那么也可以将一个student类型传过去。

这个观点只对public继承才会成立,private继承并不属于is-a的关系。

public继承和is-a的等价关系听起来十分的简单,但是十分容易犯错误。

再举一个简单的例子:

企鹅是鸟,符合is-a的关系

鸟会飞,这也是一个事实

按照我们之前所说,企鹅是鸟,但是企鹅不会飞,如果按照上述的继承体系,我们就要好好研究一下,怎么处理企鹅会飞这一问题

有两种做法:

1、令程序在运行期间发生错误

2、在编译期间直接报错,"企鹅不会飞"

我们一般采用的是第二种做法,我们应该防止无效代码通过编译

二、避免隐藏继承而来的名称

我们还要回顾一下C++的相关概念

 所谓的隐藏是指隐藏名称,至于函数的参数和返回值并不重要

接下来再回顾一下作用域

派生类的作用域是内嵌于基类的作用域中

class Base
{
public:
    virtual void mf1() = 0;
    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;
int x;

d.mf1();  // 没问题调用Derived::mf1
d.mf1(x); // 有问题Derived::mf1隐藏了Base::mf1
d.mf2();  // 没有问题调用Base::mf2
d.mf3();  // 没有问题调用Derived::mf3
d.mf3(x); // 有问题Derived::mf3隐藏了Base::mf3

 如果想要调用到Base类中的成员函数,需要我们手动指明作用域

三、区分接口继承和实现继承

身为class的设计者,有时希望派生类只继承成员函数的接口,有时又希望派生类同时继承函数的接口和实现,但是又希望能够重写它们所继承的实现,有时候又希望派生类同时继承函数的接口和实现,并且不允许重写任何东西

class Shape
{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
};

class Rectangle : public Shape
{

};

class Ellipse : public Shape
{

};

我们看这个简单的继承关系

Shape是一个抽象类,因为它有纯虚函数draw,所以它不能够实例化出对象,它只能够实例化它的派生类,但是他还是强烈的影响了与它继承的所有派生类,因为它的全部派生类都继承了它的纯虚函数,如果派生类没有重写该虚函数,导致它的派生类依然是抽象类。

1、成员函数的接口总是被继承的,所谓接口就是值它的函数声明

2、纯虚函数只具体指定接口继承

3、非纯虚函数具体指定接口继承及缺省实现继承

4、普通成员函数具体指定接口继承以及强制性实现继承

但是普通成员函数同时指定函数声明和函数缺省行为,却可能造成危险

假如有一家航空公司,它现在有两种飞机A,B,两者都以相同的方式飞行

下面是简单的代码实现

class AirPlane
{
public:
    virtual void fly(const std::string &destination);
};

void AirPlane::fly(const std::string &destination)
{
    std::cout << " fly "
              << "青岛"
              << " to " << destination << std::endl;
}

class ModelA : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelB : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

为了表明不同飞机的飞行方式,所以fly函数加上了virtual声明,代表继承接口和缺省实现继承

这是一个典型的面向对象的设计,两个class共享同一份fly函数,所有的飞机共性都放到了Base类中,避免了代码重复,减少长期维护的成本。

现在该公司添加了C型飞机,该公司程序员增加了class ModeC,但是忘记重新定义了C的fly函数

class ModelC : public AirPlane
{
public:
    //…………
};

这将造成重大灾难,因为C机型飞行方式根本与前两种飞行方式完全不同

为了避免这样的问题,我们可以将fly函数声明为纯虚函数,只继承它的接口,不继承它的实现,这样因为没有重写虚函数导致C还是抽象类而报错

这里将基类改成抽象类 

class AirPlane
{
public:
    virtual void fly(const std::string &destination) = 0;
};

编译时就会报错,避免了无效代码编译通过

 


class ModelC : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        std::cout << "Model C "
                  << " fly "
                  << " 青岛 "
                  << "to" << destination << std::endl;
    }
};

这里我们重写C机型的fly函数,而其他机型的fly函数什么都不用做


class AirPlane
{
public:
    virtual void fly(const std::string &destination) = 0;
};

void AirPlane::fly(const std::string &destination)
{
    std::cout << " fly "
              << "青岛"
              << " to " << destination << std::endl;
}

class ModelA : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelB : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelC : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        std::cout << "Model C "
                  << " fly "
                  << " 青岛 "
                  << "to" << destination << std::endl;
    }
};

 

四、考虑virtual函数以外的其他选择

假如你在写一个FPS游戏,游戏中的人物有血量限制,需要为任务角色类添加一个成员函数来计算血量。因为每一个人物会有不同的方式计算血量,所以让这个成员函数声明为virtual

class Character
{
public:
    virtual int healthValue() const;
    //…………
};

但是我们还可以选择更好的实现方法

class Character
{
public:
    int healthValue() const
    {
        int retVal = 0;
        //…………

        retVal = doHealthValue();
        //…………

        return retVal;
    }
private:
    virtual int doHealthValue() const
    {
        //………………
    }
};

这种通过非virtual成员函数间接调用private virtual函数称为"non-virtual interface"(NVI)手法

它是Template Method设计模式的一种独特表现形式,这个非virtual函数称为外覆器。

外覆器能够确保在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景

事前工作包括锁定互斥器,记录日志等等


NVI模式下,没有必要让virtual函数一定是private的,可以灵活变通,某些class继承体系要求派生类在virtual函数的实现内必须调用其基类的兄弟,为了让这种操作合法,virtual函数必须声明为protected


另一种做法是使用function来完成Strategy模式

我们可以让每个角色的构造函数接受一个function,function中是计算血量的函数(仿函数,lambda表达式),然后我们直接调用function就能够根据不同的角色来计算血量


int defaultHealthValue(const Character& c);

class Character
{
    typedef std::function<int(const Character&)> HealthCalcFunc;
public:
    Character(HealthCalcFunc hfc = defaultHealthValue)
        :_healthFunc(hfc)
    {}

    int healthValue() const
    {
        return _healthFunc(*this);
    }

private:
    HealthCalcFunc _healthFunc; 
};

五、不要重新定义继承而来的non-virtual函数

class A
{
public:
    void func() const
    {
        std::cout << "Hello A" << std::endl;
    }
};

class B : public A
{
};

B b;
A *pa = &b;
pa->func();//调用A::func

B* pb = &b;
pb->func();//调用B::func

原因是普通成员函数是静态绑定的,pa定义为A*类型,所以通过它调用的普通成员函数永远是指向A所定义的版本

virtual成员函数是动态绑定的,如果func是virtual函数,无论是通过pa调用func还是通过pb调用func,他都会调用B::func

简单说:

先看函数类型,如果是普通成员函数那么就看指针类型,如果是virtual函数那么就看指针实际所指的空间类型

为了避免出现上面的情况,在任何情况下都不应该重新定义一个继承而来的普通成员函数

六、不要重新定义继承而来的缺省参数

根据前面所说,不要重新定义继承而来的普通成员函数

所以本条成立的前提是:继承一个带有缺省值的virtual函数

我们还是看一个小例子

class Shape
{
public:
    enum ShapeColor 
    {
        RED,
        GREEN,
        BLUE
    };

    virtual void draw(ShapeColor color = RED) const = 0;
};

class Rectangle : public Shape
{
public:
    virtual void draw(ShapeColor color = GREEN) const
    {

    }
};

class Circle : public Shape
{
public:
    virtual void draw(ShapeColor color) const
    {

    }
};

这样写出的代码十分的诡异

当以对象调用draw函数时,一定要传参数

因为静态绑定下这个函数并没有从基类继承缺省值

如果以指针或者引用去调用draw,可以不指定参数

因为动态绑定下这个函数会从基类继承缺省参数

解决办法还是NVI手法

基类内的一个普通成员函数调用private virtual函数

class Shape
{
public:
    enum ShapeColor 
    {
        RED,
        GREEN,
        BLUE
    };

    void draw(ShapeColor color = RED) const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle : public Shape
{
public:
    void draw(ShapeColor color = GREEN) const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color) const
    {
        //…………
    }
};

七、尽量使用复合塑模出has-a

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
  
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse) ,因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。


总结


以上就是今天要讲的内容,本文仅仅回顾了C++继承和面向对象的细节

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】Innodb存储引擎之物理存储结构(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/ Liunx内核/ C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1…

云服务器安装jdk

第一步使用工具连接自己的服务器 连接成功后 在左侧选择需要上传的文件到opt目录 在云服务器的命令行操作界面输入指令 解压&#xff0c;输入jdk按table键自动补全 tar -zxvf 配置环境变量 vim /etc/profile 修改环境变量&#xff08;具体视安装 java 地址修改&#xff09; …

计算机毕设Python+Vue学生实验报告管理系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

jQuery 介绍

文章目录jQuery 介绍介绍下载安装jQuery 介绍 介绍 jQuery本身就是用JavaScript来写的&#xff0c;它只是把JavaScript中最常用的功能封装起来&#xff0c;以方便开发者快速开发。遥想当年&#xff0c;jQuery的创始人John Resig就是受够了JavaScript的各种缺点&#xff0c;所…

微服务框架 SpringCloud微服务架构 服务异步通讯 51 死信交换机 51.1 初识死信交换机

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 服务异步通讯 文章目录微服务框架服务异步通讯51 死信交换机51.1 初识死信交换机51.1.1 初识死信交换机51.1.2 总结51 死信交换机 51.1 初识…

java 多线程 上

目录 基本概念 线程的创建和使用 Thread类 API中创建线程的两种方式 Thread类的有关方法 线程的调度 线程的优先级 总结 基本概念 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码&#xff0c;静态对象。 进程(process)是程…

TapTap 算法平台的 Serverless 探索之路

作者&#xff1a;陈欣昊 Serverless 在构建应用上为 TapTap 节省了大量的运维与开发人力&#xff0c;在基本没投入基建人力的情况下&#xff0c;直接把我们非常原始的基建&#xff0c;或者说是资源管理水平拉到了业界相对前沿的标准。最直观的数据是&#xff0c;仅投入了个位数…

代码随想录Day55|392.判断子序列、115.不同的子序列

文章目录392.判断子序列115.不同的子序列392.判断子序列 文章讲解&#xff1a;代码随想录 (programmercarl.com) 题目链接&#xff1a;392. 判断子序列 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。…

koa 使用

&#xff08;贴个官网&#xff0c;koa 内容真不多&#xff0c;非常的小巧轻量&#xff09; 1. koa 是什么 一个更小、更富有表现力、更健壮的 Web 框架。使用 koa 编写 web 应用&#xff0c;通过组合不同的 generator&#xff0c;可以免除重复繁琐的回调函数嵌套&#xff0c;…

关于新正方教务系统(湖北工程学院)的one day越权漏洞的说明

关于正方教务系统漏洞的说明 此漏洞基于湖北工程学院教务管理系统进行演示&#xff0c;漏洞覆盖新正方教务系统8.0以下版本&#xff0c;为本人一年前提交的漏洞&#xff0c;所以并非0day漏洞 此漏洞影响范围巨大&#xff0c;几乎涉及国内一半高校的教务系统&#xff0c;包含武…

我国油气行业勘探开发投入提升 石油资源存在供需短缺矛盾 天然气需求高速发展

根据观研报告网发布的《2022年中国油气市场分析报告-市场竞争策略与发展动向前瞻》显示&#xff0c;油气是指石油和伴生的天然气&#xff0c;被誉为“能源之王”、“工业的血液”&#xff0c;是全世界各国的战略性产业。油气资源种类多样&#xff0c;根据开采难度可分为两大类&…

Python:三方库安装路径及路径变更

文章目录一、安装三方库的几种方式二、指定第三方库的镜像源三、查看安装默认路径四、修改安装默认路径五、查看安装的库六、导出库安装文件七、安装小结一、安装三方库的几种方式 1.直接pip install安装&#xff08;有网的环境下通用&#xff09; &#xff1a; 在python–>…

CSDN上讲得最好的——Linux权限

目录 一、shell原理精讲 二、Linux权限概念 三、权限管理 1、访问者分类 2、文件类型及访问权限 3、表示方法 4、设置方法 &#xff08;1&#xff09;chmod (2)chown (3)chgrp (4)umask 四、目录权限 五、粘滞位 一、超级管理员删除 二、该目录的所有者删除 三、…

GDAL之重投影(详细篇)

一、空间坐标系对应EPSG编号 二、通用横向墨卡托(UTM)投影坐标系和WGS84地理坐标系转换 一、目标地区的编号查看(中国东部地区属于UTM Zone 50N) 从180“W开始&#xff0c;有60个纵向投影区&#xff0c;编号为1到60。除了挪威和斯瓦尔巴群岛附近的一些例外&#xff0c;每个区…

【毕业设计_课程设计】基于 U-Net 网络的遥感图像语义分割(源码+论文)

文章目录0 项目说明1 研究目的2 研究方法3 研究结论4 论文目录5 项目工程0 项目说明 **基于 U-Net 网络的遥感图像语义分割 ** 提示&#xff1a;适合用于课程设计或毕业设计&#xff0c;工作量达标&#xff0c;源码开放 实验训练使用 Anaconda 版 Python 3.7 下的 TensorFlo…

OpenSSL BIO源码简析

文章目录1. BIO简介BIO chainBIO数据结构BIO_METHOD数据结构2. Base64示例分析初始化构造BIO链写数据free1. BIO简介 相关文档 /html/man7/bio.html /html/man3/BIO_*.htmlbio - Basic I/O abstraction&#xff0c;即IO抽象层。 BIO有两种: source/sink BIO&#xff0c;即数…

win7系统升级IE11,打补丁KB2729094失败解决办法

因银行这边很多都需要IE11版本&#xff0c;但win7系统大部分需要打一些补丁才能安装。其他补丁都打上了&#xff0c;唯独这个KB2729094一直失败&#xff0c;搞得很无语。还好找到可以直接用命令安装。就不需要打这个补丁了&#xff0c;直接安装使用即可。 1、下载IE11离线安装…

DBCO-PEG-Dopamine,二苯并环辛炔-聚乙二醇-多巴胺,DBCO聚乙二醇衍生物

●中文名&#xff1a;二苯并环辛炔-聚乙二醇-多巴胺&#xff0c;多巴胺聚乙二醇环辛炔 ●英文名&#xff1a;DBCO-PEG-Dopamine&#xff0c;Dopamine-PEG-DBCO ●外观以及性质&#xff1a; DBCO-PEG-Dopamine产物呈固体或粘性液体&#xff0c;取决于PEG分子量&#xff0c;DBCO…

“R语言+遥感”的水环境综合评价方法

目标&#xff1a; 1、掌握R语言基础应用及水环境数据分析方法 2、掌握水环境遥感数据预处理方法 3、掌握水线提取——水体指数与阈值混合法&#xff08;遥感&#xff09; 4、掌握水深提取——多元回归分析方法&#xff08;R语言遥感&#xff09; 5、掌握水温提取——支持向…

(附源码)springboot学生社团信息管理 毕业设计 011238

目 录 摘要 1 1 绪论 1 1.1 研究背景 1 1.2 研究意义 1 1.3论文结构与章节安排 1 2 学生社团信息管理系统系统分析 3 2.1 可行性分析 3 2.2 系统流程分析 3 2.2.1 数据增加流程 4 2.2.2 数据修改流程 4 2.2.3 数据删除流程 5 2.3 系统功能分析 5 2.3.1 功能性分析 5 2.3.2 非功…