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[…

collections求和方法_java集合求和最大值最小值示例分享

package com.happyelements.athene.game.util;import static com.google.common.base.Preconditions.checkNotNull;import java.util.Collection;import com.google.common.collect.Lists;/*** Math工具类** version 1.0* since 1.0*/public class MathUtil {/*** see MathUtil…

为什么要给计算机配置IP地址,更改ip地址 为何要重启电脑

在系统中,如果想修改计算机的IP地址,系统会提示重新启动计算机,有没有什么办法不重启机器又照样改IP呢? 首先按自己的需要改动IP地址,点击[确定] 按钮后,系统会出现重新启动计算机的提示。此时点击[否]&…

static函数

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

python中数据类型转换原理_python的可变与不可变数据类型的原理是什么呢?

Python中变量的进阶知识&#xff1a; 涉及到Python的对象知识。 即&#xff1a;可变对象和不可变对象。题目中的可变和不可变数据类型&#xff0c;表述不准确。 Python中&#xff0c;一切皆对象。 对象是一种抽象。抽象是什么&#xff1f;抽象就是一种概念上的简化。 《learnin…

python股票历史最低点_Python统计某一只股票每天的开盘,收盘,最高,最低价格!...

模块&#xff1a;Numpy码字不易&#xff0c;转载请注明出处&#xff01;十分感谢&#xff01;准备工作&#xff1a;抓取某一只股票的信息&#xff0c;每30min为一组数据&#xff0c;可以参考上一篇&#xff1a;Note: 只为演示如何统计&#xff0c;更精准的可以抓取每5min为一组…

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

由计算机提供的、因应用程序的存在和运行所消耗或占用的物质条件&#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;表示一个圆心在…

elasticsearch threadpool

http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/modules-threadpool.html转载于:https://www.cnblogs.com/SamuelSun/p/4287218.html

python argument list too long_[已解决]Argument list too long如何处理?

Argument list too long 本质是需要处理的长度超过系统的长度&#xff0c;因此无法执行相关命令。经过搜索发现了两种方法&#xff0c;思想都是将参数切分成小的段落进行执行。法一&#xff1a;通过xargs传递参数 (数据量大了还是不太奏效)find . -name "*.log" | xa…

计算机专业内存容量8g,内存容量8g和16g区别

语音内容&#xff1a;大家好&#xff0c;我是时间财富网智能客服时间君&#xff0c;上述问题将由我为大家进行解答。以计算机为例&#xff0c;内存容量8g和16g区别如下&#xff1a;1、16GB和8GB内存在同品牌、同芯片规格等情况下&#xff0c;仅存在容量大小区别&#xff0c;即1…

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; 不要动现有的…

[objective-c] 08 - 内存管理

OC语言中的内存管理机制为ARC(Automatic Reference Counting,自动引用计数)。于2011年中旬推出&#xff0c;替换陈旧且低效的手动内存管理&#xff0c;关于手动内存管理的内容&#xff0c;本章教程不再讲授。本章主要从以下几个方面对内存管理进行展开讲解。 内存管理原则对象引…

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

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

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

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

数字逻辑基础与verilog设计_数字电路学习笔记(五):逻辑设计基础

马上就要正式进入电路设计了&#xff0c;再来看最后一个知识点&#xff1a;逻辑设计吧。之前我们花了两章&#xff0c;探讨了逻辑运算是什么&#xff0c;怎么算&#xff1b;但还有最后一个大问题&#xff0c;巧妇难为无米之炊&#xff0c;我们得先有一个逻辑式&#xff0c;才能…

完整的开发一个ContentProvider步骤

1、定义自己的ContentProvider类&#xff0c;该类需要继承Android提供的ContentProvider基类。2、向Android系统注册这个"网站"&#xff0c;也就是在AndroidManifest.xml文件中注册这个ContentProvider&#xff0c;就像注册Activity一样。注册ContentProvider时需要为…

河北省高校计算机大赛,河北省教育厅关于举办2016年华北五省(市、自治区)及港澳台大学生计算机应用大赛河北赛区竞赛的通知...

有关高等学校&#xff1a;为深入贯彻落实《国家中长期教育改革和发展规划纲要(2010-2020年)》和教育部《关于全面提高高等教育质量的若干意见》(教高﹝2012﹞4号)精神&#xff0c;推动区域高等教育合作发展、资源共享&#xff0c;培养大学生创新能力、实践能力和团队协作意识&a…