一、简介

C语言是面向过程的一门语言,C++是面向对象,这个特性早已深入人心。但是嵌入式底层以及大型操作系统等却都是用C开发的。如果只用面向过程去开发这类大型软件,肯定行不通。

因此,C语言也可以用面向对象的思维去开发。例如现在的STM32的HAL库就在向着这个趋势进发。

二、 C语言实现封装

大部分使用 C 语言进行开发的工程师,在接触更高级的编程语言之前,都认为 C 语言是面向过程的。确实,对于一些小规模的应用程序,C 语言一般都被用作面向过程编程。例如:单片机应用程序开发。

但是,如果是使用 C 语言开发一些规模较大的软件时,就必须用面向对象的思想去考虑和设计整个软件框架了。例如:嵌入式Linux操作系统。

嵌入式Linux操作系统虽然是使用 C 语言作为主要的编写语言,但里面的设计大部分都使用了面向对象的编程思想。很多单片机工程师或者嵌入式Linux驱动初学者,觉得入门困难,很大一部分原因就是,他们还停留在单片机那种面向过程的思维模式上面。

编程语言只是一种工具,编程思想才是用好这个工具的关键。C 语言只是工具,而面向对象是一种编程思想,用来指导我们如何用好 C 语言。

接下来,我们将尝试使用 C 语言进行面向对象程序开发,务求使用 C 语言实现面向对象的一些基本特性。

首先,我们先来说说封装。

封装就是把一个抽象事物的属性和属性的操作函数打包在一起,外界的模块只能通过这个抽象事物对外提供的函数接口,对其属性进行访问。在C++或其他高级语言中,封装通常被称作“类”。而 C 语言一般使用结构体对事物进行封装。

接下来,我们先看两段代码,这两段代码主要声明和定义了一个坐标类对象,以及其坐标属性,还提供坐标属性的操作函数。

头文件 coordinate.h

#ifndef __COORDINATE_H_
#define __COORDINATE_H_//声明一个位置类,属性为坐标x,y
typedef struct coordinate{short int x;short int y;
}COORDINATE_T,*P_COORDINATE_T;extern P_COORDINATE_T coordinate_create(short int x,short int y);
extern void coordinate_destroy(P_COORDINATE_T p_coordinate);
extern void coordinate_moveby(P_COORDINATE_T p_coordinate,short int dx,short int dy);
extern short int coordinate_get_x(P_COORDINATE_T p_coordinate);
extern short int coordinate_get_y(P_COORDINATE_T p_coordinate);
extern void coordinate_test_function(void);
#endif // !__COORDINATE_H_

源文件 coordinate.c


#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "inc/coordinate.h"//创建一个coordinate对象
P_COORDINATE_T coordinate_create(short int x,short int y)
{if((x < 0) || (y < 0)){printf("coordinate creat error! x or y can not be less than zero \n");return NULL;}P_COORDINATE_T p_coordiante = NULL;p_coordiante = (P_COORDINATE_T)malloc(sizeof(COORDINATE_T));if(NULL != p_coordiante){p_coordiante->x = x;p_coordiante->y = y;        }else printf("coordinate malloc error! \n");return p_coordiante;
}//销毁一个coordinate对象
void coordinate_destroy(P_COORDINATE_T p_coordiante)
{if(NULL != p_coordiante){free(p_coordiante);p_coordiante = NULL;}
}//修改coordinate的属性值
void coordinate_moveby(P_COORDINATE_T p_coordiante,short int dx,short int dy)
{if(NULL != p_coordiante){p_coordiante->x += dx;p_coordiante->y += dy;}
}//获取coordinate的属性值x
short int coordinate_get_x(P_COORDINATE_T p_coordiante)
{  return (NULL != p_coordiante) ? p_coordiante->x : -1;
}//获取coordinate的属性值y
short int coordinate_get_y(P_COORDINATE_T p_coordiante)
{return (NULL != p_coordiante) ? p_coordiante->y : -1;
}

代码比较简单,在头文件 coordinate.h里面,通过结构体封装了一个coordinate类,里面有两个坐标属性 x 和 y 。

coordinate_create 函数主要用于创建一个 P_COORDINATE_T 类型的对象,并为其分配内存空间,内存分配成功后,设置两个坐标属性的初始值,最后返回申请成功的对象指针。

coordinate_destroy  主要是释放对象之前申请的内存空间,然后把对象指针重置为NULL。

