c++ class struct同名_C/C++面向对象编程之封装

8bcf6bd7dbd60c67f3b9f4ff3dc8abad.png点击“蓝字”关注我们吧前言:

何为面向过程:

面向过程,本质是“顺序,循环,分支”

面向过程开发,就像是总有人问你要后续的计划一样,下一步做什么,再下一步做什么,意外、事物中断、突发事件怎么做。理论上来说,任何一个过程都可以通过“顺序,循环,分支”来描述出来,但是实际上,很多项目的复杂度,都不是“顺序循环分支”几句话能说清楚的。稍微大一点的项目,多线程,几十件事情并发, 如果用这种最简单的描述方式,要么几乎无法使用,缺失细节太多,要么事无巨细,用最简单的描述,都会让后期复杂度提升到一个爆炸的状态。

何为面向对象:

面向对象,本质是“继承,封装,多态”

面向对象的核心是把数据和处理数据的方法封装在一起。面向对象可以简单的理解为将一切事物模块化 ,面向对象的代码结构,有效做到了层层分级、层层封装,每一层只理解需要对接的部分,其他被封装的细节不去考虑,有效控制了小范围内信息量的爆炸。然而当项目的复杂度超过一定程度的时候,模块间对接的代价远远高于实体业务干活的代价, 因为面向对象概念的层级划分,要实现的业务需要封装,封装好跟父类对接。多继承是万恶之源,让整个系统结构变成了网状、环状,最后变成一坨乱麻。

Erlang 的创建者 JoeArmstrong 有句名言:

面向对象语言的问题在于,它们依赖于特定的环境。你想要个香蕉,但拿到的却是拿着香蕉的猩猩,乃至最后你拥有了整片丛林。

能解决问题的就是最好的:

程序设计要专注于“应用逻辑的实现”本身,应该尽量避免被“某种技术”分心 。《UNIX编程艺术》,第一原则就是KISS原则,整本书都贯彻了KISS(keep it simple, stupid!) 原则。写项目、写代码,目的都是为了解决问题。而不是花费或者说浪费过多的时间在考虑与要解决的问题完全无关的事情上。不管是面向过程,还是面向对象,都是为了解决某一类问题的技术。各有各的用武之地:

在驱动开发、嵌入式底层开发这些地方,面向过程开发模式,干净,利索,直观,资源掌控度高。在这些环境,面向过程开发几乎是无可替代的。

在工作量大,难度较低、细节过多、用简单的规范规则无法面面俱到的环境下,用面向对象开发模式,用低质量人力砸出来产业化项目。

1、面向对象编程

面向对象只是一种设计思路,是一种概念,并没有说什么C++是面向对象的语言,java是面向对象的语言。C语言一样可以是面向对象的语言,Linux内核就是面向对象的原生GNU C89编写的,但是为了支持面向对象的开发模式,Linux内核编写了大量概念维护modules,维护struct的函数指针,内核驱动装载等等机制。而C++和java为了增加面向对象的写法,直接给编译器加了一堆语法糖。

2、什么是类和对象

在C语言中,结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。
在C++语言中,类也是一种构造类型,但是进行了一些扩展,可以将类看做是结构体的升级版,类的成员不但可以是变量,还可以是函数;不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)在 C++ 中,通过类名就可以创建对象,这个过程叫做类的实例化,因此也称对象是类的一个实例(Instance)类的成员变量称为属性(Property),将类的成员函数称为方法(Method)。在C语言中的使用struct这个关键字定义结构体,在C++ 中使用的class这个关键字定义类。

结构体封装的变量都是 public 属性,类相比与结构体的封装,多了 private 属性和 protected  属性, private 和protected  关键字的作用在于更好地隐藏了类的内部实现 ,只有类源代码才能访问私有成员,只有派生类的类源代码才能访问基类的受保护成员,每个人都可以访问公共成员。这样可以有效的防止可能被不知道谁访问的全局变量。

C语言中的结构体:

1//通过struct 关键字定义结构体
2struct object3{
4    char name[8];             
5    char type;                
6    char flag;               
7    //指向函数的指针类型
8    void  (*display)(void);           
9};

