当前位置: 首页 > news >正文

C++感受12-Hello Object 派生版

不变的功能,希望直接复用原有代码;变化的功能,希望在分开的代码里实现。

  1. 派生的基本概念和目的
  2. 如何定义派生类以及创建派生对象
  3. 派生对象的生死过程 

0. 课堂视频

ff14-HelloObject-派生版

1. 派生的基本概念与目的

编程,或者说软件系统的设计与实现的主要工作和最大痛苦来源,就是:变化。由于要面对变化,于是产生了许多程序设计思想、原则或方法,派生正是其中之一。

1.1 变与不变的代码原则

写程序时,很多时候,会发现当前地方需要用到的功能,在之前的某个地方,已经实现过一回了;又有很多时候,会发现,当前需要在原来的功能上,实现一些新的功能。这时候,一条基本且重要的原则出现了:

需要用到一样的功能,我们希望直接复用原有代码,而不是重新写一遍(哪怕你有复制粘贴大法);而当需要写不一样的功能时,我们希望新功能和旧功能能够在分开的代码中写,而不是混在一起写。

这个原则很好理解:

  1. 实现同样功能的代码如果到处出现,不仅费时费事,而且将来功能有变动时,就得到处修改;
  2. 同一段代码,却包含了多种不同逻辑的实现,表现看起来非常强大,但一来整体逻辑变得很复杂,并且非常容易写错。

派生就可以同时满足以上两点要求的一种编程方法(归在“面向对象”的编程思想中)。

课堂讲到的,实现“区分对待女神与普通人的自我介绍功能” 的 “if/else” 和 派生方案的对比,目的就是为了让同学通过实例,体会、理解到这个原则所能发挥的作用,以及发挥作用的方法。

一句话:在新的类型定义(当前我们学习了如何定义 struct )中实现新功能,从而保证“新功能和旧功能分开写”;而又能同时复用(继承)原有类型的原有功能。在这段话的描述中,新的类型就是派生类(derived-class),旧的类型叫基类(base-class)。

基类和派生类之间,可以称为 “派生 / derive ”,另一种常用的说法是“继承 / inherit”。当我们说“继承”时,通常是在强调新的类型从旧的类型身上复用(继承)了原有的功能;而当我们说“派生”时,则通常是在强调新的类型将在旧的类型的基础上,扩展(派生)了一些新的功能。两种说法是同一硬币的两个面,关系紧密,比如派生新功能时所用到的基础,往往就是继承自基类的原有方法或数据。

跨越语法的,通常用以下图形表示类型和类型之派生关系:

继承关系

阅读笔记

1.2 变与不变的具体例子

这是我们在上一节课 《Hello Object 成员版》 写的一段代码:

// 定义人类结构 
struct Person 
{ Person() { std::cout << "哇哇~" << std::endl; }; ~Person() { std::cout << "呜呜~" << std::endl; } // 自我介绍 void Introduce() // 成员函数,方法 { std::cout << "大家好,我叫 " << name << std::endl; }  std::string name; // 成员数据,属性 
}; 

新需求是:美女能够有不同的自我介绍方式,但美女的生死过程要和普通人类(Person)一样地会“哇哇”或“呜呜”。

当然可以借助 if/else 来实现,只需要在原来就有的 Introduce () “动刀”:

