1. 面向对象的编程思想
当时自以为C还是学得不错的,各种语法都用得挺熟,什么指针、移位、指针数组等等。但实际应用中,用C写的程序的规模并不大,经常觉得对程序没有控制感,程序也不灵活、出错的时候也多。这时正好在看一本书《UNIX高级教程 系统技术内幕》中间在谈文件系统的vnode/vfs体系结构时提到C语言中的“对象”。我当时大吃一惊,C语言还有“对象”?仔细一看,果然如此。C语言也有对象,不过在表达方式上和面向对象的语言有所不同。并且面向对象的思维方法很早就用于UNIX系统的开发中了。
C语言中的对象是以结构的形式表达的,结构中不仅可以包含一般数据类型的成员,也可以包含诸如函数指针等复杂类型的成员。这也就是所谓的“抽象”和“封装”。结构中的函数指针相当于定义了类的纯虚函数。
C面向对象的处理方法在UNIX系统中有着非常广泛的应用,如文件系统、设备驱动等等。以下是几个著名的使用面向对象思想设计的例子
UNIX的块设备结构(UNIX高级教程 P452):
struct bdevsw{
int (*d_open)();
int (*d_close)();
int (*d_strategy)();
int (*d_size)();
int (*d_xhalt)();
…
}bdevsw[];
linux文件操作的结构
struct file_operations {
int (*lseek)(struct inode *inode,struct file *filp, off_t off,int pos);
int (*read)(struct inode *inode,struct file *filp, char *buf, int count);
int (*write)(struct inode *inode,struct file *filp,char *buf,int count);
int (*readdir)(struct inode *inode,struct file *filp,struct dirent *dirent,int count);
int (*select)(struct inode *inode,struct file *filp, int sel_type,select_table *wait);
int (*ioctl) (struct inode *inode,struct file *filp, unsigned int cmd,unsigned int arg);
int (*mmap) (void);
int (*open) (struct inode *inode, struct file *filp);
void (*release) (struct inode *inode, struct file *filp);
int (*fsync) (struct inode *inode, struct file *filp);
};
Apache的模块
typedef struct module_struct {
int version;
int minor_version;
int module_index;
const char *name;
void *dynamic_load_handle;
struct module_struct *next;
unsigned long magic;
void (*init) (server_rec *, pool *);
void *(*create_dir_config) (pool *p, char *dir);
void *(*merge_dir_config) (pool *p, void *base_conf, void *new_conf);
void *(*create_server_config) (pool *p, server_rec *s);
void *(*merge_server_config) (pool *p, void *base_conf, void *new_conf);
const command_rec *cmds;
const handler_rec *handlers;
…
} module;
C语言中面向对象的编程方法早已开始应用。这些概念和方法在20,30年前就已经存在,也不是什么“秘密”。所有基础知识在C语言的教科书中都讲过,问题仅仅在于我们忽略了如何更有效地使用C语言。认识到C面向对象的特点,整个程序的风格、可靠性、可维护性以及效率方面将会有一个飞跃。
当然,和纯粹的面向对象相比,C语言的面向对象的特征还是显得比较原始。但从另一个角度来看,即使是这些“原始”的特征已经可以解决80%-90%的问题了。
2. C语言的“泛型”---void*
我相信每个程序员或多或少都学过或用过一些基本的数据结构,例如链表、HASH、TABLE、树等等。一般来讲,数据结构主要关注于算法,而对实现的方法不太重视。从算法研究的角度这无可厚非。但对于需要实际应用到数据结构的场合,往往会觉得直接使用书上提供的代码颇为困难。例如,链表是较为基本的一个数据结构,程序中使用的机会也比较多。常规的做法:定义一个包含next和prev的结构,再写相关的处理函数。例如定义一个用户信息的结构:
struct SUserInfo
{
char name[12 +1];
char male;
int age;
…
struct SUserInfo* prev;
struct SUserInfo* next;
};
以上结构的最大问题是代码不能重用:链表的操作是固定的,但其包含的内容却是变化的。对于一种新的数据,若每次都需要重新定义结构,拷贝代码,则代码的重用性差,开发的效率低,并且出错的机会也多。有没有办法来处理这些通用的部分呢?答案是肯定的,除了C++以外,使用 C语言的void*也可以解决这个问题。
void*本身没有类型,它可以指向任意的指针,当然可以指向自定义的结构指针。以下一段是可以支持任意类型双向链表的部分代码:
/** 抽象链表中的节点 */
typedef struct SDBNode
{
void* dn_ptr; /** 节点指向的数据指针 */
struct SDBNode *dn_prev; /** 前向节点*/
struct SDBNode *dn_next; /** 后向节点 */
}SDBNoode;
typedef struct SDBList
{
long dl_nums; /** 链表中节点的数量 */
struct SDBNode *dl_head; /** 链表头指针 */
struct SDBNode *dl_tail; /** 链表尾指针 */
}SDBList;
SDBList* dl_create()
{
SDBList* list;
list = (SDBList*) malloc ( sizeof (SDBList) );
memset ( list, 0 ,sizeof( SDBList) );
return list;
}
void dl_insert_tail(SDBList* list,void* ptr)
{
SDBNode* node = malloc(SDBNode);
node -> dn_ptr = ptr;
if (list -> dl_nums == 0)
list -> dl_head = node;
else
list -> dl_tail -> dn_next = node;
node -> dn_prev = list -> dl_tail;
node -> dn_next = NULL;
list -> dl_tail = node;
list -> dl_nums++;
}
void* dl_remove_tail(SDBList* list)
{
void* p;
SDBNode *node = list -> dl_tail;
if(node)
dl_remove(list,node);
else
return NULL;
p = node -> dn_ptr;
freep(node);
return p;
}
doubleList的使用
int main()
{
int i;
SUserInfo* p;
SDBList* dl;
dl = dl_create();
for( i=0; i<10; i++)
{
p = malloc(sizeof SUserInfo);
…
dl_insert_tail(dl,p);
}
…
}
同样地,使用void*可以构造hash、Tree等通用的数据结构。这些基础的数据结构代码无疑是我们编程的利器。
另外,很难界定以上的代码是不是完全符合面向对象的编程方法。从语法到实现都很简单,重用性又好。是否把处理函数都封装到结构中,实现类似dl.insert_tail(&dl,ptr)的操作,还是需要根据具体的应用而定。原则是在满足需求的情况下,越简单越好,不要人为地增加复杂性。
3. C程序的基本处理模式
在上研究生过程中,老师的一句话,到现在还记忆犹新:企业最关心的是数据,数据是企业的核心而不是程序。这句话给了我很深的启发。在经过多年的编程后发现一点(其实早已被广泛应用):
在决大多数的程序中,代码围绕者一段小小的内存块在进行各种逻辑处理,这些内存块一般用结构来封装。根据合适的应用定义一个结构或者是几个相互关联的结构是整个系统的基础和核心。围绕着核心编写各种处理代码,这些处理代码就是我们想实现的业务逻辑。形成一个个小核心后,根据需要进行封装和组合,形成复杂的应用。
分析了多个开发源码的产品的结构,例如LINUX内核、apache、Mysql等等,这种处理方法比比皆是。
在上一节中所提及的双向链表就是这样的一个小核心。
4. C语言的缺点
C语言是一种强大的编程工具,它是一匹好马,但也是一匹烈马,如果我们的经验和驾驭技巧不足,很容易被它掀翻在地;如果能很好地控制它,你会发现,骑着它可以轻松、快捷地到达目的地。
首先,C语言提供了强大的指针功能,指针是C语言的重要特征。通过指针可以直接操作内存,实现相当高的灵活性和效率。但如何正确使用指针却是一个难点。稍有不对,就会引起程序崩溃,这也是指针常常被指责的地方。
其次,用好C语言需要较高的技巧,以及长期不断地自我改进(同样适用于其他语言,但其他语言入门相对容易一些)。这对初学者来说有一定的难度,在基本概念不清的情况下,难以正确使用C语言的某些特性,容易打击自信心。
第三,深入讲解C使用方法和技巧的资料太少。在开放源码普及以前,很少有实际的例子可供学习和参考。很多时候是靠程序员自己的体会和实践来获得这些知识。
第四,标准库严重不足。目前ANSI C所定义的标准库的内容太少,一些基本的数据结构,例如链表,hash都不提供。虽然目前在各主流的操作系统上提供了很多库,例如XML,编码转换、zlib等等。但由于这些库都不是ANSI C所定义的标准库,对使用和移植都带来了一定的困难。(在移植上,JAVA有一定的优势,C++的难度较大)