赞
踩
我们可以从各种途径学习计算机,学习编程,也可以很快地学会一大打的编程语言。但是,所谓会不如精,学会编写好的程序,也是非常重要的。
下面,我就以实际开发中的情况为例子,记叙一下我遇到过的各种不好的编程习惯。希望由此自省,更希望对别人有所帮助。
我是做Linux下开发的,所以本文更加侧重Linux环境下的Console C程序设计。
我们可以任意挥洒思路,却绝对不能任意涂抹程序。因为,程序并不是写出来,编译出来,就完事了。它是要被人读的,至少,作者以后可能读到。
好的格式,无疑是快速理解程序的开端。大概有以下这几种情况。
注释是以后读这些源码最重要的提示,写得好了,可以事半功倍。
变量名的命名,直接影响着程序的易读性以及易写性。
变量声明了,一定要注意初使化。用得好了,这里面有很多技巧。
这是出问题最多的地方。
处理好函数或程序返回值,才能很方便地被程序或其它程序调用。
shell下的那么多程序,可以被方便地写进脚本,联合起来,执行强大的功能,就是因为它们一致的接口、输入输出以及返回值。
打开文件或分配内存时,一定要判断是否成功,否则我们将常常发生错误以后,找不着线索。调用函数时,也要在关键之处判断成功与否,有的,要仔细检查errno。
结构化的程序,易于理解,易于编写,易于维护。
输出操作可能是程序设计中最多的操作,同时,也是目前用得最滥的操作。
我们要写好的、功能强大的、稳定高效的程序,编译是一个不得不认真对待的方面。
假设字符串有10个字节,这样,每判断一个字节是不是’\0’,复制一个字节,CPU就要至少执行两次。10个字节,就是20次。并且,由于每次都进行判断,使得CPU的指令流水线无法或者低效处理,更是雪上加霜。所以,字符串复制,是一项对内存要求不大,但是却非常浪费CPU的操作,应该尽量避免。
在一个好的C程序中,会把所有的静态变量,写到一起,程序中用指针来指定。除非有输入或者输出,否则很少很少会用字符串复制。
动态分配内存,是一项复杂的操作,涉及到很多链表甚至遍历、判断等操作。而我们在栈上分配一块空间,只是栈顶指针偏移一下。所以,应该尽量减少动态分配内存,而是采用静态的方式。这样,效率要高许多。
有人喜欢做字符串输入的时候,先判断字符串长度,再申请长度加1的内存,这是非常得不偿失的举动。
另外,千万不要在一个函数中,把本可以静态分配的结构,非要动态分配,再在末尾释放。
程序应该是在保证功能、保证可读性、保证性能的前提下,越简短越好的。并不是写得花哨一些,就有多好。相反,有的时候,就是无用而且有害的。
比如有的程序员为了防止注入,把本来的strcpy (dst, src),硬生生写成strncpy (dst, src, strlen (src))。这除了增加一轮字符遍历导致性能下降而外,没有任何用处。因为,strcpy本身就是靠末尾的0结束符来判断复制完成的,而strlen也是靠末尾的0结束符来判断长度的。这样用strncpy,就是把一项操作,调用了两次。正确的做法是,比如我们的dst只有10字节长,那么,用strncpy (dst, src, 9),然后,硬性把dst[9] = ‘\0’。
一个好的程序员,应该是做有意义的事,尽量少做重复的事。
在一个大的函数下,根据输入参数的不同,而处理大量根本不同的操作。这样的话,可以在调用这个函数的时候,不用判断参数,而方便一些。而结果是,导致别人读起来异常吃力,并且,效率也并不高。因为要判断的,在哪都是要判断的。这种情况,最好是用不同的函数,实现不同的功能。在调用的地方,判断情况,分别调用不同的函数。
还有的正好相反,功能相似的函数,只是一点参数不同,一写一大堆(当然可能是复制出来的),这样也不好。其实这样的地方,多半可以用宏来解决。或者,用偏移地址定位。比如,我们要根据输入的enum值,来返回不同的字符串,就可以把字符串声明成一个大的二维数组,这个函数用来返回二维数组的偏移量指针,最好不要不辞辛苦地写一大片switch或者if else。
我看到一个现象,写Windows程序熟练的人,喜欢把一列上的一个node,给一个int类型的handle(id),然后在程序中,到处传递这个handle(id),每次要定位这个node的时候,就是根据它的handle(id),到列上去查找,然后返回node。这可能是受关系数据库的观念影响吧?
为什么要这样?直接在程序各处用这个node的指针不是挺好么?指针是四个字节,int也是四个字节,而用指针根本不用遍历查找。
很多第三方库的使用,需要进行一个初使化,然后使用它的功能,最后再释放。这就使得一些保险的程序员,为了保证程序不出错,一用到库中的功能的时候,就初使化一下,没有必要。因为类似这种情形的初使化,一般来说都是资源消耗非常大的。
比如很多要求性能的程序,连系统的内存都不会每次使用时再申请,而是有自己的内存管理算法,分配出来,多次使用。
对这类的库的好的方法是,程序运行最初,初使化这些东西,退出时再释放。如果怕出问题,就加一个全局的标志,只要进行过初使化,就标志一下,以后怕出问题的地方,都查看这个标志位。
曾经我几个同事,喜欢上了void *, 一定要求别人,写一个库的时候,一个handle用
typedef void *handle;
来写。于是,大量的以此结构为传入参数的函数,全都变成了接收void *类型的函数调用。
这种做法的好处是:
1、handle的真正结构被隐藏在了实现文件中,做到了信息隐藏。
2、操作handle的函数,可以接受任意类型的指针。
可是,带来的问题更多。
第一个问题就是,本来好好的函数代码,全都多了一层强制类型的转换。头文件的作用是什么?就是为了类型检查,避免出现参数不配产生的错误。
比如,我们声明了
int fun (int *a);
,当传入一个非int类型的指针的时候,编译器就会警告传错参数了。可是,如果变成
int fun (void *a);
再怎么传,编译器也不管对还是错了。这是我们希望的吗?
再有,头文件做到信息隐藏,有意义吗?我们给人家用我们开发的库,却还要把结构隐藏起来,给人家看到一个不正确的结构,对我们有什么好处?
有人说这样可以防止上层再有人声明同名的结构时产生冲突,这个逻辑上也说不过去。把结构换成void *,就可以解决命名空间问题吗?
还有人说,声明成这种结构,可以让上层用户不必关心结构细节。既然不关心细节,那么我们声明成什么结构,上层也不会管的。
恰恰相反,如果上层需要关心这里的细节,我们倒要声明成void *。
比如,pthread的启动函数
extern int pthread_create (pthread_t *__restrict __newthread,
__const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg) __THROW __nonnull ((1, 3));
这里为什么第四个函数参数是void *?就是因为这个结构,pthread不会管。而是由start_routine这个过程,自己去实现它自己的处理。
所以,我们可以写出这样的代码:
int *func (int *a);
int a;
pthread_create (&pth, &attr, func, &a);
这里传入了一个int的指针,完全正确。而如果这里需要传入一个结构,一样可以这样
struct sockaddr *func2 (struct sockaddr *a);
struct sockaddr a;
pthread_create (&pth, &attr, func2, &a);
这样,pthread这个函数,就实现了多态。
当然,这个例子也可以看出来,pthread_t不必声明成void *结构,也万万不应该声明成void *结构。
所以,我上面提到,在函数开头写强转,弊大于利。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。