C++语言中的类:

 1//通过class关键字类定义类
2class object{
3public:
4    char name[8];            
5    char type;              
6    char flag;               
7    //类包含的函数体
8    void display(){
9        printf("123456789");
10    }
11};

3、内存分布的对比

不管是C语言中的结构体或者C++中的类,都只是相当于一个模板,起到说明的作用,不占用内存空间;结构体定义的变量和类创建的对象才是实实在在的数据,要有地方来存放,才会占用内存空间。

结构体变量的内存模型:
结构体的内存分配是按照声明的顺序依次排列,涉及到内存对齐问题。
为什么会存在内存对齐问题,引用傻孩子公众号裸机思维的文章《漫谈C变量——对齐》加以解释:

在ARM Compiler里面,结构体内的成员并不是简单的对齐到字(Word)或者半字(Half Word),更别提字节了(Byte),结构体的对齐使用以下规则:

  • 整个结构体,根据结构体内最大的那个元素来对齐。比如,整个结构体内部最大的元素是WORD,那么整个结构体就默认对齐到4字节。

  • 结构体内部,成员变量的排列顺序严格按照定义的顺序进行。

  • 结构体内部,成员变量自动对齐到自己的大小——这就会导致空隙的产生。

  • 结构体内部,成员变量可以通过 attribute ((packed))单独指定对齐方式为byte。

strut对象的内存模型:

1//通过struct 关键字定义结构体
2struct {
3    uint8_t    a;
4    uint16_t   b;
5    uint8_t    c;
6    uint32_t   d;
7};

memory layout:

29e11531cc5a87f25fe21baabce52d8e.png

class对象的内存模型:
假如创建了 10 个对象,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码,放在code区。如下图所示:

208fbf329f67050d46074b09eb36fd51.png

成员变量在堆区或栈区分配内存,成员函数放在代码区。对象的大小只受成员变量的影响,和成员函数没有关系。对象的内存分布按照声明的顺序依次排列,和结构体非常类似,也会有内存对齐的问题。

可以看到结构体和对象的内存模型都是非常干净的,C语言里访问成员函数实际上是通过指向函数的指针变量来访问(相当于回调),那么C++编译器究竟是根据什么找到了成员函数呢?
实际上C++的编译代码的过程中,把成员函数最终编译成与对象无关的全局函数,如果函数体中没有成员变量,那问题就很简单,不用对函数做任何处理,直接调用即可。
如果成员函数中使用到了成员变量该怎么办呢?成员变量的作用域不是全局,不经任何处理就无法在函数内部访问。
C++规定,编译成员函数时要额外添加一个this指针参数,把当前对象的指针传递进去,通过this指针来访问成员变量。

this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。

这样通过传递对象指针完成了成员函数和成员变量的关联。这与我们从表明上看到的刚好相反,通过对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。
这在C++中一切都是隐式完成的,对程序员来说完全透明,就好像这个额外的参数不存在一样。

无论是C还是C++,其函数第一个参数都是一个指向其目标对象的指针,也就是this指针,只不过C++由编译器自动生成——所以方法的函数原型中不用专门写出来而C语言模拟的方法函数则必须直接明确的写出来

4 掩码结构体

在C语言的编译环境下,不支持结构体内放函数体,除了函数外,就和C++语言里定义类和对象的思路完全一样了。还有一个区别是结构体封装的对象没有好用的private 和protected属性,不过C语言也可以通过掩码结构体这个骚操作来实现private 和protected的特性。

注:此等操作并不是面向对象必须的,这个属于锦上添花的行为,不用也不影响面向对象。

先通过一个例子直观体会一下什么是掩码结构体,以下例子来源为:傻孩子的PLOOC的readme,作者仓库地址:https://github.com/GorgonMeducer/PLOOC

 1//! the original structure in class source code
