【实战】到底什么是C语言对象编程?

李肖遥

共 1581字,需浏览 4分钟

 ·

2020-09-02 01:05


ID:技术让梦想更伟大

作者:ZhengNL

整理:李肖遥

前言

在之前肖遥分享写过一篇关于面都对象的文章,真的可以,用C语言实现面向对象编程OOP , 本篇肖遥给大家整理了ZhengNL三合一的一篇面对对象的文章,例子也很通俗易懂,希望对大家有帮助。

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)
 {
  //free(rect);
  //rect = NULL;
  abort();
 }
 /* 给rect->object_name字符串申请内存 */
 rect->object_name = (char*)malloc(strlen(object_name) + 1);
 if (NULL == rect->object_name)
 {
  //free(rect->object_name);
  //rect->object_name = NULL;
  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 
#include 
#include "rect.h"

int main(void)
{
 HandleRect rect = rect_create("rect_obj");  // 创建Rect对象句柄
 rect_set(rect, 205);         // 设置     
 rect_display(rect);            // 打印显示 
 rect_delete(rect);             // 删除Rect对象句柄 
 
 return 0;
}

运行结果:


在基于对象的编程中,封装性是最基础也最重要的内容。其对象主要包含两方面内容:属性方法

在基于C语言的对象编程中,可以使用句柄来表示对象,即句柄指向的数据结构的成员代表对象的属性,实际操作句柄的函数则表示对象的方法。

继承 

继承简单说来就是父亲有的东西,孩子可以继承过来。

当创建一个类时,我们不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。

这个已有的类称为基类,新建的类称为派生类

继承在C++ 中还会细分为很多,我们就不考虑那么多了,只分享比较简单也比较实用的。

在C语言对象编程中,有两种方法实现继承:

第一种是:结构体包含结构体实现继承。

第二种是:利用私有指针实现继承。

下面依旧以实例进行分享:

结构体包含结构体

我们以上一篇笔记的例子为例继续展开。上一篇的例子为:

假如我们要操作的对象变为长方体,长方体就可以继承长方形的数据成员和函数,这样就可以复用之前的一些代码。具体操作看代码:

1、结构体

2、头文件

3、长方体对象创建、删除函数

4、操作函数

5、测试及测试结果


可见,长方体结构体可以继承长方形结构体的数据、长方体对象相关操作也可以继承长方形对象的相关操作。这样可以就可以复用上一篇关于长方形对象操作的一些代码,提高了代码复用率。

利用私有指针实现继承

在结构体内部增加一个私有指针成员,这个私有成员可以达到扩展属性的作用,比如以上的Rect结构体设计为:

typedef struct _Rect
{

 char *object_name;
 int length;
 int width;
 voidprivate
}Rect, *pRect;

这个private指针可以在创建对象的时候与其它拓展属性做绑定。比如:

想要拓展的数据为:


带拓展属性的对象创建函数:


显然,使用私有指针也是可以实现继承的一种方式。

不过对于本例来说,使用私有指针来做继承似乎弄得有点混乱,因为长方形的属性大致就是只有长、宽,加了个高之后就不叫长方形了。

这个例子不太适合做演示,越演示越乱。。就不继续演示下去了。我们大概知道有这样一种方法就可以。

结构体里包含一个私有指针成员在很多大牛的代码中经常都有看到,尽管可能不是实现对象继承,所以应尽量掌握。


多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

比如关于多态的C++的例子(该C++代码来自菜鸟教程):

左右滑动查看全部代码>>>

#include  
using namespace std;

// 基类  
class Shape 
{

   protected:
      int width, height;
   public:
      Shape( int a=0int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      
{
         cout << "Parent class area" <<endl;
         return 0;
      }
};

// 派生类Rectangle
class Rectangle: public Shape
{
   public:
      Rectangle( int a=0int b=0):Shape(a, b) { }
      int area ()
      

         cout << "Rectangle class area" <<endl;
         return (width * height); 
      }
};

// 派生类Triangle
class Triangle: public Shape
{
   public:
      Triangle( int a=0int b=0):Shape(a, b) { }
      int area ()
      

         cout << "Triangle class area" <<endl;
         return (width * height / 2); 
      }
};

