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

跟我学C++中级篇——封装对象的实践

一、对象封装

在面向对象编程中,首要的事情就是如何进行对象的封装。说的直白一些,就是如何设计类或者是结构体。许多开发者看过不少的书,也学过很多的设计方法,更看过很多别人的代码。那么如何指导自己进行对象的封装呢?
在书籍中学过设计原则如才分析过的六大原则,也从书本上知道了继承是面向对象设计的一个重要方式。还有前面提到的内联以及学习过的内存对齐、依赖注入(注意不是依赖倒置)等等,那么如何才能在对象封装的过程中灵活的运用这些知识呢?这就需要一个封装设计从整体到细节,从粗芜到精细的过程。

二、封装的过程

在对象的封装过程中,可以划分成三个层次:
1、宏观设计
需要考虑这个类的功能,它如何与其它功能类或者管理类进行交互。这里就需要考虑一些设计原则如单一职责、开闭原则、迪米特法则等 。正如前面分析的,首先从宏观要保证这个类或者结构体的功能要尽量自耦合,尽量只提供对外的输出接口(这个当然是理想的),尽量减少对内的输入接口,少继承多组合等等。
那么有人会问,这个体现在哪儿,只是一种大脑的风暴?其实这就是在前文(“架构设计杂谈”)中提到的从思想到规则产出。这就需要一些工具,如一些UML的设计工具,一些流程图的工具等等。开发者可以在这些设计的具现化过程中,把自己的思想逐步形成一个设计的实体(形成规则)。

2、局部设计
局部设计其实就是进一步的细化,在类初步被设计出来已后,如何设计对外交互接口和头文件暴露的程度就提到了日程来。一般来说,一个封装的对象(类或结构)中,函数分成两大类,一种是自用的,不对外暴露;另外一种是对外的,也就是常说的对外接口。对内的函数可以随时调整(当然这种情况还是要尽量避免),因为它只能影响自己,但对外接口一般相对是比较稳定的。所以这就要求在设计时需要考虑对外接口的控制。
接口的控制一般考虑是尽量要齐全,比如看一些库或框架时,可以看到一些从0~N(N上了十的量级)的参数。这在早期的C++程序中是不可避免的,即使到了C++11以后,仍然在模板编程中可以看到类似的一些机制。另外一个就是是否跨平台上,需要考虑ABI的兼容性设计。最后还要考虑的是,不要随便的引入其它的依赖,这个非常重要,也非常典型。举一个例子,在开发之初可能需要某个三方的库,但后来不用了,却忘记删除相关的头文件,注意是头文件,因为别的文件很容易想到就剔除了,这其实就引入了一些莫名的风险。包括可以使用依赖注入来实现一些反转控制等等。
另外一个是头文件的暴露, 这里有一个原则就是尽量不要暴露变量,只暴露接口函数。其实就是出于简单原则和安全原则了。它的实现方法也很多,如简单的抽象出一个虚接口类(但这可能就ABI无法兼容了)、Pimpl、设计一个专门的接口类等等。
还有头文件和 cpp文件的是否分享的问题,这一般就涉及到是否为模板、是否有大量的内联函数、内部类、是否普遍公用的头文件等等。模板一般是建议都写在头文件中,毕竟大多数的编译器都不支持模板的分离编译。有大量的内联函数的类一般也建议写到头文件中。其它情况自己可以根据情况斟酌。

3、细节设计
其实上面的两层设计,可能大多数开发者还是觉得没有什么实际意义,毕竟开发的程序有几个是大规模的应用,很多都是自己搞来搞去的。但下面的细节设计,就非常重要了。
a) 变量设计
变量的设计从访问限制上分为三类,public(公有),protected(保护),private(私有)。一般来说不涉及到继承的只考虑公有和私有两类。从主流的设计思想出发,变量一般是不建议公有的即变量一律私有化,通过接口和控制变量的访问,只有某些静态成员变量可能需要公有。
从变量的类型来说有静态、静态局部和普通变量,这个一般来说比较好区别,使用静态一般都有一些特殊的要求,比如直接暴露变量并且唯一一个,常见的就是单例。
从CV限定上来看,变量如果是一个恒定值,或者只读访问,那么使用const限制;如果是和硬件通信或者多线程通信中使用一些易变的变量 ,需要增加volatile限定。不过这里需要注意的是,有些平台对volatile有一些不同的设定。比如在WINDOWS和Linux平台上,对其的使用限定就不完全相同。但是这一个一般在嵌入式中可能更有实用性,在上层应用上,就属于小众应用了。
变量的初始化,C++11以前,少量(两个以内 )的可以使用构造(初始化)函数内赋值,但一般是建议使用列表列表初始化;在C++11以后,增加了默认赋值,即在定义成员变量时就赋值。可以综合运用。另外还有下面提到的委托构造和继承构造。
在变量设计中还有一个比较少见但比较重要的应用就是mutable,它可以和 const函数共同使用来达到一些特殊的修改动作,要谨慎使用。
inline是C++17后才提供的,可以根据其实功能来和实际场景匹配。如果需要进行指针控制,建议使用智能指针。