2struct byte_queue_t {
3    uint8_t   *pchBuffer;
4    uint16_t  hwBufferSize;
5    uint16_t  hwHead;
6    uint16_t  hwTail;
7    uint16_t  hwCount;
8};
9
10//! the masked structure: the class byte_queue_t in header file
11typedef struct byte_queue_t {
12    uint8_t chMask [sizeof(struct {
13        uint8_t   *pchBuffer;
14        uint16_t  hwBufferSize;
15        uint16_t  hwHead;
16        uint16_t  hwTail;
17        uint16_t  hwCount;
18    })];
19} byte_queue_t;

为了使其工作,我们必须确保类源代码不包括其自己的接口头文件。您甚至可以这样做…如果您对内容很认真

 1//! the masked structure: the class byte_queue_t in header file
2typedef struct byte_queue_t {
3    uint8_t chMask [sizeof(struct {
4        uint32_t        : 32;
5        uint16_t        : 16;
6        uint16_t        : 16;
7        uint16_t        : 16;
8        uint16_t        : 16;
9    })];
10} byte_queue_t;

通过这个例子,我们可以发现给用户提供的头文件,其实是一个固态存储器,即使用字节数组创建的掩码,用户通过掩码结构体创建的变量无法访问内部的成员,这就是实现属性私有化的方法。至于如何实现只有类源代码才能访问私有成员,只有派生类的类源代码才能访问基类的受保护成员的特性,这里先埋个伏笔,关注本公众号,后续文章再深入探讨。

还回到掩码结构体本身的特性上,可以发现一个问题,掩码结构体丢失了结构体的对齐信息,因为掩码的本质是创建了一个chMask数组,我们知道数组是按照元素对齐的,而原本结构体是按照Word对齐的。所以当你用掩码结构体声名结构体变量的时候,这个变量多半不是对齐到word的,当你在模块内访问这个对象的时候…编译器默认你整个结构体是对齐到word,这就会导致错位的产生,可能会直接导致hardfault了!

为了解决这个问题,可以利用_ attribute_ ((align))以及 _ alignof_的操作,对它进行如下改进:

 1//! the original structure in class source code
2struct byte_queue_t {
3    struct  {                                                               \
4            uint8_t   *pchBuffer;
5            uint16_t  hwBufferSize;
6            uint16_t  hwHead;
7            uint16_t  hwTail;
8            uint16_t  hwCount;                                                         \
9               }__attribute__((aligned(__alignof__(struct {uint8_t   *pchBuffer;
10            uint16_t  hwBufferSize;
11            uint16_t  hwHead;
12            uint16_t  hwTail;
13            uint16_t  hwCount;}))));   
14};
15
16//! the masked structure: the class byte_queue_t in header file
17typedef struct byte_queue_t {
18            uint8_t chMask                  \
19                [sizeof(struct {uint8_t   *pchBuffer;
20            uint16_t  hwBufferSize;
21            uint16_t  hwHead;
22            uint16_t  hwTail;
23            uint16_t  hwCount;})]                              \
24                __attribute__((aligned(__alignof__(struct {uint8_t   *pchBuffer;
25            uint16_t  hwBufferSize;
26            uint16_t  hwHead;
27            uint16_t  hwTail;
28            uint16_t  hwCount;}))));                                       \
29} byte_queue_t;

这部分理解起来可能稍微有点复杂,但是不理解也没关系,现在先知道有这个东西,后续文章还会有更骚的操作来更直观的实现封装、继承和多态!

5 C语言实现类的封装

如果你趟过了掩码结构体那条河,那么恭喜你,你已经成功上岸了。我们继续回到面向对象的问题上,面向对象的核心是把数据和处理数据的方法封装在一起。封装并不是只有放在同一个结构体里这一种形式,放在同一个接口头文件里(也就是.h)里,也是一种形式——即,一个接口头文件提供了数据的结构体,以及处理这些数据的函数原型声明,这已经完成了面向对象所需的基本要求。下边将通过C语言的具体实例加以说明。

假设我们要封装一个基于字节的队列类,不妨叫做Queue,因此我们建立了一个类文件queue.c和对应的接口头文件queue.h。假设我们约定queue.c将不包含queue.h(这么做的好处很多,在以后的内容里再讲解,当然对掩码结构体技术来说,模块的实现是否包含模块的接口头文件并不是关键)。

