点击“蓝字”关注我们吧前言:

何为面向过程:

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

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

何为面向对象:

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

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

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:


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


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

可以看到结构体和对象的内存模型都是非常干净的,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}; 910//! the masked structure: the class byte_queue_t in header file11typedef 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};1516//! the masked structure: the class byte_queue_t in header file17typedef 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;1617...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

长按关注我们CSDN博客:Aladdin Wang微信号:17630350805“在看”的小可爱永远十八岁!

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

  1. 【C语言】C语言实现面向对象编程之封装

    00. 目录 文章目录 00. 目录 01. 前言 02. 简单程序示例 03. 程序示例优化 04. 总结 05. 参考 01. 前言 面向对象编程(OOP)并不是一种特定的语言或者工具,它只是一种 ...

  2. JS面向对象编程之封装

    我们所熟知的面向对象语言如 C++.Java 都有类的的概念,类是实例的类型模板,比如Student表示学生这种类型,而不表示任何具体的某个学生,而实例就是根据这个类型创建的一个具体的对象,比如zha ...

  3. C/C++面向对象编程之多态

    C/C++面向对象编程 目录: 1.类型转换 2.为什么引入多态和虚函数 3.编译器如何实现多态和虚函数 4.多态的思想 5.C语言用实现多态的思想 目录: C/C++面向对象编程之封装 C/C++面 ...

  4. C语言--面向对象编程之多态

    系列文章目录 C语言–面向对象编程之封装 C语言–面向对象编程之继承 文章目录 系列文章目录 前言 一.多态是什么? 二.C语言实现多态 1.多态基本实现原理 2.一个简单的demo 3.简要分析 三 ...

  5. C语言--面向对象编程之继承

    系列文章目录 C语言实现面向对象编程的第二篇,在第一篇里面我们已经了解到了何为面向对象以及实现面向对象的第一大特性–封装,对于C来说,实现继承和多态要稍微麻烦一些. C语言实现面向对象- - 封装 C ...

  6. python面向对象编程之实例属性和类属性

    廖雪峰python教程 面向对象编程之实例属性和类属性 练习 为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加: # -*- coding: utf-8 -*- ...

  7. python面向对象编程之访问限制

    廖雪峰python教程 面向对象编程之访问限制 https://www.liaoxuefeng.com/wiki/1016959663602400/1017496679217440 练习: 请把下面的 ...

  8. python面向对象编程指南 脚本之家_Python面向对象编程之继承与多态详解

    本文实例讲述了Python面向对象编程之继承与多态.分享给大家供大家参考,具体如下: Python 类的继承 在OOP(Object Oriented Programming)程序设计中,当我们定义一 ...

  9. 【历史上的今天】5 月 17 日:面向对象编程之父出生;国内全面接入互联网;惠普收购 Cray

    整理 | 王启隆 透过「历史上的今天」,从过去看未来,从现在亦可以改变未来. 今天是 2022 年 5 月 17 日,在 1969 年的今天,国际电信联盟第二十四届行政理事会正式通过决议,决定把国际电 ...

最新文章

  1. 怎样能拿到第一份编程工作?这里告诉你答案 | 码书
  2. 安卓高手之路之图形系统(6)ListView继续
  3. 安装hmmlearn
  4. 4.5. Rspamd
  5. 【转】Android Camera 相机开发详解
  6. mac 使用brew卸载安装node
  7. 运行Vue在ASP.NET Core应用程序并部署在IIS上
  8. 英语中十二个月份的由来
  9. 偷梁换柱 | 无备份情况下的数据恢复实践(二)
  10. g729语音编码 matlab,Case7 FreeSwitch配置开启转码功能及安装G729语音编码
  11. 七.OpenCv图像轮廓
  12. Kubernetes Deployment故障排除图解指南
  13. mysql hugepage_Linux配置HugePage
  14. 回溯法采用的搜索策略_下面哪种函数是回溯法中为避免无效搜索采取的策略( )...
  15. 百度信息流是什么?哪些行业适合投放百度信息流?
  16. el-date-picker 实现禁止选择今日以后的日期,以及时间跨度不超过365天,和设置默认选择日期,解决选择当天无效问题
  17. Excel绘制带象限散点图的4种办法
  18. 虹科小课堂|密度测量,你了解多少?
  19. 下载的视频花屏怎么办?其实很简单!!!
  20. OAuth2.0协议(一) - 授权码许可流程

热门文章

  1. cocos2dx 圆盘抽奖_cocoscreator之微信小游戏的抽奖转盘
  2. python给定一个整数n、判断n是否为素数_输入一个大于3的整数n,判断它是否为素数...
  3. 深入浅出mysql gtid_深入理解MySQL GTID
  4. 散点画三维曲面图_UG 复杂曲面合金零件的数控加工
  5. 20190501-编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串...
  6. [源码和文档分享]基于C语言的语法高亮设计与实现
  7. POJ 计算几何(3)
  8. shell自动生成的文件有一个问号的后缀
  9. linux tar 使用
  10. VS_VERSION_INFO信息的读取