文章目录
一.概述
流水灯是单片机入门学习的小项目,实现逻辑很简单。往往我们使用C语言面向过程的方法就可以很容易的实现,本文借助流水灯这个简单的场景来讲解面向对象的编程思想,并比较C和C++实现方法的区别。都说C语言是非面向对象的语言,这是不准确的,面向对象是一种编程思想,和使用什么语言无关,正如linux内核如此庞大的工程也是使用c实现的,并且其中使用了大量的面向对象的编程方法。因此只能说c语言并不原生支持面向对象编程,而c++原生支持面向对象编程,这就导致了使用c实现面向对象会更复杂一些,但完全可以实现,下面就结合代码分析比较:
二.C语言实现流水灯
1.程序源码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//定义流水灯的个数
#define N 7
typedef void (*pfunc_ledon_t)(int n); //定义ledon函数模板
typedef void (*pfunc_ledoff_t)(int n); //定义ledoff函数模板
//led亮操作函数
void ledon(int n)
{
printf("led%don\r\n", n);
}
//led灭操作函数
void ledoff(int n)
{
printf("led%doff\r\n", n);
}
//封装led类
struct led {
//led的属性
int led_num; //代表led的编号
//led的操作
pfunc_ledon_t vledon; //定义ledon操作函数
pfunc_ledoff_t vledoff; //定义ledoff操作函数
};
int main(void)
{
//实例化对象(创建结构体变量)
struct led myled[N];
//循环给N个led对象初始化
for (int i = 0; i < N; i++)
{
myled[i].led_num = i;
myled[i].vledon = ledon; //结构体函数指针变量指向具体的函数(实例化)
myled[i].vledoff = ledoff; //结构体函数指针变量指向具体的函数(实例化)
}
//使用对象调用成员函数的方法实现流水灯
while (1)
{
//使用for循环完成N中状态一次循环
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
if(i == j)myled[j].vledon(myled[j].led_num); //执行亮函数:只有第i个灯亮
else myled[j].vledoff(myled[j].led_num); //执行灭函数:其余灯都灭
}
printf("----------------\r\n");
Sleep(1000);
}
}
return 0;
}
2.执行结果
led0on
led1off
led2off
led3off
led4off
led5off
led6off
----------------
led0off
led1on
led2off
led3off
led4off
led5off
led6off
----------------
led0off
led1off
led2on
led3off
led4off
led5off
led6off
----------------
led0off
led1off
led2off
led3on
led4off
led5off
led6off
----------------
3.代码分析
上面的代码很简单,核心思想就是将led看作一个对象,这个对象包括一个属性(led的编号)和两个操作(ledon、ledoff)。然后使用struct结构体将属性和操作封装起来构成一个led类。有了类就可以实例化对象myled[0],myled[1]…myled[N-1];,实例化对象之后就需要对这个对象的属性和操作初始化,也就是给成员变量赋初值,将操作函数指向具体的实现函数,这是c和c++的最大不同(实际上在c中struct更像是一种打包的方式,并不考虑其中函数和变量之间的操作关系)。在对对象完成初始化之后就可以操作其属性和方法了:myled[j].vledon(myled[j].led_num); //执行亮灯函数
myled[j].vledoff(myled[j].led_num); //执行关灯函数
三.C++实现流水灯
1.程序源码
#include <iostream>
#include <string>
#include <vector>
#include "test.h"
#include <windows.h>
using namespace std;
//定义流水灯的个数
#define N 3
//定义一个led类
class led
{
public:
//默认构造函数
led()
{
led_num = 0; //led编号默认为0
}
//有参构造函数,初始化led编号
led(int my_led_num):led_num(my_led_num)
{
}
//对led对象的操作
void ledon(void)
{
cout << "led" << this->led_num << "on" << endl;
}
void ledoff(void)
{
cout << "led" << this->led_num << "off" << endl;
}
private:
//led的属性
int led_num; //代表led的编号
};
int main(void)
{
//定义对象指针数组
led* myled[N] = { NULL };
//循环创建N个led对象
for (int i = 0; i < N; i++)
{
myled[i] = new led(i);
}
//使用对象调用成员函数的方法实现流水灯
while (1)
{
//使用for循环完成N中状态一次循环
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
if(i == j)myled[j]->ledon(); //只有第i个灯亮
else myled[j]->ledoff(); //其余灯都灭
}
cout << "----------------" << endl;
Sleep(1000);
}
}
return 0;
}
2.执行结果
led0on
led1off
led2off
----------------
led0off
led1on
led2off
----------------
led0off
led1off
led2on
----------------
led0on
led1off
led2off
----------------
led0off
led1on
led2off
----------------
3.代码分析
可以看出c++的实现思路和c完全相同,也是创建led类,实例化对象,调用成员函数。但是c++实现起来更加简洁明了,这就是因为c++原生支持面向对象编程,在我们使用class创建类的时候,属性led_num和两个操作函数之间就自动建立了某种内在联系,并且我们可以将函数的实现定义在类中而不需要再像c一样指向具体的实现函数,后面使用对象执行操作就基本一致了。通过比较我们可以发现c和c++都可以很好的实现面向对象编程,但是很显然c++封装的更好些,且更加简洁明了,因此在我们应对更大规模的复杂工程时c++的优势更加能得以体现。
四.C实现面向对象的典型案例分析
1.linux中面向对象的编程方法
在linux中进行驱动编程的时候,会使用struct封装某个驱动类,并且定义好操作函数的接口形式,驱动对象的实例化操作以及结构体指针的实现方法由驱动开发人员去实现,也就是Linux内核提供了面向对象的封装方法并且规定了操作函数的参数列表及类型,开发人员只需要实例化其即可。
例如linux封装的文件操作类,其中一些属性和操作的需要开发人员在实际使用中去实例化它:
struct file_operations {
struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES
loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作
int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
int (*open) (struct inode *, struct file *); //打开
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); //关闭
int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据
int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据
int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
比如我们自定义一个file_operations结构体变量,并且去填充:
static const struct file_operations test_fops ={
.owner = THIS_MODULE, //惯例,直接写即可
.open = test_chrdev_open, //将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, //将来应用release释放这个设备时实际调用的
.read = test_chrdev_read, //将来应用read读这个设备时实际调用的
.write = test_chrdev_write, //将来应用write写这个设备时实际调用的
....................................
};
我们需要去实例化 test_chrdev_open,test_chrdev_release, test_chrdev_read,test_chrdev_write等这些函数,然后系统会将我们的这些操作传递给内核去调用。比如实现test_chrdev_write函数如下:
static ssize_t test_chrdev_write(struct file*file,const char __user *ubuf,size_t count,loff_t *ppos)
{
int ret = -1;
printfk("-------------------");
....................................
....................................
}
2.华为LiteOS中面向对象编程思想
LiteOS中支持linux,liteos,macos,novos,ucos_ii等操做系统内核,那么LiteOS是怎样同时支持这这些不同操作系统内核的呢?这就是面向对象思想的封装,一套接口可以有不同的实现,不同的操作系统对同一个函数可能有不同的实现方法,那么在LiteOS中就定义了一个公共接口的结构体,在实例化对象的时候,可以将接口函数(结构体成员函数)指向不同操作系统的实现函数,这样就实现了对不同操作系统的调用。在LiteOS中osal文件中就是封装了公共接口。简单展示如下:
tyepdef struct{
//task function needed
void * (*task_create)(canst char *name,int (*task_entry)(void *args),void *args,int stack_size,void *stack,int prior);
int (*task_kill)(void *task);
void (*task_exit)();
void (*task_sleep)(int ms);
//mutex function needed
bool_t (*mutex_create)(osal_mutex_t *mutex);
bool_t (*mutex_lock)(osal_mutex_t mutex);
bool_t (*mutex_unlock)(osal_mutex_t mutex);
bool_t (*mutex_del)(osal_mutex_t mutex);
//sem function needed
....................................
....................................
}tag_os_ops;
每个不同的os实现上述函数的具体方法不同,但是可以通过tag_os_ops实例化出不同os的对象,并在具体的对象中实例化出具体的操作函数(明确函数指针的指向)。
3.STM32 HAL库中面向对象编程
例如GPIO的初始化:
tyepdef struct{
uint32_t Pin; //pin引脚
uint32_t Mode; //模式
uint32_t Pull; //上下拉属性
uint32_t Speed; //引脚速度
uint32_t Alternate;
}GPIO_InitTypeDef;
以上是HAL库封装好的类属性。
在GPIO初始化的时候就会定义实例化对象并设置这些属性:
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //实例化一个操作对象
/* Configure GPIO Pin :PC15 */
GPIO_InitStruct.pin = GPIO_PIN_15; //设置对象属性
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //设置对象属性
....................................
....................................
HAL_GPIO_Init(GPIOC,&GPIO_InitStruct);
}
最后调用HAL_GPIO_Init是非面向对象方法,因为HAL_GPIO_Init不是类的操作函数,这是因为C本身一种面向对象语言,强行面向对象反而会更复杂,这是一种折中方案。
五.总结
再次强调面向对象是一种编程思想,和使用什么语言无关。使用c和c++都可以很好的实现面向对象编程,只是实现的复杂度不同。因此c++更适合应用于解决复杂工程项目中。