经常看到关于OO编程的讨论,C++, Java, C#...还有最近很流行的动态语言Python,Ruby等,但很少看到有C的份。在我看来,OO编程的核心是OO的思想,用什么语言倒是其次。但是,不可否认,那些专门为OO编程设计的语言可以比较方便和自然地表达OO思想,有些语言甚至强制使用OO特性。
C,作为最贴近底层的高级语言,拥有简洁的语法和直接内存操作能力(指针),大量运用于系统级编程,如操作系统内核,驱动程序等。而在嵌入式系统中,由于资源有限等因素,更倾向于用C编程。
C虽然在语言特性上并没有体现OO特性,但是依然可以通过各种编程技巧来体现OO的思想。由于C的高度自由的特点,在OO编程方面还能体现有别于其他语言的特殊韵味。
OO Programing in C is not only POSSIBLE, but also PRACTICAL. --------------------------------------------------------------------------------
“class“是很多OO编程语言里的关键字,它来源于OO鼻祖Smalltalk。class(类),是对一群有相同特性的对象的抽象概括,对象称为类的实例。在class里面可以存放有状态(变量),行为(函数/方法)....有关OO概念、方法的文章太多了,不再啰嗦。在C里面,唯一可以实现自定义类型的是struct,struct是C的OO编程最重要的工具。
一个最常见的技巧,就是用struct来"仿真"class: 在struct里面放入变量,函数指针,嵌入其他struct等。
以下例子摘自我最近刚开发完成的一个USB Firmware项目:
- struct usb_device;
- struct usb_ctl;
-
- struct usb_iobuf {
- int len;
- unsigned char buf[USBEPFIFO_SIZE];
- };
-
- struct usb_endpoint { int type;
- int qlen;
-
- xQueueHandle lock;
- xQueueHandle q;
-
- int idx;
- int epx;
- int cfg;
- int bank;
- int txCount; void (*ep_process) (struct usb_device *dev, struct usb_endpoint *ep, xISRStatus *pxMessage);
- };
-
- struct usb_descriptor {
- int type;
- int idx;
- int size;
- void * data;
- struct list_head list;
- };
-
- struct usb_deviceOps {
- int (*init)(struct usb_device *dev);
- int (*reset)(struct usb_device *dev);
- int (*switch_in)(struct usb_device *dev);
- int (*switch_out)(struct usb_device *dev);
- void (*class_interface_req)(struct usb_device *dev, xUSB_REQUEST *pxRequest); int (*ctl_data_comp)(struct usb_device *dev, xCONTROL_MESSAGE *pxMessage);
- };
-
- struct usb_ctlOps {
- void (*ctl_transmit_null)(struct usb_ctl *ctl);
- void (*ctl_send_stall)(struct usb_ctl *ctl);
- void (*ctl_reset_ep0)(struct usb_ctl *ctl);
- void (*ctl_detach_usb)(struct usb_ctl *ctl);
- void (*ctl_attach_usb)(struct usb_ctl *ctl);
- void (*ctl_send_data)(struct usb_ctl *ctl, unsigned char *data,
- int req_len,
- int send_len,
- int is_des);
- };
-
-
- struct usb_ctl {
- int addr;
- int conf;
- eDRIVER_STATE state;
- xCONTROL_MESSAGE tx;
- xCONTROL_MESSAGE rx;
- struct ubufm *bufmn;
- int prio;
- xTaskHandle task_handle;
- struct usb_ctlOps *ctlOps;
- };
-
- struct usb_device {
- char name[16];
- struct usb_deviceOps *ops;
-
- struct usb_ctl *ctl;
- struct list_head desc_list;
- struct usb_endpoint *ep[MAX_ENDPOINTS];
- int active;
- xQueueHandle ready;
- void *private;
- struct list_head list;
- };
在这个例子,我用struct分别描述了USB设备,USB控制通道,USB端点,USB描述符和USB缓冲区对象。USB设备对象包含了若干个USB端点,一个USB控制通道指针,一个USB描述符表的表头(指向若干个USB描述符),和一个USB缓冲区管理对象。而且,USB设备对象还包含了name属性,一个由USB Framework调用的回调函数集,还有一个用于连接其他USB设备的链表节点。
值得一提的是,USB设备对象中有一个void *private 成员,可以指向任何数据。实际上在我的程序里,我实现了usb-serial和usb-mass-storage两个USB设备,对于usb-serial对象,private我弃之不用,而在usb-mass-storage对象中,private指向一个Mass storage对象,usb-mass-storage正是通过这个Mass storage对象访问外部大容量存储的(在我的程序里,Mass storage对象和一个MMC Card对象绑定,外部存储是SD/MMC卡)。由于对于每一种设备的具体实现来说,它知道private指向的是何种类型的设备,因此不会引起混乱。而外部程序根据需要在初始化USB设备对象前赋予private有意义的值——运行时动态绑定。
这一系列struct基本上如实地反映了USB DEVICE硬件逻辑和规范要求: "一个USB设备包含若干个端点,其中有一个固定的控制端点(端点0)。在枚举阶段USB设备要根据HOST的请求应答相应的描述符..."
现在回到OO的话题,这个例子中体现了"组合类":USB设备对象包含了USB端点对象,USB描述符对象...。还有动态绑定 (private成员)。从严格的OO意义上来看,好像有点"怪",不过我认为这恰恰是C的特点——简洁,直接。不信你用C++表达试试?也许会更漂亮,很OO,但是不一定会如此清爽!
P.S. : 熟悉USB Firmware开发的人可能对struct usb_endpoint中的epx,cfg,bank和txCount四个成员有异议,因为这些成员是和特定的硬件相关,并不是所有的USB硬件都支持ping-pong mode,所以bank和txCount不一定用得上,epx, cfg也可能因硬件的不同而不同。没错!更理想的设计是把与硬件相关的部分分离出来,用void *private指向各自的与硬件相关的配置——就像struct usb_device所采用方法,所以更好的版本应该是:
- struct usb_endpoint {
- int type;
- int qlen;
- xQueueHandle lock;
- xQueueHandle q;
- int idx;
-
-
- void (*ep_process)(struct usb_device *dev, struct usb_endpoint *ep, xISRStatus *pxMessage);
- void *private;
- };
tips: 用C表达的一个关键处就是要很好地应用struct来描述模型。
OO Programing in C is not only POSSIBLE but also PRACTICAL --------------------------------------------------------------------------------
OO的一个亮点是类的"继承",通过"继承",可以重用许多代码。而且"继承"也是现实生活中非常自然的一种关系。但是很不幸,C没有class,更没有提供"继承"的表达方式。既然能用C的struct来仿真class, 那能不能继续来仿真"继承"呢?答案是:possible。就像<
>书中所叙述的那样——你可以用C来达到所有C++能做到的事。但这种仿真显然毫无实际应用价值。
"继承"是一种表达方式,代码重用才是目的。
为了重用代码,C++可以用"继承"的方式来巧妙的达到目的,但是也必须付出代价:你必须非常仔细地设计你的类族谱,要有前瞻性,要有可扩展性,要决定分多少个层次....这些都不是容易做到的事。
C别无选择,模块化设计,函数,宏....只能通过巧妙的设计才能达到代码可重用的目的。还是举个例子来说明C是如何做到"殊途同归"的吧。
"链表"是一个非常常用的数据结构,常用于管理无序的数据(对象)集合。链表操作,特别是双向链表操作很容易出错。重用一套通用操作链表的代码可以为我们省不少事。在C++中,我们可以用经典的STL中的list类。为了适应各种数据类型,list类用模板来实现。list类实现的很巧妙,功能很强,但是,不得不说,很少人用。其实不仅list类很少用,STL都很少人用。(希望这是我的一家之言,反正我所熟悉的C++程序员都不怎么用STL :-)当然在C++中你还有另外一个选择:实现一个List基类完成链表操作,要放入链表的类从List类继承而来,就拥有了一套操作list的方法。
Linux内核中用C提供了一套非常巧妙的方法操作链表,位于.../linux/include/linux/list.h,只用一些宏和inline函数来实现双向链表。摘抄一部分出来:
- ....
- struct list_head {
- struct list_head *next, *prev;
- };
-
-
- #define LIST_HEAD_INIT(name) { &(name), &(name) }
-
- #define LIST_HEAD(name) \
- struct list_head name = LIST_HEAD_INIT(name)
-
- #define INIT_LIST_HEAD(ptr) do { \
- (ptr)->next = (ptr); (ptr)->prev = (ptr); \
- } while (0)
-
-
-
-
-
-
-
- static inline void __list_add(struct list_head *new,
- struct list_head *prev,
- struct list_head *next)
- {
- next->prev = new;
- new->next = next;
- new->prev = prev;
- prev->next = new;
- }
-
-
-
-
-
-
-
-
-
- static inline void list_add(struct list_head *new, struct list_head *head)
- {
- __list_add(new, head, head->next);
- }
-
- .....
-
-
-
-
-
-
-
- #define list_entry(ptr, type, member) \
- container_of(ptr, type, member)
-
-
-
-
-
-
- #define list_for_each(pos, head) \
- for (pos = (head)->next; prefetch(pos->next), pos != (head); \
- pos = pos->next)
-
- ......
其中 container_of 宏如下:
-
-
-
-
-
-
-
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
这里使用了GCC特有的 "typeof" 关键字,如果想用其他编译器也想编译通过的话,可以修改成:
- #define container_of(ptr, type, member) ( \
- (type *)( (char *)ptr - offsetof(type,member) ) )
为了便于说明,prefetch定义成:
- static inline void prefetch(const void *x) {;}
offsetof的一个简单版本:
- #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
好了,让我们看看怎么用:
- struct my_data {
- int x;
- int y;
- struct list_head list;
- }
-
-
- LIST_HEAD(my_listhead);
-
- void my_function()
- {
- ...
-
- struct my_data *node_1 = (struct my_data *) malloc(sizeof(struct my_data));
- struct my_data *node_2 = (struct my_data *) malloc(sizeof(struct my_data));
- ...
-
- list_add (node_1->list, &my_listhead);
- list_add (node_2->list, &my_listhead);
- ...
-
- struct my_data * node;
- struct list_head *pos;
- list_for_each (pos, &my_listhead) {
- node = list_entry (pos, struct my_data, list);
- ...
- }
其中最精彩的部分是遍历链表的表达方式:
list_for_each (...) {
...
}
这种表达方式另我想起了Ruby,C++ STL中的到处出现的iterator,和VB中的for each...in...next语句。
从内部结构角度来看,Linux的list实现方式有点类似C++中的"组合类"——在需要放入链表的对象内部放入list类(struct list_head)。但是从遍历链表的时候,可以根据list指针得到包含list节点的对象指针来看,又有点超出了"组合类"的范畴。能否把 struct my_data看成继承了struct list_head呢?从内存映像来看倒有点像(C++子类对象的内存映像是父类对象的超级)。当然,这种强行联系完全没有必要,C就是C,何必去往C+ +套呢?C自有C的表达方式 :P