其他的操作函数,主要是对类对象的属性进行操作,比如获取 x 和 y 的属性值,重置坐标的属性值。

以下是测试函数,在主函数中调用,即可测试类coordinate对外提供的接口。

void coordinate_test_function(void)
{P_COORDINATE_T p_coordiante_1 = NULL;P_COORDINATE_T p_coordiante_2 = NULL;p_coordiante_1 = (P_COORDINATE_T)coordinate_create(100,200);p_coordiante_2 = (P_COORDINATE_T)coordinate_create(10,20);if((NULL == p_coordiante_1) || (NULL == p_coordiante_2)){printf("p_coordiante_1 or p_coordiante_2 create error! \n");return;}printf("p_coordiante_1 x = %d, y = %d \n",coordinate_get_x(p_coordiante_1), coordinate_get_y(p_coordiante_1));printf("p_coordiante_2 x = %d, y = %d \n",coordinate_get_x(p_coordiante_2), coordinate_get_y(p_coordiante_2));coordinate_moveby(p_coordiante_1,50,50);coordinate_moveby(p_coordiante_2,50,50);printf("after moveby p_coordiante_1 x = %d, y = %d \n",coordinate_get_x(p_coordiante_1), coordinate_get_y(p_coordiante_1));printf("after moveby p_coordiante_2 x = %d, y = %d \n",coordinate_get_x(p_coordiante_2), coordinate_get_y(p_coordiante_2));coordinate_destroy(p_coordiante_1);coordinate_destroy(p_coordiante_2);
}

测试代码比较简单,主要是创建了两个 P_COORDINATE_T 类型的对象,然后打印其坐标初始值,再通过对外提供的函数修改其坐标值,然后再打印出来,最后销毁之前创建的对象。测试函数运行后,结果如下所示:


p_coordiante_1 x = 100, y = 200
p_coordiante_2 x = 10, y = 20
after moveby p_coordiante_1 x = 150, y = 250
after moveby p_coordiante_2 x = 60, y = 70

从上述代码可以看出,使用结构体可以很好地对数据进行封装,并且需要通过指定的操作函数对结构体内的数据进行访问。

每个操作函数的第一个参数是对象本身的指针,通过这个指针去访问具体对象里面的属性。这是因为在 C 语言中不存在像 C++ 语言那样的 this 指针,所以我们只能显式地通过函数传参的方式,让函数内部可以访问对象实例的其他成员。

对于对象属性的各种操作函数,还可以使用函数指针的方式,放入结构体内进行封装。但为了便于理解,本文并没有采用这种方法。

源码下载地址:

https://github.com/embediot/my_program_test

三、C语言实现继承

上一篇文章主要讲述了 C 语言面向对象编程 – 封装的简单概念和实现。

本篇文章继续来讨论一下,如何使用 C 语言实现面向对象编程的一个重要特性:继承

继承就是基于一个已有的类(一般称作父类或基类),再去重新声明或创建一个新的类,这个类可以称为子类或派生类。子类或派生类可以访问父类的数据和函数,然后子类里面又添加了自己的属性和数据。在 C 语言里面,可以通过结构体嵌套的方式去实现类的单继承(暂不考虑多重继承),但有一点注意事项,就是在结构体嵌套时,父类对象需要放在结构体成员的第一个位置。

现在,我们基于已有的 coordinate 类作为父类,再重新定义一个 rectangle 派生类。在上一篇文章代码的基础上,我们修改一下父类 coordinate,把操作函数通过函数指针的方式封装在结构体内,让对象的封装程度进一步提高。修改后的父类coordinate代码,如下所示:

头文件 coordinate.h

#ifndef __COORDINATE_H_
#define __COORDINATE_H_//声明一个位置类,属性为坐标x,y,提供属性操作函数
typedef struct coordinate {short int x;short int y;void (*moveby)(struct coordinate *p_coordinate,short int dx,short int dy);short int (*get_x)(struct coordinate *p_coordinate);short int (*get_y)(struct coordinate *p_coordinate);
}COORDINATE_T,*P_COORDINATE_T;extern void coordinate_init(P_COORDINATE_T p_coordinate,short int x,short int y);
extern void coordinate_uninit(P_COORDINATE_T p_coordinate);#endif // !__COORDINATE_H_

在头文件 coordinate.h 里,声明一个位置类,类里面提供了坐标属性 x 和 y,还提供了属性的操作函数指针。头文件对外提供 coordinate_init  和  coordinate_uninit  两个函数,用来初始化对象和解除初始化。