b) 构造和析构函数
构造函数和析构函数原则上来说尽量少干活,特别是涉及到内存处理的工作。如果有较多的初始化的动作(包括内存分配)或者资源回收动作,建议是做一个初始化的接口和一个回收接口来进行,虽然这样看起来有些丑陋并且增加了一些工作量。之所以不建议在构造和析构函数中做较多的工作,原因就在于二者对异常的处理比较难于控制。
另外,就不得不提到委托构造和继承构造了,这都可以大大减少代码的编写量并使整个工程代码显得整洁得体。
还有一个问题就是,构造函数是不会有是否virtual的问题,但一般析构函数都会有这个虚拟的问题,换句话说,析构函数是否需要增加virtual。这个判断的很简单,如果没有继承或者继承没有虚函数,就不要用。
如果不允许类的隐式转换,可以增加explict关键字,防止编译器进行隐式类型转化。
c) 异常的处理
这包括刚刚提到的构造函数和析构函数。大家有兴趣可以查找资料来看一看这两个函数是如何控制异常的,还是比较麻烦的,不如单独拉出来处理。
d)拷贝构造和移动构造等函数何时需要编写
C++11以前是默认的四个函数,C++11后是六个,增加了移动拷贝和移动赋值。一般来说,如需要实现非构造和析构函数中的其中任何一个,就建议全写,如果可以认定不需要,就把其设置为=delete。
特别是在赋值构造函数需要处理一下自身的赋值(没啥实际意义)。

d)普通函数的限定
包括static,const,noexcept等如此这些限定,一般来说,都比较好理解。一般不允许修改的都加const,需要回传值的加引用,如果既不允许修改,变量又比较大,为了减少内存复制可以引用加const,这里重点提一个在C++11中新增的引用限定符,即类似于:

class A
{int get0() &{return a;}
int get1()&&{return a;}
int get2()const &&{return a;}};

它可以限定左右值的调用,但需要注意的是,它只能在成员函数中使用而不能应用在static函数上。
还有最常见的inline函数,一般建议是小函数,功能简单的写成inline函数。不过话说回来,这个是向编译器建议的,不是强迫的,如果设计上对性能不是要求多高可以忽略它。
e) 封装模板类
模板类比较麻烦的在一般它只在头文件里,这里需要注意的有两点,一个是模板的延迟加载,即没有显示的调用,模板不会自动生成实例;另外一个是模板的难于调试性。只要注意到这两点,一般的开发都可以比较好的适用了。
其实重点还是在于,模板的代码不容易理解,如果实际的场景中大家都不太会用模板,还是建议将其耦合到自身,不要对外暴露,或者干脆不使用模板。

最后,如果确定类不再继承,可以使用final关键字来控制类的继承。这些都是一些比较常见的封装设计的实践经验,不一定普适,但可以根据实际情况综合应用到一起,重点在如何使用它们,这才是本文的目的。
从局部开始,其实就可以在IDE或者相关工具中进行类的设计了,当然继续在UML等工具进行设计也没有问题,毕竟很多UML工具可以直接导出为类的代码。这个就看每个开发者的习惯和想法了,不必强求。

三、结构体封装的特别说明

在C++中类和结构体基本的封装方式是一致的,但由于细节的不同,还是有些不同的:
1、结构的成员均为公有
所以上面的设计中关于变量的限制,接口的限定等就没有了实际的意义

2、内存对齐和定义顺序
写过网络和硬件开发的开发者都知道,二进制流传输是有字节对齐一说的。所以在设计一些对数据量敏感或者说有要求的结构体(POD类型的类)时,需要对字节进行对齐。它有两种方式,一种是内部调整,一种是使用编译命令处理。在C/C++面试时经常遇到一些类似下面的面试题:

struct A {int d;double d1;char c;int i;double d2;bool b;
};