void Introduce()
{if (name == "志玲"){// 在此处实现女神特有的自我介绍}else{std::cout << "大家好,我叫 " << name << std::endl;}
}

这种实现方法,除了用名字来判断一个人的外貌这一固有的“原罪”之外,还有个问题:不符合我们前面谈的基本原则中的第二个要求:新旧功能分开写。

3. 派生的基本语法

3.1 定义派生类

以我们熟悉的 struct (用户自定义类型方法之一)为例:

struct 派生类 : public 基类 
{ 
};

C++ 支持多种派生方法,比如私有派生和公有派生,其中公有派生最常用。上面示例代码中的 关键字 “public” 即指明这是一个公有派生关系。

当派生类是 struct 时,此处省略 public 也同样表示公有派生。

给个具体例子:美女类(Beauty)派生了(继承自)普通人类(Person)。

struct Beauty : public Person 
{ 
}; 

此时,表面上看,Beauty 结构中间空空,一无所有,但其实它已经拥有了继承自基类的成员数据 name 和 成员方法 Introduce() 。

依据需求,美女类将拥有自己的自我介绍方法:

struct Beauty : public Person
{void Introduce() { cout << "大家好,我是美女" << name << ",想得到大家的多多关照哦~" << endl; }
};

注意,派生类的 Introduce() 方法的原型(函数三要素:名字、返回值、入参列表)和基类的完全一样,但实现改变了(更嗲一点?)。可以放心的是,两个版本不会打架。基类的对象使用基类的,派生类的对象使用派生类的。

这里讲的就是之前课程提到的“类型即约束”。基类类型约束基类对象,派生类类型约束派生类对象,各自安生……直到下一节课,有些对象会突然醒悟,发出 “王侯将相,宁有种乎”的呐喊……

可能你已经注意到了,Beauty 确实非常直接地使用到了 name ——如前所述,它继承自基类。

更需要注意到的是:Beauty 类完全没手写的构造函数和析构函数。在此情况下,依照 C++ 语言标准,编译器会自动为它生成有默认行为的构造和析构函数,并且(重点)二者的默认行为中,包括了各自去调用基类的版本。即:

  • 派生类默认的构造函数,会自动调用基类的构造函数;
  • 派生类默认的析构函数,会自动调用基类的析构函数。

所以,尽管什么也没写,但我们在构造一个 Beauty 的对象时,它会输出 “哇哇~”,而它在释放时,也将输出 “呜呜~”。

3.2 定义派生类对象

派生类也是类,所以还是通过类型定义一个对象(变量)的那一套,我们同样给出栈对象和堆对象的例子:

/* 此处是基类 Person 和 派生类 Beauty 的类定义,略 */ 
int main()
{Beauty b1;b1.Introduce();Beauty *b2 = new Beauty(); // 或 new Beauty b2->Introduce(); delete b2;
}

3.3 完整代码


#include <iostream>
#include <string>// 基类
struct Person
{Person() { std::cout << "哇哇~" << std::endl; }~Person() { std::cout << "呜呜~" << std::endl; }// 自我介绍 void Introduce() // 成员函数,方法 { std::cout << "大家好,我叫 " << name << std::endl; }std::string name; // 成员数据,属性
};// 派生类 
struct Beauty : public Person
{void Introduce() { cout << "大家好,我是美女" << name << ",想得到大家的多多关照哦~" << endl; }
};int main()
{Person xiaoA; // 变量小A,普通人类 xiaoA.name = "小A";Person 如花; // 小心出现中文符号 如花.name = "如花";Beauty zhiLing;zhiLing.name = "王钢蛋";auto* jiaLing = new Beauty;jiaLing->name = "嘉铃"; // 我更喜欢 “加0”xiaoA.Introduce();如花.Introduce();zhiLing.Introduce();jiaLing->Introduce();delete jiaLing;
}

为什么非要有个堆变量呢?因为下节要用啊,好怕同学们才隔一节课就忘记了。

4. 派生对象的生死过程

4.1 多级构造与析构过程

  • 派生对象构造时,先调用基类的构造函数,再调用自己的构造过程;
  • 派生对象析构时,先调用自己的析构函数,再调用基类的析构过程。

如果把基类比成一楼,派生类比成二楼,那就是:

  • 构造就是盖楼,先盖一楼,再盖二楼;
  • 析构就是拆楼,先拆二楼,再盖二楼。

如果还有三楼、四楼,整个过程依次延顺。

注意,画楼层时,通常二楼在上,一楼在下。但画派生关系图时,基类在下,派生类在上,且箭头是从派生指向基类。

4.2 示例程序

关于派生类构造也析构的完整测试代码:

#include <iostream>using namespace std; // 间接限定 struct ShaFaTie // 沙发贴,“爷爷类” 
{ ShaFaTie() { cout << "哈哈,捡到沙发,笑抚二楼狗头。" << endl; }~ShaFaTie() { cout << "我是一楼,结贴。" << endl; } 
}; struct BanDengTie : public ShaFaTie // 板凳贴,“爸爸类” 
{ BanDengTie() { cout << "[回复] 抢到板凳。一楼你好舒服!笑看三楼躺地板。" << endl; } ~BanDengTie() { cout << "我是二楼,结贴。" << endl; } 
}; struct DiBanTie : public BanDengTie // 地板贴,“孙子类” 
{ DiBanTie() { cout << "[回复] 三楼怎么啦?席地而坐,凸显不同。" << endl; } ~DiBanTie() { cout << "我是三楼,结贴。" << endl; } 
}; int main() 
{ ShaFaTie l1; cout << "===============\n"; BanDengTie l2; cout << "===============\n"; DiBanTie l3; cout << "===============\n"; 
}

相关文章:

  • C语言pow函数简单介绍
  • Linux 端口
  • IO、零拷贝、多路复用、connection、池化
  • 一文让你彻底搞懂什么是CDN
  • Linux RHEL 8.6在安装PostgreSql时提示缺少en_US.UTF-8
  • 证券交易系统中服务器监控系统功能设计
  • 前端代码规范 - 日志打印规范
  • FineBI在线学习资源-数据处理
  • 【ABB】控制器语言切换
  • LLM - 神经网络的训练过程
  • C++实现简化版Qt的QObject(3):增加父子关系、属性系统
  • 遗漏知识点
  • 【全网最全ABC三题完整版】2024年APMCM第十四届亚太地区大学生数学建模竞赛(中文赛项)完整思路解析+代码+论文
  • 【Spring】DAO 和 Repository 的区别
  • 开发经验:go切片的继承
  • [译] 怎样写一个基础的编译器
  • android 一些 utils
  • Android组件 - 收藏集 - 掘金
  • angular学习第一篇-----环境搭建
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • ECMAScript入门(七)--Module语法
  • ECS应用管理最佳实践
  • isset在php5.6-和php7.0+的一些差异
  • javascript 哈希表
  • Protobuf3语言指南
  • Python打包系统简单入门
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • supervisor 永不挂掉的进程 安装以及使用
  • 读懂package.json -- 依赖管理
  • 多线程 start 和 run 方法到底有什么区别?
  • 翻译--Thinking in React
  • 力扣(LeetCode)21
  • 前端性能优化--懒加载和预加载
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 用mpvue开发微信小程序
  • 最近的计划
  • 数据可视化之下发图实践
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #数据结构 笔记三
  • $L^p$ 调和函数恒为零
  • $refs 、$nextTic、动态组件、name的使用
  • (2024最新)CentOS 7上在线安装MySQL 5.7|喂饭级教程
  • (c语言)strcpy函数用法
  • (k8s中)docker netty OOM问题记录
  • (pojstep1.1.1)poj 1298(直叙式模拟)
  • (react踩过的坑)antd 如何同时获取一个select 的value和 label值
  • (博弈 sg入门)kiki's game -- hdu -- 2147
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (规划)24届春招和25届暑假实习路线准备规划
  • (译)2019年前端性能优化清单 — 下篇
  • (转)EOS中账户、钱包和密钥的关系
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)
  • .NET C# 使用GDAL读取FileGDB要素类
  • //解决validator验证插件多个name相同只验证第一的问题