源文件 coordinate.c

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "inc/coordinate.h"//修改coordinate的属性值
static void coordinate_moveby(struct coordinate *p_coordiante,short int dx,short int dy)
{if(NULL != p_coordiante){p_coordiante->x += dx;p_coordiante->y += dy;}
}//获取coordinate的属性值x
static short int coordinate_get_x(struct coordinate *p_coordiante)
{return (NULL != p_coordiante) ? p_coordiante->x : -1;
}//获取coordinate的属性值y
static short int coordinate_get_y(struct coordinate *p_coordiante)
{return (NULL != p_coordiante) ? p_coordiante->y : -1;
}//创建一个coordinate对象
void coordinate_init(P_COORDINATE_T p_coordinate,short int x,short int y)
{if((x < 0) || (y < 0) || (NULL == p_coordinate)){printf("coordinate create error! x or y can not be less than zero \n");return;}p_coordinate->x = x;p_coordinate->y = y;p_coordinate->moveby = coordinate_moveby;p_coordinate->get_x = coordinate_get_x;p_coordinate->get_y = coordinate_get_y;
}//销毁一个coordinate对象
void coordinate_uninit(P_COORDINATE_T p_coordinate)
{if(NULL != p_coordinate){p_coordinate->x = -1;p_coordinate->y = -1;p_coordinate->moveby = NULL;p_coordinate->get_x = NULL;p_coordinate->get_y = NULL;}
}

在源文件 coordinate.c 里,属性的操作函数都使用 static 进行声明,只能在该源文件调用函数,不允许外部调用。在函数 coordinate_init 中,主要进行了属性赋值,并注册操作函数指针,后面可以直接通过函数指针对操作函数进行调用。在函数 coordinate_uninit 中,主要是清除各个属性的赋值。

至此,整个父类 coordinate 修改完成,父类把属性和属性的操作函数都封装在结构体内,其封装程度已经比较高,外部不能直接调用父类的属性操作函数,必须通过函数指针的方式进行调用。

接下来,我们基于父类 coordinate ,重新声明一个子类 rectangle ,子类在头文件中的声明,如下所示:

头文件 rectangle.h

#ifndef __RECTANGLE_H_
#define __RECTANGLE_H_
#include "coordinate.h"    //包含基类的接口//声明一个rectangle类,继承coordinate类
typedef struct rectangle {COORDINATE_T coordinate; //父类,必须放在首位unsigned short width;unsigned short height;
}RECTANGLE_T,*P_RECTANGLE_T;extern P_RECTANGLE_T rectangle_create(short int x,short int y,unsigned short width,unsigned short height);
extern void rectangle_destroy(P_RECTANGLE_T p_rectangle);
extern void rectangle_test_function(void);#endif // !__RECTANGLE_H_

源文件 rectangle.c

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "inc/rectangle.h"//创建一个rectangle类对象
P_RECTANGLE_T rectangle_create(short int x,short int y,unsigned short width,unsigned short height)
{P_RECTANGLE_T p_rectangle = NULL;p_rectangle = (P_RECTANGLE_T)malloc(sizeof(RECTANGLE_T));if(NULL != p_rectangle){       p_rectangle->width = width;p_rectangle->height = height;coordinate_init(&(p_rectangle->coordinate),x,y);}else printf("rectangle create error! \n");return p_rectangle;
}//销毁一个rectangle类对象
void rectangle_destroy(P_RECTANGLE_T p_rectangle)
{coordinate_uninit(&(p_rectangle->coordinate));if(NULL != p_rectangle){free(p_rectangle);p_rectangle = NULL;}
}

在头文件 rectangle.h 里面,通过include包含了父类coordinate的接口,并创建了一个新的结构体,用于声明一个 rectangle 类,这个结构体把父类 coordinate 放在了第一个成员的位置,同时新增了自己的两个属性,宽度width和高度height。

rectangle_create 函数用于创建一个 P_RECTANGLE_T 类型的对象,并为其分配内存空间。分配成功后,对调用父类 coordinate_init函数,对父类的各种属性进行初始化,并同时对自身的属性 width 和 height 进行初始化,最后返回创建成功后的对象指针。

rectangle_destroy 用于父类对象属性的解除初始化,并为对象属性重新分配默认值,释放之前申请的内存空间,销毁 rectangle 对象。