然后sizeof(A)的大小。然后再把一些变量的位置换一下顺序,再求一下大小。就会发现可能大小不一样。这其实就是内存对齐的原因。所以在一些对空间要求严格或者访问严格限定的情况下,就需要处理一下变量的顺序。或者在某些情况下可以干脆使用一些预编译命令,强制其内存对齐到1,比如在串口传输中,就可以使用“#pragma pack(1)”,当然不同的平台和不同的语言都不同的对齐指令或者方法,C++新标准中也提供了std::align,根据实际情况使用即可。不过,强制内存对齐一般会造成性能的损失,这个需要考虑应用场景。同时,编译器对小对象的优化也需要考虑,一般来说对于某些情况下需要限制一下对128位长度的访问变量,因为它的速度会更快一些。
另外字节对齐的原因和好处,这里就不再赘述,有兴趣可以自己查看一些资料。
做为另外两种封装枚举体和联合体,一般用得比较多的前者,但前者除了一些C++11前后的类型限定外,只增加了一些类型处理的接口,没有什么可谈的。而联合体应用的就更少了,用到后自己斟酌考虑就可以了。

四、总结

面向对象编程是一种非常广泛的编译方式,很多开发者可能对它是既了解又不了解。对一些基础的知识会用,但又不知道是否用得合适,能不能有一个标准来判定。其实这恰恰表明对面向对象编程还是掌握的不够深入。一切设计没有标准只有原则,这也意味着,实际场景下,在考虑原则的同时,更要考虑实际的需求进行适当的取舍。
最好的设计方法是没有的,只有最合适的设计方法。

相关文章:

跟我学C++中级篇——封装对象的实践

一、对象封装 在面向对象编程中,首要的事情就是如何进行对象的封装。说的直白一些,就是如何设计类或者是结构体。许多开发者看过不少的书,也学过很多的设计方法,更看过很多别人的代码。那么如何指导自己进行对象的封装呢&#xf…...

iOS面试题链接汇总

iOS开发三年经验 靠这份面试题让我从15k到25k - 简书 2021年,整理的iOS高频面试题及答案(总会有你需要的) - 知乎 iOS面试(内含面试全流程,面试准备工作面试题等)-CSDN博客 runtime: 阿里、字节 一套高效…...

TEINet: Towards an Efficient Architecture for Video Recognition 论文阅读

TEINet: Towards an Efficient Architecture for Video Recognition 论文阅读 Abstract1 Introduction2 Related Work3 Method3.1 Motion Enhanced Module3.2 Temporal Interaction Module3.3 TEINet 4 Experiments5 Conclusion阅读总结 文章信息; 原文链接:https:…...

Navicat Data Modeler Ess for Mac:强大的数据库建模设计软件

Navicat Data Modeler Ess for Mac是一款专为Mac用户设计的数据库建模与设计工具,凭借其强大的功能和直观的界面,帮助用户轻松构建和管理复杂的数据库模型。 Navicat Data Modeler Ess for Mac v3.3.17中文直装版下载 这款软件支持多种数据库系统&#x…...

NSS刷题

[SWPUCTF 2021 新生赛]jicao 类型&#xff1a;PHP、代码审计、RCE 主要知识点&#xff1a;json_decode()函数 json_decode()&#xff1a;对JSON字符串解码&#xff0c;转换为php变量 用法&#xff1a; <?php $json {"ctf":"web","question"…...

CUDA专项

1、讲讲shared memory bank conflict的发生场景&#xff1f;以及你能想到哪些解决方案&#xff1f; CUDA中的共享内存&#xff08;Shared Memory&#xff09;是GPU上的一种快速内存&#xff0c;通常用于在CUDA线程&#xff08;Thread&#xff09;之间共享数据。然而&#xff0…...

C# 判断Access数据库中表是否存在,表中某个字段是否存在

在C#中判断Access数据库中某个表是否存在以及该表中某个字段是否存在&#xff0c;可以通过以下步骤实现&#xff1a; 判断表是否存在 可以使用ADO.NET中的OleDbConnection.GetOleDbSchemaTable方法来获取数据库的架构信息&#xff0c;并检查特定的表是否存在。 using System…...

【C++】学习笔记——模板进阶

文章目录 十一、模板进阶1. 非类型模板参数2. 按需实例化3. 模板的特化类模板的特化 4. 模板的分离编译 未完待续 十一、模板进阶 1. 非类型模板参数 模板参数分为类型形参和非类型形参 。类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的…...

JAVA系列 小白入门参考资料 接口

目录 接口 接口的概念 语法 接口使用 接口实现用例 接口特性 实现多个接口和实现用例 接口间的继承 接口 接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本上的 USB 口&#xff0c;电源插座等。 电脑的 USB 口上&am…...

日报表定时任务优化历程

报表需求背景 报表是一个很常见的需求&#xff0c;在项目中后期往往会需要加多种维度的一些统计信息&#xff0c;今天就来谈谈上线近10个月后的一次报表优化优化之路&#xff08;从一天报表跑需要五分钟&#xff0c;优化至秒级&#xff09; 需求&#xff1a;对代理商进行日统计…...

excel表格里,可以把百分号放在数字前面吗?