queue.h

 1...
2//! the masked structure: the class byte_queue_t in header file
3typedef struct queue_t {
4            uint8_t chMask                  \
5                [sizeof(struct {uint8_t   *pchBuffer;
6            uint16_t  hwBufferSize;
7            uint16_t  hwHead;
8            uint16_t  hwTail;
9            uint16_t  hwCount;})]                              \
10                __attribute__((aligned(__alignof__(struct {uint8_t   *pchBuffer;
11            uint16_t  hwBufferSize;
12            uint16_t  hwHead;
13            uint16_t  hwTail;
14            uint16_t  hwCount;}))));                                       \
15} queue_t;
16
17...
18extern bool queue_init(queue_t *ptQueue, uint8_t *pchBuffer, uint16_t hwSize);
19extern bool enqueue(queue_t *ptQueue, uint8_t chByte);
20extern bool dequeue(queue_t *ptQueue, uint8_t *pchByte);
21extern bool is_queue_empty(queue_t *ptQueue);
22...

queue.c

 1...
2//! the original structure in class source code
3typedef struct __queue_t {
4    struct  {                                                               \
5            uint8_t   *pchBuffer;
6            uint16_t  hwBufferSize;
7            uint16_t  hwHead;
8            uint16_t  hwTail;
9            uint16_t  hwCount;                                                         \
10               }__attribute__((aligned(__alignof__(struct {uint8_t   *pchBuffer;
11            uint16_t  hwBufferSize;
12            uint16_t  hwHead;
13            uint16_t  hwTail;
14            uint16_t  hwCount;}))));   
15}__queue_t;
16...

可以看到,实际上类型queue_t是一个掩码结构体,里面只有一个起到掩码作用的数组chMask,其大小和真正后台的的类型__queue_t相同——这就是掩码结构体实现私有成员保护的秘密。解决了私有成员保护的问题,剩下还有一个问题,对于queue.c的函数来说queue_t只是一个数组,那么正常的功能要如何实现呢?下面的代码片将断为你解释一切:

 1...
2#define __class(__NAME)                  __##__NAME
3#define class(__NAME)                   __class(__NAME)   
4bool is_queue_empty(queue_t *ptQueue) 5{
6    CLASS(queue_t) *ptQ = (CLASS(queue_t) *)ptQueue;
7    if (NULL == ptQueue) {
8        return true;
9    }
10    return ((ptQ->hwHead == ptQ->hwTail) && (0 == ptQ->hwCount));
11}
12...

可以从这里看出来,只有类的源文件才能看到内部使用的结构体,而掩码结构体是模块内外都可以看到的,简单来说,如果实际内部的定义为外部的模块所能直接看见,那自然就没有办法起到保护作用。

从编译器的角度来说,这种从queue_t到__queue_t类型指针的转义是逻辑上的,并不会因此产生额外的代码,简而言之,使用掩码结构体几乎是没有代价的。

再次强调:实现面向对象,掩码结构体并不是必须的,只是锦上添花,所以不理解的话,也不要纠结!

想要更深入了解C语言面向对象的思想,建议参考的书籍:《UML+OOPC嵌入式C语言开发精讲》

点击下方“蓝字”,发现更多精彩。

STM32通用Bootloader——FOTA

STM32通用FLASH管理软件包——SFUD/FAL

STM32通用低功耗组件——PM

87e676cd9869b8c4a3a0571153cdcec9.png长按关注我们CSDN博客:Aladdin Wang微信号:1763035080514389569adbaecbb8710a8c9975b3c06.gif“在看”的小可爱永远十八岁!

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

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

相关文章

LeetCode 1718. 构建字典序最大的可行序列(贪心+回溯)