从头文件 rectangle.h 和源文件 rectangle.c 可以看出,子类 rectangle 是基于其父类 coordinate 进行声明和构建的,因为矩形rectangle除了 width 和 height 属性外,还包含了坐标 x 和 y 属性。

把父类放在结构体成员的第一个位置,是由于结构体内存的连续性,可以很安全地进行强制类型转换。举个例子:假如一个函数要求传入的参数是 COORDINATE_T 类型,但可以通过强制类型转换,传入 RECTANGLE_T 类型的参数,具体的使用方法,可以查看以下测试函数。


void rectangle_test_function(void)
{P_RECTANGLE_T p_rectangle_1 = NULL;P_RECTANGLE_T p_rectangle_2 = NULL;//创建两个 P_RECTANGLE_T 类型的类对象p_rectangle_1 = (P_RECTANGLE_T)rectangle_create(0,0,150,150);p_rectangle_2 = (P_RECTANGLE_T)rectangle_create(200,200,500,500);if((NULL != p_rectangle_1) && (NULL != p_rectangle_2)){//打印出类对象的初始化属性,通过函数指针的方式来调用属性操作函数printf("p_rectangle_1,x = %d,y = %d,width = %d,height = %d \n",\p_rectangle_1->coordinate.get_x(&(p_rectangle_1->coordinate)), \p_rectangle_1->coordinate.get_y(&(p_rectangle_1->coordinate)), \p_rectangle_1->width,p_rectangle_1->height);printf("p_rectangle_2,x = %d,y = %d,width = %d,height = %d \n",    \p_rectangle_2->coordinate.get_x(&(p_rectangle_2->coordinate)),   \p_rectangle_2->coordinate.get_y(&(p_rectangle_2->coordinate)),   \p_rectangle_2->width,p_rectangle_2->height);//修改类对象的属性,注意这里有两种方式,1、通过强制类型转换修改。2、通过正常方式修改p_rectangle_1->coordinate.moveby((P_COORDINATE_T)p_rectangle_1, 50, 50);p_rectangle_2->coordinate.moveby(&(p_rectangle_2->coordinate), 50, 50);//再次打印出类对象的修改后的属性printf("after moveby, p_rectangle_1,x = %d,y = %d,width = %d,height = %d \n",\p_rectangle_1->coordinate.get_x(&(p_rectangle_1->coordinate)), \p_rectangle_1->coordinate.get_y(&(p_rectangle_1->coordinate)), \p_rectangle_1->width,p_rectangle_1->height);printf("after moveby, p_rectangle_2,x = %d,y = %d,width = %d,height = %d \n",    \p_rectangle_2->coordinate.get_x(&(p_rectangle_2->coordinate)),   \p_rectangle_2->coordinate.get_y(&(p_rectangle_2->coordinate)),   \p_rectangle_2->width,p_rectangle_2->height);}//销毁类对象rectangle_destroy(p_rectangle_1);rectangle_destroy(p_rectangle_2);
}

测试函数的运行效果,如下图所示:


p_rectangle_1,x = 0,y = 0,width = 150,height = 150
p_rectangle_2,x = 200,y = 200,width = 500,height = 500
after moveby, p_rectangle_1,x = 50,y = 50,width = 150,height = 150
after moveby, p_rectangle_2,x = 250,y = 250,width = 500,height = 500

通过上述代码的测试,可以总结出以下几点内容:

1、外部函数可以通过子类直接使用父类的各个成员,但只能通过子类结构体的第一个成员来访问。

2、父类放在子类结构体的第一个位置,由于结构体内存的连续性,因此可以通过强制类型转换来直接访问。

3、由于C语言结构体的特性,即使子类存在与父类同名的函数,父类的函数不会被子类的函数覆盖和重写,因此,子类与父类之间不存在函数重载。

源码下载地址:

https://github.com/embediot/my_program_test

看完了吗?看完还不赶紧点赞?

说明:本文是来自一位微信网友

