嵌入式C语言自我修养:C语言的面向对象编程思想
⭐关联知识点:C和C++的区别
代码复用与分层思想
什么是代码复用呢?
(1)函数级代码复用:定义一个函数实现某个功能,所有的程序都可以调用这个函数,不用自己再单独实现一遍,函数级的代码复用。
(2)将一些通用的函数打包封装成库,并引出API供程序调用,实现了库级的代码复用;
(3)将一些类似的应用程序抽象成应用骨架,然后进一步迭代成框架,实现框架级的代码复用;
如果从代码复用的角度看操作系统,操作系统其实也是对任务调度、任务间通信等功能实现,并引出API供应用程序调用,相当于实现了操作系统级的代码复用。
通常将要复用的具有某种特定功能的代码封装成一个模块,各个模块之间相互独立,使用的时候可以以模块为单位集成到系统中。随着系统越来越复杂,集成的模块越来越多,模块之间会产生依赖关系。为了便于系统的管理和维护,又开始出现分层思想,可以把一个计算机系统分为应用层、系统层、硬件层。
在Linux内核中,往往包含很多模块和子系统,如文件系统、内存管理子系统、进程调度等。每一个模块或子系统也包含着分层的思想。如Linux文件系统,就包括虚拟文件系统VFS和各种类型的文件系统Ext、Fat、NFS等。底层的磁盘、文件系统、虚拟文件系统及应用层的API读写接口也可以实现分层。
一个系统通过分层设计,各层实现各自的功能,各层之间通过接口通信。每一层都是对其下面一层的封装,并留出API,为上一层提供服务,实现代码复用。
面向对象编程基础
面向过程编程中函数是程序的基本单元,可以把一个问题分解成多个步骤来解决,每
一步或每一个功能都可以使用函数来实现。
在面向对象编程中,对象是程序的基本单元,对象是类的实例化,类则是对客观事物抽象而成的一种数据类型,其内部包括属性和方法。
面向对象编程则侧重于将问题抽象、封装成一个个类,然后通过继承来实现代码复用,面向对象编程一般用于复杂系统的软件分层和架构设计。
Linux内核中的OOP思想:封装
内核中的很多子系统、模块在实现过程中处处体现了面向对象编程思想。
类的C语言模拟实现
C语言中没有class关键字,但是可以使用struct模拟一个类,C++类中的属性类似结构体的各个成员。虽然结构体内部不能像类一样可以直接定义函数,但可以在结构体中内嵌函数指针来模拟类中的方法。
struct animal{
int age;
int weight;
void (*fp)(void);
}
如果一个结构体中需要内嵌多个函数指针,可以把这些函数指针进一步封装到一个结构体内。
struct func operations{
void(*fp1)(void);
void (*fp2)(void);
void (*fp3)(void);
void (*fp4)(void);
}
struct animal{
int age;
int weight;
struct func operations fp;
}
通过把函数封装在结构体中,然后嵌入该结构体,可以把一个类的属性和方法都封装在一个结构体里。
如何继承?
struct cat{
struct animal *p;
struct animal ani;
char sex;
void (*eat)(void);
}
C语言可以通过在结构体中内嵌另一个结构体或结构体指针来模拟类的继承。
在结构体类型cat里内嵌结构体类型animal,此时结构体cat就相当于模拟了一个子类cat,而结构体animal相当于一个父类。
C语言中,内嵌结构体或内嵌指向结构体的指针,可以看作对“继承”的模拟。
链表的抽象与封装
Linux内核中为了实现对链表操作的代码复用,定义了一个通用的链表及相关操作.
struct list head{
struct list head *next, *prev;
}
void INIT LIST HEAD(struct list head *list);
int list empty(const struct list head *head);
void list add(struct list head *new, struct list head *head);
void list del(struct list head *entry);
void list replace(struct list head *old,struct list head *new),
void list move(struct list head *list, struct list head *head);
如果想复用Linux内核中的通用链表及相关操作,就可以通过内嵌结构体来继承list_head的属性和方法。
struct my_list node{
int data;
struct list head list;
}
设备管理模型
Linux如何管理和维护这些设备的信息呢?
Linux的设备管理模型说起。Linux内核中定义了一个非常重要的结构体类型。
struct kobject{
const char *name;
struct list head entry;
struct kobject *parent;
struct kernfs node *sd;
struct kset *kset;
struct kobj_type *ktype;
struct kref kref;
unsigned int state initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state add uevent sent:1;
unsigned int state_remove_uevent sent:1;
unsigned int uevent suppress:1;
}
所有设备在系统中的树结构,kobject结构体用来表示Linux系统中的一个设备,相同类型的kobject通过其内嵌的list_head链成一个链表,然后使用另外一个结构体kset来指向和管理这个列表。
以后再说。
以字符设备为例,我们可以看到字符设备结构体cdev在内核中的定义。
struct cdev {
struct kobject kobj; // 内嵌kobject结构体
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
在结构类型cdev中,通过内嵌结构体kobject来模拟对基类kobject的继承,字符设备的注册与注销,都可以通过继承基类的kobject_add()/kobject_del()方法来完成。与此同时,字符设备在继承基类的基础上,也完成了自己的扩展:实 现 了 自 己的read/write/open/close接口,并把这些接口以函数指针的形式封装在结构体file_operations中。
struct file_operations {
struct module *owner;
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
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 *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
...
};
不同的字符设备,会根据自己的硬件逻辑实现各自的read()、write()函数,并注册到系统中。当用户程序读写这些字符设备时,通过这些接口,就可以找到对应设备的读写函数,对字符设备进行打开、读写、关闭等各种操作。
总线设备模型
Linux每一个设备都要有一个对应的驱动程序,否则就
无法对这个设备进行读写。每一个字符设备都对应的字符设备驱动程序;每一个块设备都有对应的块设备驱动程序。对于一些总线型的设备,如鼠标、键盘、U盘等USB设备,设备通信是按照USB标准协议进行的。
Linux系统为了实现最大化的驱动代码复用,设计了设备-总线-驱动模型:用总线提供的一些方法来管理设备的插拔信息,所有的设备都挂到总线上,总线会根据设备的类型选择合适的驱动与之匹配。通过这种设计,相同类型的设备可以共享同一个总线驱动,实现了驱动级的代码复用。
总线设备模型相关的3个结构体分别为device、bus、driver,它们可以看成基类kobject的子类。
struct device {
struct device *parent; // 父设备
struct device_private *p; // 内嵌私有数据
struct kobject kobj; // 内嵌kobject结构体
const struct device_type *type; // 设备类型
struct bus_type *bus; // 总线类型
struct device_driver *driver; // 驱动程序
void *platform_data;
void *driver_data;
dev_t devt;
u32 id;
struct klist_node knode_class;
struct class *class;
void (*release)(struct device *dev);
};
与字符设备cdev类似,在结构体类型device的定义里,也通过内嵌kobject结构体来完成对基类kobject的继承。但其与字符设备不同之处在于,device结构体内部还内嵌了bus_type和device_driver,用来表示其挂载的总线和与其匹配的设备驱动。
device结构体可以看成一个抽象类,我们无法使用它去创建一个
具体的设备。其他具体的总线型设备,如USB设备、I2C设备等可以通
过内嵌device结构体来完成对device类属性和方法的继承。
struct usb_device {
int devnum;
char devpath[16];
u32 route;
enum usb_device_state state;
enum usb_device_speed speed;
struct usb_tt *tt;
int ttport;
unsigned int toggle[2];
struct usb_device *parent;
struct usb_bus *bus;
struct usb_host_endpoint ep0;
struct device dev; // 内嵌device 结构体
...
};
Linux内核中的OOP思想:继承
继承与私有指针
除了内嵌结构体,C语言还可以有其他方法来模拟类的继承,如通过私有指针。我们可以把使用结构体类型定义各个不同的结构体变量,也可以看作继承,各个结构体变量就是子类,然后各个子类通过私有指针扩展各自的属性或方法。这种继承方法主要适用于父类和子类差别不大的场合。
如Linux内核中的网卡设备,不同厂家的网卡、不同速度的网卡,以及相同厂家不同品牌的网卡,它们的读写操作基本上都是一样的,都通过标准的网络协议传输数据,唯一不同的就是不同网卡之间存在一些差异,如I/O寄存器、I/O内存地址、中断号等硬件资源不相同。
遇到可以将各个网卡一些相同的属性抽取出来,构建一个通用的结构体net_device,然后通过一个私有指针,指向每个网卡各自不同的属性和方法,这样可以最大程度地实现代码复用。
struct net_device {
char name[IFNAMSIZ]; // 网络设备名称
const struct net_device_ops *netdev_ops; // 网络设备操作
const struct ethtool_ops *ethool_ops; // Ethtool操作
void *ml_priv; // 中间层私有数据
struct device dev; // 内嵌device结构体
};
当我们使用该结构体类型定义不同的变量来表示不同型号的网卡设备时,这个私有指针就会指向各个网卡自身扩展的一些属性。
继承与抽象类
含有纯虚函数的类,称之为抽象类。抽象类不能被实例化,实例化也没有意义,如animal类,它只能被子类继承。抽象类的作用,主要就是实现分层:实现抽象层。当父类和子类之间的差别太大时,很难通过继承来实现代码复用,如生物类和狗
类,我们可以在它们之间添加一个animal抽象类。抽象类主要用来管理父类和子类的继承关系,通过分层来提高代码的复用性。
如上面设备模型中的device类,位于kobj类和usb_device类之间,通过分层,
可以更好地实现代码复用。
Linux内核中的OOP思想:多态
可以使用C语言来模拟多态:如果把使用同一个结构体类型定义的不同结构体变量看成这个结构体类型的各个子类,那么在初始化各个结构体变量时,如果基类是抽象类,类成员中包含纯虚函数,则为函数指针成员赋予不同的具体函数,然后通过指针调用各个结构体变量的具体函数即可实现多态。
#include <stdio.h>
// 文件操作结构体
typedef struct file_operation {
void(*read)(void); // 读取操作
void(*write)(void); // 写入操作
} FileOperation;
// 文件系统结构体
typedef struct file_system {
char name[20]; // 文件系统名称
FileOperation fops; // 文件操作
} FileSystem;
// 扩展文件系统的读操作
void ext_read(void) {
printf("ext read...\n");
}
// 扩展文件系统的写操作
void ext_write(void) {
printf("ext write...\n");
}
// FAT文件系统的读操作
void fat_read(void) {
printf("fat read...\n");
}
// FAT文件系统的写操作
void fat_write(void) {
printf("fat write...\n");
}
int main(void) {
// 初始化扩展文件系统
FileSystem ext = {"ext3", {ext_read, ext_write}};
// 初始化FAT文件系统
FileSystem fat = {"fat32", {fat_read, fat_write}};
// 文件系统指针
FileSystem *fs_ptr;
// 指向扩展文件系统
fs_ptr = &ext;
fs_ptr->fops.read(); // 调用扩展文件系统的读操作
// 指向FAT文件系统
fs_ptr = &fat;
fs_ptr->fops.read(); // 调用FAT文件系统的读操作
return 0;
}