文章目录1. 题目2. 解题1. 题目 给你一个整数 n ,请你找到满足下面条件的一个序列: 整数 1 在序列中只出现一次。2 到 n 之间每个整数都恰好出现两次。对于每个 2 到 n 之间的整数 i ,两个 i 之间出现的距离恰好为 i 。 序列里面两个数 a[…

static函数

// 以下摘自网络 静态函数在函数的返回类型前加上关键字static&#xff0c;函数就被定义成为静态函数。函数的定义和声明默认情况下是extern的&#xff0c;但静态函数只是在声明他的文件当中可见&#xff0c;不能被其他文件所用。定义静态函数的好处&#xff1a;<1> 其他…

操作系统是计算机的什么管理者,操作系统是计算机资源的管理者

由计算机提供的、因应用程序的存在和运行所消耗或占用的物质条件&#xff0c;叫做计算机的资 源&#xff0c;例如处理器的时间、内存空间、外部设各等。为了有效地利用计算机的各种资源&#xff0c;操作系统 必须为用户承担起对计算机资源的管理任务。例如&#xff0c;为了使应…

python文件流读取二进制_Python使用web.py从POST请求中提取二进制文件

I am developing an API that allows outside clients to send a binary file which will be processed. my web.data() is a string and the function I am calling requires a binary. How do I get it into the correct format? Maybe I have the incorrect Headers? how …

LeetCode 1828. 统计一个圆中点的数目

文章目录1. 题目2. 解题1. 题目 给你一个数组 points &#xff0c;其中 points[i] [xi, yi] &#xff0c;表示第 i 个点在二维平面上的坐标。多个点可能会有 相同 的坐标。 同时给你一个数组 queries &#xff0c;其中 queries[j] [xj, yj, rj] &#xff0c;表示一个圆心在…

LeetCode 1829. 每个查询的最大异或值(前缀异或 + 位运算)

文章目录1. 题目2. 解题1. 题目 给你一个 有序 数组 nums &#xff0c;它由 n 个非负整数组成&#xff0c;同时给你一个整数 maximumBit 。你需要执行以下查询 n 次&#xff1a; 找到一个非负整数 k < 2^maximumBit &#xff0c;使得 nums[0] XOR nums[1] XOR ... XOR num…

python的运行环境_python-运行环境配置-1

Linux下安装Python3.6和第三方库 如果本机安装了python2&#xff0c;尽量不要管他&#xff0c;使用python3运行python脚本就好&#xff0c;因为可能有程序依赖目前的python2环境&#xff0c; 比如yum&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 不要动现有的…

计算机主板最常见的问题,计算机主板功能 电脑实用技巧解决常见问题

电脑实用技巧解决常见问题1.解决问题 插电即开机问题现象&#xff1a;有些朋友有关机后断开电源板电源的习惯&#xff0c;可是却常常被一个问题困扰&#xff0c;就是电源板一通电&#xff0c;计算机就自动开机了&#xff0c;Power键形同虚设。解决问题&#xff1a;有些主板在BI…

zeal刷新不出来_饥荒:游戏中的这些事物都是无中生有,几乎可以无限制刷新!...

在饥荒这款经典的生存类游戏中&#xff0c;其事物的“刷新”和许多其他的生存游戏不太一样&#xff0c;有些生存游戏的事物刷新是定时定量的&#xff0c;而饥荒则不完全&#xff0c;就拿牛群来形容&#xff0c;他们必须要有多个成年的牛进行繁殖才会出现新的牛&#xff0c;而像…

c盘清理代码_拒接卡顿,从c盘减负、系统修复及网络加速做起!奥利~~~

你是否有过系统卡顿&#xff0c;C盘饱满&#xff1f;你是否有过蓝屏、闪退&#xff1f;你是否有过网络爆卡&#xff1f;今天&#xff0c;他来了&#xff0c;一篇文章教会你清理C盘、修复系统、网络加速&#xff01;&#xff01;&#xff01;首先&#xff0c;按下winr键&#xf…

LeetCode 1833. 雪糕的最大数量(贪心)

文章目录1. 题目2. 解题1. 题目 夏日炎炎&#xff0c;小男孩 Tony 想买一些雪糕消消暑。 商店中新到 n 支雪糕&#xff0c;用长度为 n 的数组 costs 表示雪糕的定价&#xff0c;其中 costs[i] 表示第 i 支雪糕的现金价格。 Tony 一共有 coins 现金可以用于消费&#xff0c;他…

用递归计算一个数字每一位相加的结果。_leetcode 2 两数相加(c++)

两种解法&#xff0c;第一种是在原有的两个链表中选择更长的那个作为结果返回&#xff0c;虽然节约了空间&#xff0c;但是增加了时间复杂度&#xff0c;而且没有用到如何设置链表的增加与删除&#xff0c;第二种看起来清爽很多&#xff0c;逻辑也清晰。### 题目给出两个非空的…

LeetCode 1834. 单线程 CPU(排序 + 优先队列)

文章目录1. 题目2. 解题1. 题目 给你一个二维数组 tasks &#xff0c;用于表示 n​​​​​​ 项从 0 到 n - 1 编号的任务。 其中 tasks[i] [enqueueTimei, processingTimei] 意味着第 i​​​​​​​​​​ 项任务将会于 enqueueTimei 时进入任务队列&#xff0c;需要 pro…

服务器支持磁盘阵列,服务器磁盘阵列、RAID级别的阐述

磁盘阵列磁盘阵列(Redundant Arrays of Independent Disks&#xff0c;RAID)&#xff0c;有“独立磁盘构成的具有冗余能力的阵列”之意。磁盘阵列是由很多价格较便宜的磁盘&#xff0c;组合成一个容量巨大的磁盘组&#xff0c;利用个别磁盘提供数据所产生加成效果提升整个磁盘系…

LeetCode 1835. 所有数对按位与结果的异或和(位运算 (ab)^(ac) = a(b^c) )

文章目录1. 题目2. 解题1. 题目 列表的 异或和&#xff08;XOR sum&#xff09;指对所有元素进行按位 XOR 运算的结果。 如果列表中仅有一个元素&#xff0c;那么其 异或和 就等于该元素。 例如&#xff0c;[1,2,3,4] 的 异或和 等于 1 XOR 2 XOR 3 XOR 4 4 &#xff0c;而 …

python入门基础系列_03python—9个基础常识-python小白入门系列

《python小白入门系列教程》 专栏 • 第03篇 文 | xc_718 深度好文&#xff1a;1828字 | 4分钟阅读 ​ 1. 注释 1&#xff09;单行注释&#xff1a;****# #注释内容 print(123) #123 print(abc) #abc print("abc") #abc **2&#xff09;多行注释&#xff1a; 或 **&q…

css英文左右对齐,中文英文左右padding一致两端对齐实现_js

先看下图&#xff1a;就是一个定宽的容器&#xff0c;左右padding值20像素&#xff0c;结果输入一段文字后(有中文也有英文字符)&#xff0c;会发现右侧根本就不对齐&#xff0c;有些地方距离右侧的空白大小也不是20像素&#xff0c;感觉不和谐&#xff0c;设计师就希望排列能够…

python判断字符类型编程_Python检测数据类型的方法总结

我们在用python进行程序开发的时候&#xff0c;很多时候我们需要检测一下当前的变量的数据类型。比如需要在使用字符串操作函数之前先检测一下当前变量是否是字符串。下面小编给大家分享一下在python中如何检测数据类型 首先我们打开CMD控制台&#xff0c;进入到python环境&…

RDD 编程

文章目录1. RDD 创建2. RDD转换3. RDD动作4. 持久化5. 分区6. 文件数据读写6.1 本地6.2 hdfs6.3 Json文件6.4 Hbase学习自 MOOC Spark编程基础1. RDD 创建 从文件创建 Welcome to____ __/ __/__ ___ _____/ /___\ \/ _ \/ _ / __/ _//___/ .__/\_,_/_/ /_/\_…

用python解决生活问题_Python解决生活问题之闹钟程序的实现

昨天下班回家忘了带手机充电器&#xff0c;手机熄火没闹钟了&#xff0c;可现实是迟到30分钟以内要罚100RMB&#xff0c;超过30分钟算旷工要扣除3天工资&#xff0c;想想这代价&#xff0c;好吧&#xff0c;还是自己动手写一个闹钟程序吧&#xff01; 系统环境&#xff1a; Lin…