跟我学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前后的类型限定外,只增加了一些类型处理的接口,没有什么可谈的。而联合体应用的就更少了,用到后自己斟酌考虑就可以了。

四、总结

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

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

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

相关文章

数学学习笔记1——二次函数中的数形结合

二次函数中的数形结合 一、解一元二次不等式 基本方法&#xff1a;配方。 x 2 − 4 x 3 < 0 → ( x − 2 ) 2 < 1 → ∣ x − 2 ∣ < 1 → 1 < x < 3 x^2-4x3<0\to(x-2)^2<1\to\lvert x-2\rvert<1\to1<x<3 x2−4x3<0→(x−2)2<1→∣x−…

VBA_MF系列技术资料1-605

MF系列VBA技术资料1-605 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-0…

具备教学意义的实操(用队列实现栈)

225. 用队列实现栈 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/implement-stack-using-queues/description/ 实现逻辑 一个是先进先出&#xff08;队列&#xff09;&#xff0c;一个是后进先出&#xff08;栈&#xff09; 这里用两个队列导入一下数据…

JS实现递归功能

// 递归函数示例&#xff1a;计算阶乘 function factorial(n) {if (n 0) {return 1;} else {return n * factorial(n - 1);} }// 调用递归函数计算阶乘 const result factorial(5); console.log(result); // 输出 120 在上面的示例中&#xff0c;我们定义了一个递归函数fact…

项目管理 | 如何做好项目管理?

大部分人在做项目管理时会遇到以下问题&#xff1a; 团队沟通不畅&#xff0c;对于项目的各个环节和配合方无法掌控项目的任务分配和跟踪困难&#xff0c;项目进度不透明项目上线进度慢&#xff0c;没有威信难以服众 那项目管理怎么做&#xff1f;这篇就结合简道云团队的经验…

爬虫学习:XPath提取网页数据

目录 一、安装XPath 二、XPath的基础语法 1.选取节点 三、使用XPath匹配数据 1.浏览器审查元素 2.具体实例 四、总结 一、安装XPath 控制台输入指令&#xff1a;pip install lxml 二、XPath的基础语法 XPath是一种在XML文档中查找信息的语言&#xff0c;可以使用它在HTM…

MySQL 数据库中 Insert 语句的锁机制

在数据库系统中&#xff0c;Insert 语句是常用的操作之一&#xff0c;用于向数据库表中插入新的数据记录。然而&#xff0c;当多个会话&#xff08;或者线程&#xff09;同时对同一张表执行 Insert 操作时&#xff0c;可能会引发一些并发控制的问题&#xff0c;特别是涉及到锁的…

数据结构----二叉树

博主主页: 码农派大星. 关注博主带你了解更多数据结构知识 1. 树型结构 1.1 概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上…

【软考】模拟考卷错题本2024-05-11

1 设计模式- 适配器模式 基本上上述的图解已经涵盖了绝大多数主流的设计模式和其特点。理解记忆下即可&#xff0c;这里对下午的考题也有帮助的。 2 计算机组成原理 cpu 访问速度 这个真的是憨憨咯~看到内存就选内存&#xff0c;题目都没审好。这里的速度比cpu内部的要比外部的…

c++ STL 之栈—— stack 详解

vector 是 stl 的一个关联容器,名叫“栈”&#xff0c;何为“栈”&#xff1f;其实就是一个数组&#xff0c;但有了数组何必还需栈&#xff0c;这是一个高深的问题。 一、简介 1. 定义 栈&#xff0c;是一个柔性数组&#xff08;可变长数组&#xff09;&#xff0c;可以变大变小…

巩固学习5

ls.sort(keylambda x:x[1], reverseTrue) ls.sort(): 这是 Python 中用于对列表进行排序的方法之一。与 sorted() 函数不同&#xff0c;sort() 方法会直接修改原始列表&#xff0c;而 sorted() 函数则返回一个新的已排序列表。 keylambda x: x[1]: 这部分是一个关键参数&#x…

ASE docker related research

ASE 2022 Understanding and Predicting Docker Build Duration: An Empirical Study of Containerized Workflow of OSS Projects 理解和预测 Docker 构建持续时间&#xff1a;OSS 项目容器化工作流程的实证研究 Docker 构建是容器化工作流程的关键组成部分&#xff0c;它…

Centos7安装图形化界面

前言&#xff1a;原文在我的博客网站中&#xff0c;持续更新数通、系统方面的知识&#xff0c;欢迎来访&#xff01; Centos7安装图形化界面https://myweb.myskillstree.cn/43.html 目录 一、安装GNOME桌面 二、开机自启动修改为命令行模式 三、卸载图形化界面 一、安装GN…

Oracle 误操作insert delete update 数据回滚

查询回滚数据 select * from tablename AS OF TIMESTAMP TO_TIMESTAMP(2023-12-29 10:29:00,yyyy-mm-dd hh24:mi:ss) where not exists (select 1 from tablename A where A.xh tablename.xh and A.TIME tablename.TIME); TO_TIMESTAMP(2023-12-29 10:29:00,yyyy-mm-dd h…

【C++】string类的使用③(修改器Modifiers || 非成员函数重载Non-member function overloads)

&#x1f525;个人主页&#xff1a; Forcible Bug Maker &#x1f525;专栏&#xff1a; STL || C 目录 前言&#x1f525;修改器&#xff08;Modifiers&#xff09;**operator**appendpush_back和pop_backassigninserterasereplaceswap &#x1f525;非成员函数重载&#xff…

Java入门基础学习笔记4——开发Helloworld入门程序

Java程序开发的三个步骤&#xff1a; 1&#xff09;编写代码 2&#xff09;编译代码 3&#xff09;运行代码 注意事项&#xff1a; 第一个java程序建议使用记事本来编写。 建议代码文件名全英文、首字母大写、满足驼峰模式&#xff0c;源代码文件的后缀必须是.java 注意&a…

栈实现队列

一、分析 栈的特点是先出再入&#xff0c;而队列的特点为先入先出&#xff0c;所以我们创造两个栈&#xff0c;一个用来存放数据&#xff0c;一个用来实现其它功能此时栈顶为队尾&#xff1b;当要找队头数据时将前n-1个数据移入到另一个栈中&#xff0c;此时剩余那个数据为队头…

leetcode 1749.任意子数组和的绝对值的最大值

思路&#xff1a;dp 说到绝对值&#xff0c;大家肯定不陌生&#xff0c;但是用在dp上就会使问题变得稍微复杂一些了。 我们在最大子数组和的那道题中知道&#xff0c;在状态转移的时候&#xff0c;我们会舍弃掉为负数的连续部分&#xff0c;重新构建连续的子串。但是&#xf…

Sqlite在Mybatis Plus中关于时间字段的处理

我的个人项目中&#xff0c;使用Mybatis-Plus 和 Sqlite数据库&#xff0c; 但是在存储和查询时间字段的时候&#xff0c;总是出现问题&#xff0c;记录下我解决问题的过程。 Sqlite会默认把时间字段转成时间戳存储到数据库的字段中&#xff0c;看起来不直观&#xff0c;所以我…

k8s部署数据库等pass产品的时候用那种控制器

在Kubernetes中部署数据库等持久化存储&#xff08;Persistent Storage&#xff0c;简称Pass&#xff09;产品时&#xff0c;通常会使用以下几种控制器&#xff1a; StatefulSet&#xff1a;这是部署有状态应用的首选控制器&#xff0c;特别是当应用需要稳定的身份标识&#xf…