c语言如何实现面向对象编程相关推荐

  1. 【Python】Python语言学习:面向对象编程,类和对象,封装、继承和多态

    这一周Python语言学习,记录如下. 01 面向对象编OOP 1.1 为什么学习和应用OOP? 1 OOP适合更加复杂的需求分析和项目开发. 2 OOP具有更强大的封装能力. 3 OOP相比于面向过 ...

  2. 零基础入门C 语言如何实现面向对象编程

    具体和抽象 具体:客观存在着的或在认识中反映出来的事物的整体,是具有多方面属性.特点.关系的统一: 抽象:从具体事物中被抽取出来的相对独立的各个方面.属性.关系等. 以 Person 为例:" ...

  3. 如何利用函数指针在c语言中实现面向对象编程

    面向过程编程 c语言,是一种函数流语言,看他的代码,大部分是call1(data), call2(data)类似的代码.所以他的主体思想是函数,把数据丢进去给函数执行. 面向对象编程 java语言,和 ...

  4. 面向对象程序设计c 语言,C 语言如何实现面向对象编程

    1. 具体和抽象 具体:客观存在着的或在认识中反映出来的事物的整体,是具有多方面属性.特点.关系的统一: 抽象:从具体事物中被抽取出来的相对独立的各个方面.属性.关系等. 以 Person 为例:&q ...

  5. 动态语言和静态语言的在面向对象编程上的差异

    对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法. 对于Python这样的动态语言来说,则不一定需要传入A ...

  6. python面向对象语言_Python语言基础之——面向对象编程

    1.类属性 1)类属性: 直接定义在类中的变量是类属性 类属性的值不会因为对象不同而不一样 2)对象属性 通过 self.属性名 = 值 定义在init函数中的属性 对象属性的值会因为对象不同而不一样 ...

  7. 利用c语言面向对象编程,用C语言程序实现面向对象编程

    [武汉PHP培训]一.类的封装实现:借用高焕堂的宏头文件,类很容易封装为如下的格式1.类的定义,其中 CLASS() 是 lw_oopc_kc.h 中定义的宏#include "lw_oop ...

  8. 七、Go 语言面向对象编程

    @Author:Runsen 在 Go 语言中没有类(Class)的概念,但这并不意味着 Go 语言不支持面向对象编程,毕竟面向对象只是一种编程思想.对于面向对象编程的支持Go 语言设计得非常简洁而优 ...

  9. Python|装饰器|执行时间|递归|动态属性|静态方法和类|继承和多态|isinstance类型判断|溢出|“魔法”方法|语言基础50课:学习记录(6)-函数的高级应用、面向对象编程、进阶及应用

    文章目录 系列目录 原项目地址: 第16课:函数的高级应用 装饰器(记录执行时间的例子) 递归调用 简单的总结 第17课:面向对象编程入门 类和对象 定义类 创建和使用对象 初始化方法 打印对象 面向 ...

最新文章

  1. Apriori算法进行关联分析(1)
  2. C# 开发圆角控件的具体实现
  3. docker设置镜像源 树莓派_树莓派上 Docker 的安装和使用
  4. java 十六进制转十进制_JAVA知识-分析JAVA中的重点和难点
  5. 深入深出Sigmoid与Softmax的血缘关系
  6. 怎么加载csv_python爬虫入门实战(四)!爬取动态加载的页面
  7. Nginx设置上传文件大小限制
  8. 杭电计算机组成原理教材答案,杭电计算机组成原理包建课后作业答案详解.doc...
  9. 【动态规划】P1018 线性dp:乘积最大
  10. threadpooltaskexecutor线程池使用_线程池的理解及使用
  11. mysql优化概述2
  12. 人口会一直增长下去吗_想要一直美下去?护肤雷区你避开了吗?
  13. matlab做多元统计分析,matlab多元统计分析
  14. Spring学习的书-夏昕(3)
  15. Android 求职简历
  16. 51单片机——串口通信
  17. 独立开发者能够从这位作家身上学些什么
  18. Filebeat日志采集
  19. SpringBoot项目resources下指定目录的所有文件下载到Centos服务器上,浏览器访问API后资源包直接下载本地磁盘,Java删除linux文件,zip文件打包
  20. rvz03支持服务器主板,占地面积超小的一款ITX机箱、银欣RVZ03小乌鸦3机箱 评测

热门文章

  1. Sublime Text 3技巧:支持GB2312和GBK编码
  2. python wmi_python中wmi介绍和使用
  3. FPT:又是借鉴Transformer!这次多方向融合特征金字塔 | ECCV 2020
  4. Ext树形结构——TreePanel
  5. 汕头市队赛 yyl杯1 T2
  6. 「题解」清华集训 2016 你的生命已如风中残烛
  7. Java JVM 读书笔记 1
  8. 2020新年伊始,我的新年计算机书单(持续更新)
  9. 个人对 Python 数据类型理解整理所得 以及深/浅copy
  10. 工业互联网:工业制造与互联网深度融合