C++感受14-Hello Object 封装版 - 上
1. 封装即约束——封装和派生、多态的本质区别
一门计算机语言,要如何帮助程序员写出优秀的代码?两个方法:一是给程序员更多能力,二是给程序员更多约束。之前我们学习的派生和多态,更多的是给我们技能,而封装,更多的是想约束我们。这就是封装和派生与多态最本质的区别。
简单复习下派生和多态:
- 派生——继承遗产
派生可以让我们仅在基类拥有一个属性或实现一个功能,它的所有派生类以及派生类的派生类,就都将得到这个属性或功能,这很像现实生活中,子孙从祖辈那边继承到巨额财富的情况 - 多态——继承遗志
多态则让在基类仅仅制定一个目标、或写下一个愿望,所有派生类就会各自努力去实现这个目标或愿望。这有点像父母们自己做不到的事(比如考上名校),却偏要为儿女后代们制定各种规矩——偏偏 ,后辈们还真的一个个努力实现了,不实现也可以,代价是:不实现祖辈遗愿的派生类,他们产生不了对象(莫非这就叫“无脸见人”?)
派生和多态还可以——事实上非常惯用——结合起来。以考上名校为例,基类可以自己先读上北大,而后它的派生类非常舒服:要么继承基类读北大的能力,要么嫌北大不好,改为努力去读清华……
就是这么舒服——这是因为派生和多态,本质是在给我们(程序员)能力。
但是今天我们要学习的面向对象的最后一个特性“封装”,就不是这样子了,相反,它总是想着法子来约束我们——严格讲,是希望我们多用它提供的各种办法,来自我约束。
2. 把约束封装成类型
2.1 海选报名的选手怎么定义?
假设在一个业务场景是 “选美大赛” 的项目代码中,你看到名为 “Person” 和 “Beauty” 的两个类型(比如struct),你会怎么想?
你肯定会有这样一个判断:Person 类型的对象,应该是有的长得好看,有的一般,而如果是 Beauty 类型的对象,就肯定得是美女。
正常来说,这个判定肯定是对的(当然,也有不正常的,比如 2024 年美国小姐选美,课堂视频里有它的冠军,希望你喜欢)。现在的关键问题是,作为程序员,你要如何保证 Beauty 类型产生的对象,在身材、长相(当然,应该也要包含心灵)都符合美女的要求?
在之前的课堂里,我们已经学习过 “类型即约束”,所以,一个 int 类型产生的对象(变量),它一定要能参加四则运算(扣除为零时不能作为除数的情况)……但是,像int、char、bool、float、double 这些都是语言内置类型,它们的约束,由语言硬性规定,而,当我们我们当上造物主,要如何让我们所创造的类型,拥有说一不二的约束力呢?
约束他人,先从约束自己做起。当我们写下一个结构的名字,名字不具备任何实质约束,就像现实生活中自称 “美女” 的人不一定美一样,我们必须往这个结构里添加字段——这就有了今天的第一个重要知识点:每当你往结构里加一个字段,你就是在为这个类型加一个约束,所以请保持克制。
就以这个 Person 为例,在程序世界里,如果它不拥有任何字段,那么,Person 类型对它的对象就没有任何要求(约束)。意思是,世间任何事物都可以映射到这个程序中的“人类”定义——一块石头也可以自称是人类。而一旦加了字段,就开始产生要求。比如,加上 “性别”,那么,无性繁殖的事物,就不可能成为这个程序中的人类对象。
继续以 “选美” 大赛为例,我们让需求再明确一些:希望用 Person 来表达选美赛海选阶段来报名的选手,那么,我们可以或者需要在 Person 中加入什么约束呢?这事不能光听老板,或者迷信技术大佬,我们得听具体的业务要求。
假设,本次海选有这么一条规则:选手必须是女生,但是在海选阶段,我们也非常欢迎 “伪娘”前来报名……
- 为什么有这么矛盾的需求?
组委会说了,这样才容易产生有利宣传的噱头,以求在海选阶段引发更多关注……
所以,Person 类型至少需要有个“性别”字段,而一旦有性别字段,就意味着漂亮的灯塔水母,它们受到约束,无法报名了……
- 灯塔水母虽为“母”,但它们其实是无性动物……
接着,假设报名的海选评比,对选手的身高有要求,那么,就得为 Person 类加上身高字段,同理、我们还要检查体重、还要检查年龄……于是:
// 海选报名选手类定义的伪代码
struct Person
{性别身高体重年龄
};
切记:类型所拥有的每一样属性,都是这个类型在表达的一个约束。
2.2 海选胜选后的美女,怎么定义?
我们让业务继续发展:如何定义那些通过海选成功进入二选的选手?
常见的错误思路:让 Person 通用一点,既可以表达海选选手,也可以表达二选选手。这种想法,根源上可能是想让某个类型具备更好的可复用性。
同一事物放在A处也能用,放在B处也能用,这是对 “复用” 的侮辱。请理解,类型具有好的可复用性和代码具有好的可复用性的区别。
假设二选考核指标为三围数据,那么按上面的思路,就应该在 Person 中加入三围数据:
// 让 Person 变得 “通用” 的思路下,Beauty 的定义(伪代码)
struct Person
{性别身高体重年龄胸围、腰围、臀围
};
这就叫作程序设计中的“放纵”,现在,当一个 Person 对象用于表达普通选手时,用户(程序员,包括你自己)需要记住:此时三围数据没什么用,更糟糕的是,除非看代码的上下文(包括注释),否则一个用户(程序员,包括你)只看到某个 Person 对象,他很容易陷入这样一个困境:我必须清楚这个 p 的真实身份,偏偏代码写得让人很难清楚。
解决办法有两种:一是让功能可以复用,同时对象有清楚的类型,这种方法叫“继承/派生”;二是让功能可以复用,同时用户(程序员,包括你)可以不去关心它的具体身份类型,这种方法叫“多态”。
先来看第一种方法:派生。
// 派生的好处:又复用,又类型明确 (伪代码)
struct Person
{性别身高体重年龄
};struct Beauty : Person
{胸围、腰围、臀围
};
现在,Person 类型并不具备可复用性,它无法用来定义一个通过海选的选手,但是 Person类定义这段代码被很好地复用了——以 基类的形式被派生类使用——并且,这种复用,Person类本身毫不知情。
小节知识点
编程时,往类型定义里添加字段,就像做菜时往菜里倒调料:加时容易,想再挑出来就难了。
3.成员访问控制的约束
成员访问控制,用来控制类型的成员(包括成员数据和成员函数)的可访问范围。C++分了三级:
- public(公开的)
- protected(受保护的)
- private(私有的)
为什么要加成员访问控制?假设海选阶段报名选手的类型最终定义如下:
struct Person
{int no; // 编号std::string name; // 姓名int gender; // 性别(0女,1男)int age; // 年龄int height; // 身高int weight; // 体重
};
然后有一个报名函数,用于登记选手信息,代码如:
void 报名()
{Person a; // 构造时,选手会输入自己的各项信息登记(a); // 将当前选手登记到大赛系统中a.gender = 0; // ???...
};
报名函数打问号那行显得非常可疑,怎么会登记之后突然又去设置选手性别呢?这是要让选手在报名大厅里现场做变性手术吗?
显然,需要有个手段来约束这种行为,约束程序员不要这样不分场合地 “动” 一个对象的内部数据。“成员访问控制” 就是方法之一,并且是主流的面向对象语言中提供这种约束的最常用的方法。
对于类而言,访问控制围绕代码的位置展开,有三类位置:
- 【位置一】当前类的内部,包括类定义内部(类定义的那对{}内部),以及类成员函数内部(类成员函数体的那对{}内部);
- 【位置二】当类的派生类内部;
- 【位置三】类外部,即当前类之外,包括其它类(但不是派生类)或所有类之外的位置,比如自由函数。
对应到三级访问权限控制:
- 加 private 修饰的成员,只能在类内部中直接访问(包括读和写,下同);
- 加 protected 修饰的成员,只能在类内部和派生类内部中直接访问;
- 加 public 修饰的成员,可以类内、类外访问,即位置一、二、三都可直接访问。
struct 的成员,如果不加任何访问控制修饰,就是 public 的。之前我们定义 struct 时,并没有写什么访问控制,但都可以在结构外部直接访问,比如上面 “报名” 函数的例子:在自由函数中直接访问 a 对象的 gender 数据。
C++为成员添加访问控制的具体写法是:在成员前面写上控制级别的关键字,并加一个冒号。则:从冒号往后直到类定义结束,或者遇到另一个访问控制关键字,其间所有成员(包括成员数据或成员函数),都拥有所指定级别的访问权限。
示例:想让 Person 的所有数据,都无法在外部访问(包括读或写),写法是:
struct Person
{
private:int no; // 编号std::string name; // 姓名int gender; // 性别(0女,1男)int age; // 年龄int height; // 身高int weight; // 体重
};
现在,在 Person 之外,我们改不了对象的性别等数据。不过,新的问题产生了:现在我们连查询 Person 对象的性别也做不到了(其它如年龄、身高、体重也一样无法获知)——这显然无法满足举办选美大赛的需求。总不能让评委在问选手 “芳龄几何” 时,得到一个白眼吧?
解决方法是加上相应的查询方法,并且这些方法设定为 public;这样数据是私有的,但方法是公开的。外界可以通过公开的查询方法来获得信息,同时保证“只看不动”,不会修改信息:
struct Person
{
public:int GetNo() { return no; } // 查编号std::string GetName() { return name; } // 查姓名int GetGender() { return gender; } // 查性别... // 这边还有很多 GetXXX()
private:int no;std::string name;int gender;int age;int height;int weight;
};
应用例子:
void 报名()
{Person p; // 在 Person 外调用其 public 成员函数if (p.GetGender() != 0) cout << "你不是女生哦!" << endl;
}
4. 常量成员函数的约束
之前我们学过“常量”。当时说的是数据。当一个数据是常量,那么这个数据必须在定义的时候就确定值,且固定此但值,不让修改。
在类型定义中,我们也可以定义某个成员数据是常量,称其为 “常量成员数据”。特别地,在我们这个例子中,性别字段就很适合定义成 “常量成员数据”。除非大赛想允许选手在参赛期间变性,否则,一个人的生理性别确实应该在胚胎形成阶段就确定下来,并不再改变。
作为“快速感受也探索”之旅的课堂,我们并不在这节课细讲常量成员数据这种更加严厉的约束手段,我们重点讲更常见的一种约束:“常量成员函数”。
下面代码中,GetName() 这个成员函数的实现非常邪恶:
struct Person
{
public:std::string GetName() { gender = 1 - gender; // 邪恶的代码在这行!!!return name; }int GetGender() { return gender; }... // 更多公开方法,略
private:int no; std::string name; int gender; ... // 更多私有数据,略
};
每当我们查询某个选手的姓名,这个选手就会立刻暗地里变性一次!这次男变女,下次女变男……
显然,我们对自己的约束,还是不够……怎么办?只见C++像多啦A梦一样,从肚兜里掏出一条麻绳,不对,是掏出一个语言特性:常量成员函数。
只要在类成员函数的头和体之间,加上 const 修饰,这个成员函数就会变成常量成员函数。效果则是:在它的内部,以及它所调用的当前类(包括从基类继承而得的)的其他函数中,都不允许修改到当前类(包括从基类继承而得的)成员数据。
仍以 GetName() 成员为例:
struct Person
{
public:std::string GetName() const // <-- 常量成员函数{ gender = 1 - gender; // 编译失败!!!return name; }int GetGender() { return gender; }... // 更多公开方法,略
private:int no; std::string name; int gender; ... // 更多私有数据,略
};
现在,如果邪恶的程序员改为在 GetGender() 里偷偷摸摸修改性别,然后在常量版本的 GetName() 里调用 GetGender(),是不是也能犯下 “查询选手姓名时令选手变性” 的罪行呢?放心,不会的。当一个函数是成员函数,那么它能调用的成员函数(包括自身或继承所得),都必须也是常量成员函数。套用到到本例,就是 GetGender() 也必须是常量成员函数。
事实上,正确设计往往就是自然而然的设计:类的所有查询信息的操作,都不应该修改类的内部数据。
女神本来很饿,你过来问一句:“吃了吗?要不要一起晚餐?”,她答:“吃过了,谢谢!”。这种事情在生活中我帮不了你,但是在C++中,可以通过常量成员函数来避免。
5. 从struct到class,越成长,越懂得自我约束
5.1 语法差别
从功能上讲,struct 和 class 完全可以互相替代 。在语法作用上,它们的差别也很少,主要两点:
- struct中成员的默认访问控制是 public,而 class 则默认是 private;
- struct作为派生类时,默认的派生方式是公开派生,而class默认是私有派生。
第二点我们在后面的课堂会再谈到。现在就看第一点,请对比:
5.2 何时 struct 何时 class?
重点在语义区别,一个类型,什么时候适合用 struct 表达,什么时候适合用 class 表达呢?
这个问题没有标准答案,但是有一些流行的约定。我们来介绍两种。
- 方法一:数据有联动修改关系,用 class,否则用 struct
什么叫联动关系? 就是一个成员数据发生变化后,如果可能引起另外一个成员数据也发生变化,就叫作二者间存在联动修改关系。
联动修改关系在判断上又分为宽松版和严格版。在对象初始化过程中,字段间所存在互相影响的关系如果算联动,就叫严格版,否则为宽松版。
听起来有点不太好理解?来看实例:还是 Person(人类)的定义,我们把它简化到只包含姓名和性别。父母在为之取名时,肯定会考虑宝宝的性别因素——如果认为这种对象初始化过程的互相影响也算是数据间存在联动修改关系,这种判断方法叫严格版,否则叫宽松版。
实际工作中我喜欢宽松版。不过,今天我们的重点是另一种方法,它显得又简单又粗暴:
- 方法二:用到非公开(public)成员,就用 class 定义。
也就是说,如果你所定义的类型中,存在不想被公开访问的成员数据或函数,那么,用这个方法,你就将得到一个class而不是一个struct。
6. 课堂视频
视频和文字教程,一定结合学习,效果好太多。
ff17-HelloObject-封装版-上
相关文章:

C++感受14-Hello Object 封装版 - 上
1. 封装即约束——封装和派生、多态的本质区别 一门计算机语言,要如何帮助程序员写出优秀的代码?两个方法:一是给程序员更多能力,二是给程序员更多约束。之前我们学习的派生和多态,更多的是给我们技能,而封…...

网络安全中大数据和人工智能应用实践
传统的网络安全防护手段主要是通过单点的网络安全设备,随着网络攻击的方式和手段不断的变化,大数据和人工智能技术也在最近十年飞速地发展,网络安全防护也逐渐开始拥抱大数据和人工智能。传统的安全设备和防护手段容易形成数据孤岛࿰…...

RISC-V架构下OP-TEE 安全系统实践
安全之安全(security)博客目录导读 本篇博客,我们聚焦RISC-V 2024中国峰会上的RISC-V和OP-TEE结合的一个安全系统实践,来自芯来科技桂兵老师。 关于RISC-V TEE(可信执行环境)的相关方案,如感兴趣可参考R...

40分钟学 Go 语言高并发:【实战】分布式缓存系统
【实战课程】分布式缓存系统 一、整体架构设计 首先,让我们通过架构图了解分布式缓存系统的整体设计: 核心组件 组件名称功能描述技术选型负载均衡层请求分发、节点选择一致性哈希缓存节点数据存储、过期处理内存存储 持久化同步机制节点间数据同步…...

[创业之路-186]:《华为战略管理法-DSTE实战体系》-1-为什么UTStarcom死了,华为却活了,而且越活越好?
目录 前言 一、市场定位与战略选择 二、技术创新能力 三、企业文化与团队建设 四、应对危机的能力 五、客户为中心的理念 六、市场适应性与战略灵活性 七、技术创新与研发投入 八、企业文化与团队建设 九、应对危机的能力 前言 UT斯达康(UTStarcom&#…...

python如何多行注释
在Python中,多行注释通常有两种方式: 使用三个单引号()或三个双引号(""")来创建多行字符串,这可以被用来作为多行注释。这种方式在Python中实际上是创建了一个多行的字符串对象…...

前端工程化面试题目常见
前端工程化面试常见题目包括: • 谈谈你对WebPack的认识。 • Webpack打包的流程是什么? • 说说你工作中几个常用的loader。 • 说说HtmlWebpackPlugin插件的作用。 • Webpack支持的脚本模块规范有哪些? • Webpack和gulp/grunt相比有什么特…...

定点数的乘除运算
原码一位乘法 乘积的符号由两个数的符号位异或而成。(不参与运算)被乘数和乘数均取绝对值参与运算,看作无符号数。乘数的最低位为Yn: 若Yn1,则部分积加上被乘数|x|,然后逻辑右移一位;若Yn0&…...

页面置换算法模拟 最近最久未使用(LRU)算法
最近最久未使用(LRU)算法是一种基于页面访问历史的页面置换算法。它选择最久未使用的页面进行置换。当需要访问一个不在内存中的页面时,如果内存已满,则选择最久未使用的页面进行置换。LRU算法通过记录页面的访问时间戳来判断页面…...

Ubuntu与Centos系统有何区别?
Ubuntu和CentOS都是基于Linux内核的操作系统,但它们在设计理念、使用场景和技术实现上有显著的区别。以下是详细的对比: 1. 基础和发行版本 Ubuntu: 基于Debian,使用.deb包管理系统。包含两个主要版本: LTSÿ…...

RK3568平台开发系列讲解(pinctrl 子系统篇)pinctrl_debug
🚀返回专栏总目录 文章目录 1. Overview2. debug信息2.1 pinctrl-devices2.2. pinctrl-handles2.3. pinctrl-handles3. debug信息3.1. 查看(pinctrl_register_pins)注册了哪些pins3.2. 查看pin groups;3.3. 查看每种functions所占用的gpio groups信息:3.4. pinconf沉淀、…...

避大坑!Vue3中reactive丢失响应式的问题
在vue3中,我们定义响应式数据无非是ref和reactive。 但是有的小伙伴会踩雷!导致定义的响应式丢失的问题。 reactive丢失响应式的情况1(直接赋值) 场景: 1.你定义了一个数据:let datareactive({name:"",age:"" }) 2.然后你…...

springSecurity权限控制
权限控制:不同的用户可以使用不同的功能。 我们不能在前端判断用户权限来控制显示哪些按钮,因为这样,有人会获取该功能对应的接口,就不需要通过前端,直接发送请求实现功能了。所以需要在后端进行权限判断。࿰…...

Pytorch训练固定随机种子(单卡场景和分布式训练场景)
模型的训练是一个随机过程,固定随机种子可以帮助我们复现实验结果。 接下来介绍一个模型训练过程中固定随机种子的代码,并对每条语句的作用都会进行解释。 def seed_reproducer(seed2333):random.seed(seed)os.environ["PYTHONHASHSEED"] s…...

Conda + JuiceFS :增强 AI 开发环境共享能力
Conda 是当前 AI 应用开发领域中非常流行的环境和包管理系统,因其能够简单便捷地创建与系统资源相隔离的虚拟环境广受欢迎。 Conda 支持在不同的操作系统上重建相同的工作环境,但在环境共享复用方面仍存在一些挑战。比如,在不同机器上复用相…...

人工智能-人机交互的机会
目录 引言HCI领域的发展机会人工智能领域的崛起与机会博雅智信的HCI与AI辅导服务结语 引言 在人类科技不断进步的今天,HCI(人机交互)和人工智能(AI)是两个密切相关且充满潜力的领域。HCI研究如何优化人类与计算机之间…...

【系统架构核心服务设计】使用 Redis ZSET 实现排行榜服务
目录 一、排行榜的应用场景 二、排行榜技术的特点 三、使用Redis ZSET实现排行榜 3.1 引入依赖 3.2 配置Redis连接 3.3 创建实体类(可选) 3.4 编写 Redis 操作服务层 3.5 编写控制器层 3.6 测试 3.6.1 测试 addMovieScore 接口 3.6.2 测试 g…...

elasticsearch基础总结
最近实习,项目用的elasticseatch做的存储库,但是之前对于es接触的不多,查询语法有些不熟,每次想写个DSL查询时都要gpt或者施展搜索大法,所以索性就自己总结总结,以后忘了也方便查。所以这篇文章会持续更新。…...

【慕伏白教程】Zerotier 连接与简单配置
文章目录 下载与安装WindowsLinuxapt安装官方脚本安装 Zerotier 配置新建网络网络配置 终端配置WindowsLinux 下载与安装 Windows 进入Zerotier官方下载网站,点击下载 在下载目录找到安装文件,双击打开后点击 Install 开始安装 安装完成后,…...

Brain.js(九):LSTMTimeStep 实战教程 - 未来短期内的股市指数预测 - 实操要谨慎
系列的前一文RNNTimeStep 实战教程 - 股票价格预测 讲述了如何使用RNN时间序列预测实时的股价, 在这一节中,我们将深入学习如何利用 JavaScript 在浏览器环境下使用 LSTMTimeStep 进行股市指数的短期预测。通过本次实战教程,你将了解到如何用…...

C# 字符串(String)
文章目录 前言创建 String 对象的方式1. 通过给 String 变量指定一个字符串2. 通过使用 String 类构造函数3. 通过使用字符串串联运算符( )4. 通过检索属性或调用一个返回字符串的方法5. 通过格式化方法来转换一个值或对象为它的字符串表示形式 String …...

二进制文件
大多数人听到“二进制”的时候,脑海里可能马上就会联想到电影《黑客帝国》中由“0”和“1”组成的矩阵。 笔者不打算在这里详细讨论二进制的运算、反码、补码之类枯燥的东西,但有几个和开发相关的概念需要做一点澄清和普及。因为这些内容就像空气——用…...

【电子元器件】音频功放种类
本文章是笔者整理的备忘笔记。希望在帮助自己温习避免遗忘的同时,也能帮助其他需要参考的朋友。如有谬误,欢迎大家进行指正。 一、概述 音频功放将小信号的幅值提高至有用电平,同时保留小信号的细节,这称为线性度。放大器的线性…...

linux之vim
一、模式转换命令 vim主要有三种模式:命令模式(Normal Mode)、输入模式(Insert Mode)和底线命令模式(Command-Line Mode)。 从命令模式切换到输入模式:i:在当前光标所在…...

QT的ui界面显示不全问题(适应高分辨率屏幕)
//自动适应高分辨率 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);一、问题 电脑分辨率高,默认情况下,打开QT的ui界面,显示不全按钮内容 二、解决方案 如果自己的电脑分辨率较高,可以尝试以下方案:自…...

数据结构--串、数组和广义表
串 定义:串(String)是由零个或多个字符组成的有限序列。 子串:串中任意个连续字符组成的子序列称为该串的子串。 主串:包含子串的串相应地称为主串。 字符位置:字符在该序列中的序号为该字符在串中的位置…...

LLMs之Agent之Lares:Lares的简介、安装和使用方法、案例应用之详细攻略
LLMs之Agent之Lares:Lares的简介、安装和使用方法、案例应用之详细攻略 导读:这篇博文介绍了 Lares,一个由简单的 AI 代理驱动的智能家居助手模拟器,它展现出令人惊讶的解决问题能力。 >> 背景痛点:每天都有新的…...

1-1.mysql2 之 mysql2 初识(mysql2 初识案例、初识案例挖掘)
一、mysql2 概述 mysql2 是一个用于 Node.js 的 MySQL 客户端库 mysql2 是 mysql 库的一个改进版本,提供了更好的性能和更多的功能 使用 mysql2 之前,需要先安装它 npm install mysql2 二、mysql2 初识案例 1、数据库准备 创建数据库 testdb CREAT…...

企业邮箱为什么不能经常群发邮件?
企业邮箱是用企业域名作为后缀的邮箱,虽然企业邮箱确实具备群发邮件的功能,但它更适用于企业内部的群发,而非用于外部推广。如果是在企业邮件域内进行群发,通常可以借助企业邮箱的邮件列表来实现。然而,对于域外的大量…...

集成运算放大电路反馈判断
集成运算放大电路 一种具有很高放大倍数的多级直接耦合放大电路,因最初用于信号运算而得名,简称集成运放或运放 模拟集成电路中的典型组件,是发展最快、品种最多、应用最广的一种 反馈 将放大电路输出信号的一部分或全部通过某种电路引回到输…...