前言
上次整理了一篇关于面向对象的笔记:什么是面向对象?,简单地分享了面向对象的一些基础知识。C语言虽不是面向对象的语言,但也可以使用面向对象的思想来设计我们的程序,C语言 + 面向对象的思想
在我们嵌入式中使用得很广泛,主要优点就是能使我们的软件拓展性更好、更易读、更容易维护等。
因为这一块知识也比较重要,属于通用知识,所以打算分享几篇笔记与大家一起学习一下。当然,C语言并不是面向对象的语言,要想完全实现与C++一样的一些面向对象的特性会比较难。所以我们分享的内容也面向基础、实用的为主。
本篇笔记分享的是:封装与抽象
。
封装与抽象
封装性
是面向对象编程的三大特性(封装性、继承性、多态性)之一,但也是最重要的特性。封装+抽象
相结合就可以对外提供一个低耦合的模块。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
在C语言中,数据封装可以从结构体
入手,结构体里可以放数据成员和操作数据的函数指针成员。当然,结构体里也可以只包含着要操作的数据。
下面以一个简单的实例作为演示。
设计一个软件模块,模块中要操作的对象是长方形
,需要对外提供的接口有:
1、创建长方形对象;
2、设置长、宽;
3、获取长方形面积;
4、打印长方形的信息(长、宽、高);
5、删除长方形对象。
下面我们来一起完成这个demo代码。首先,我们思考一下,我们的接口命名大概是怎样的?其实这是有规律可循的,我们看RT-Thread的面向对象接口是怎么设计的:
我们也模仿这样子的命名形式来给我们这个demo的几个接口命名:
1、rect_create
2、rect_set
3、rect_getArea
4、rect_display
5、rect_delete
我们建立一个rect.h
的头文件,在这里声明我们对外提供的几个接口。这时候我们头文件可以设计为:
这样做是没有什么问题的。可是数据隐藏得不够好,我们提供给外部用的东西要尽量简单。我们可以思考一下,对于C语言的文件操作,C语言库给我们提供怎么样的文件操作接口?如:
FILE *fopen(const char *pathname, const char *mode);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
我们会创建一个文件句柄(描述符),然后之后只要操作这个文件句柄就可以,我们不用关心FILE
具体是怎么实现的。
什么是句柄?看一下百度百科的解释:
我们也可以创建我们的对象句柄,对外提供的头文件中只需暴露我们的对象句柄,不用暴露具体的实现。以上头文件rect.h
代码可以修改为:
这里用到了void*
,其为无类型指针,void *可以指向任何类型的数据
。然后具体要操作怎么样的结构体可以在.c中实现:
下面我们依次实现上述五个函数:
1、rect_create函数
/* 创建长方形对象 */
HandleRect rect_create(const char *object_name)
{
printf(">>>>>>>>>> %s: %s (line: %d) <<<<<<<<<<\n", __FILE__, __FUNCTION__, __LINE__);
/* 给rect结构体变量分配内存 */
pRect rect = (pRect)malloc(sizeof(Rect));
if (NULL == rect)
{
printf("rect memory malloc failed!\n");
abort();
}
/* 给rect->object_name字符串申请内存 */
rect->object_name = (char*)malloc(strlen(object_name) + 1);
if (NULL == rect->object_name)
{
printf("rect->object_name memory malloc failed!\n");
abort();
}
/* 给结构体各成员进行初始化 */
strncpy(rect->object_name, object_name, strlen(object_name) + 1);
rect->length = 0;
rect->width = 0;
return ((HandleRect)rect);
}
rect对象创建函数:首先分配内存,然后对rect结构体各个成员进行赋值操作,最后返回的是rect对象句柄。rect的object_name成员是个字符串,因此要单独分配内存。
2、rect_set函数
/* 设置长方形对象长、宽 */
void rect_set(HandleRect rect, int length, int width)
{
printf(">>>>>>>>>> %s: %s (line: %d) <<<<<<<<<<\n", __FILE__, __FUNCTION__, __LINE__);
if (rect)
{
((pRect)rect)->length = length;
((pRect)rect)->width = width;
}
}
3、rect_getArea函数
/* 获取长方形对象面积 */
int rect_getArea(HandleRect rect)
{
return ( ((pRect)rect)->length * ((pRect)rect)->width );
}
4、rect_display函数
/* 打印显示长方形对象信息 */
void rect_display(HandleRect rect)
{
printf(">>>>>>>>>> %s: %s (line: %d) <<<<<<<<<<\n", __FILE__, __FUNCTION__, __LINE__);
if (rect)
{
printf("object_name = %s\n", ((pRect)rect)->object_name);
printf("length = %d\n", ((pRect)rect)->length);
printf("width = %d\n", ((pRect)rect)->width);
printf("area = %d\n", rect_getArea(rect));
}
}
5、rect_delete函数
void rect_delete(HandleRect rect)
{
printf(">>>>>>>>>> %s: %s (line: %d) <<<<<<<<<<\n", __FILE__, __FUNCTION__, __LINE__);
if (rect)
{
free(((pRect)rect)->object_name);
free(rect);
((pRect)rect)->object_name = NULL;
rect = NULL;
}
}
rect对象删除函数:主要是对创建函数中的malloc申请的内存做释放操作。
可以看到这五个对象接口主要包含三类:创建对象函数、操作函数、删除对象函数
。这里的操作函数就是rect_set函数、rect_getArea函数与rect_display函数,当然还可以有其它更多的操作函数。操作函数的特点是至少需要传入一个表示对象的句柄,在函数的内部再做实际数据结构的转换,然后再进行相应的操作。
6、测试程序:
#include <stdio.h>
#include <stdlib.h>
#include "rect.h"
int main(void)
{
HandleRect rect = rect_create("rect_obj"); // 创建Rect对象句柄
rect_set(rect, 20, 5); // 设置
rect_display(rect); // 打印显示
rect_delete(rect); // 删除Rect对象句柄
return 0;
}
运行结果:
在基于对象的编程中,封装性是最基础也最重要的内容。其对象主要包含两方面内容:属性
与方法
。在基于C语言的对象编程中,可以使用句柄来表示对象,即句柄指向的数据结构的成员代表对象的属性,实际操作句柄的函数则表示对象的方法。
以上就是本次的分享,如有错误,欢迎指出!本篇笔记相关代码可在公众号后台回复关键字:C语言对象编程第一弹:封装与抽象
,即可获取。