// 程序的主函数
int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();
 
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
   
   return 0;
}

编译、运行结果为:


代码中用到了一个关键字:virtual,这是C++的关键字。基类中用virtual关键字修饰的函数叫做虚函数

这虚函数有点像弱定义的感觉,先定义一个弱的/虚的函数,其它地方再定义同名的真的函数,实际用的是真的函数。

该例中,在派生类中重新定义基类中定义的虚函数area时,会告诉编译器不要静态链接到该函数,而是根据所调用的对象类型来选择调用真正的函数。

假如这个例子中不使用virtual来修饰基类中的area函数,则上例输出结果则为:


显然,如果没有virtual来修饰的话,用到的都是基类中的area。

本篇笔记我们还需要知道一个知识:虚函数表。具体介绍如(图片截图自百度百科):

本篇笔记关于C++相关知识的就不再拓展,感兴趣的朋友可自行查资料进行学习。下面来看看C语言中怎么来实现上诉的例子:

C语言多态实例分析

这一节我们用C语言来实现上述例子的功能。下面看具体实现:

1、虚函数表

首先,我们可以使用函数指针来模拟C++的虚函数表:

/* 模拟C++的虚函数表 */
typedef struct _Ops
{

 int (*area)(void);
}Ops;

2、基类Shape:

/* 基类 */  
typedef struct _Shape 
{

 Ops ops;
 int width;
 int height;
}Shape;

3、派生类Rectangle、Triangle

/* 派生类Rectangle */
typedef struct _Rectangle
{

 Shape shape;
 char rectangle_name[20];
}Rectangle;

/* 派生类Triangle */
typedef struct _Triangle
{

 Shape shape;
 char triangle_name[20];
}Triangle;

4、两个派生类对应的area函数

/* Rectangle的area函数 */
int rectangle_area(void)
{
 printf("Rectangle class area\n");
}

/* Triangle的area函数 */
int triangle_area(void)
{
 printf("Triangle class area\n");
}

5、主函数/测试函数

左右滑动查看全部代码>>>

/* 主函数 */
int main(void)
{
 Rectangle rectangle;
 memset(&rectangle, 0sizeof(Rectangle));
 rectangle.shape.ops.area = rectangle_area; /* 与自己的area函数做绑定 */

 Triangle triangle;
 memset(&triangle, 0sizeof(Triangle));
 triangle.shape.ops.area = triangle_area; /* 与自己的area函数做绑定 */

 Shape *shape;

 shape = (Shape*)&rectangle;
 shape->ops.area();

 shape = (Shape*)▵
 shape->ops.area();
 
 return 0;
}

编译、运行结果为:


与C++例子中得到的结果是一样的。即父类指针shape来操作两个子类时,使用相同的接口时调用了不同的函数:


以上实现了简单的多态的功能。

这个例子中我们的操作函数(虚函数)只有一个,即area函数。

假如有多个操作函数,我们可以再建个结构体变量(函数表)把这些函数再包一层,这样会更清晰些。

在这个例子中,有如下对应关系:


因为这里只有一个操作函数,所以就没有建立一个函数表来包装一层了。我们可以再加一个函数表,如:


有多个函数的话,就更有必要构建一个函数表了:

总结

C语言并不是面向对象的语言,要想完全实现与C++一样的一些面向对象的特性会比较难,但是在嵌入式开发过程中,C语言又应用广泛,而在大型项目中,一个好的软件框架可以帮助我们更有效的开发,所以面对对象的思想就显得极其重要了。

推荐阅读:


嵌入式编程专辑
Linux 学习专辑
C/C++编程专辑

关注微信公众号『技术让梦想更伟大』,后台回复“m”查看更多内容,回复“加群”加入技术交流群。

长按前往图中包含的公众号关注

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报