在有些版本里是可以的&#xff0c;这样做&#xff1a; 选中数据&#xff0c;鼠标右键&#xff0c;点击设置单元格格式&#xff0c;切换到自定义&#xff0c;在右侧栏输入%0&#xff0c;点击确定就可以了。 这样设置的好处是&#xff0c;它仍旧是数值&#xff0c;并且数值大小没…...

应用案例 | 商业电气承包商借助Softing NetXpert XG2节省网络验证时间

一家提供全方位服务的电气承包商通过使用Softing NetXpert XG2顺利完成了此次工作任务——简化了故障排查的同时&#xff0c;还在很大程度上减少了不必要的售后回访。 对已经安装好的光纤或铜缆以太网网络进行认证测试可能会面临不同的挑战&#xff0c;这具体取决于网络的规模、…...

【JAVA语言-第20话】多线程详细解析(二)——线程安全,非线程安全的集合转换成线程安全

目录 线程安全 1.1 概述 1.2 案例分析 1.3 解决线程安全问题 1.3.1 使用synchronized关键字 1.3.1.1 同步代码块 1.3.1.2 同步方法 1.3.2 使用Lock锁 1.3.2.1 概述 代码示例&#xff1a; 1.4 线程安全的类 1.4.1 非线程安全集合转换成线程安全集合 1.5 总结 …...

区块链中的加密算法及其作用

区块链技术以其去中心化、不可篡改、透明公开的特性&#xff0c;在全球范围内引发了广泛的关注和讨论。其中&#xff0c;加密算法作为区块链技术的核心组成部分&#xff0c;对于维护区块链网络的安全、确保数据的完整性和真实性起到了至关重要的作用。本文将详细介绍区块链中常…...

微信小程序跳转微信管理平台配置的客服及意见页面

<button open-type"contact" bindcontact"handleContact" session-from"sessionFrom">帮助与客服</button> 不需要路径 在当前小程序中会自动进入 open-type"contact" 其他参数不用修改 只修改这个参数对应表单组件 /…...

灌溉机器人 状压dp

灌溉机器人 题目描述 农田灌溉是一项十分费体力的农活&#xff0c;特别是大型的农田。小明想为农民伯伯们减轻农作负担&#xff0c;最近在研究一款高科技——灌溉机器人。它可以在远程电脑控制下&#xff0c;给农田里的作物进行灌溉。 现在有一片 N 行 M 列的农田。农田的土…...

用于接收参数的几个注解

了解四种主要请求方法的传参格式 GET方法&#xff1a; 参数通常通过URL的查询字符串&#xff08;query string&#xff09;传递&#xff0c;形式为key1value1&key2value2。示例&#xff1a;http://example.com/api/resource?key1value1&key2value2 POST方法&#xf…...

Flask-Login 实现用户认证

Flask-Login 实现用户认证 Flask-Login 是什么 Flask-Login 是 Flask 中的一个第三方库&#xff0c;用于处理用户认证和管理用户会话&#xff0c;它提供了一组工具和功能&#xff0c;使得在 Flask 应用程序中实现用户认证变得更加简单和方便。 如何使用 Flask-Login 1.安装…...

基于WPF的DynamicDataDisplay曲线显示

一、DynamicDataDisplay下载和引用 1.新建项目&#xff0c;下载DynamicDataDisplay引用&#xff1a; 如下图&#xff1a; 二、前端开发&#xff1a; <Border Grid.Row"0" Grid.Column"2" BorderBrush"Purple" BorderThickness"1"…...

股票问题(至多两次购买

class Solution {public int maxProfit(int[] prices) {int[] dpnew int[4];dp[0]-prices[0];//第一次持有dp[1]0;dp[2]-prices[0];//第二次持有dp[3]0;for(int i1;i<prices.length;i){dp[0]Math.max(dp[0],-prices[i]);dp[1]Math.max(dp[1],dp[0]prices[i]);dp[2]Math.max(…...

DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径

目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

OpenLayers 分屏对比(地图联动)

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能&#xff0c;和卷帘图层不一样的是&#xff0c;分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

GruntJS-前端自动化任务运行器从入门到实战

Grunt 完全指南&#xff1a;从入门到实战 一、Grunt 是什么&#xff1f; Grunt是一个基于 Node.js 的前端自动化任务运行器&#xff0c;主要用于自动化执行项目开发中重复性高的任务&#xff0c;例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

Linux nano命令的基本使用

参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时&#xff0c;显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准

城市路内停车管理常因行道树遮挡、高位设备盲区等问题&#xff0c;导致车牌识别率低、逃费率高&#xff0c;传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法&#xff0c;正成为破局关键。该设备安装于车位侧方0.5-0.7米高度&#xff0c;直接规避树枝遮…...