正文

C/C++编程新手错误语录(1)2006-11-13 13:26:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/stef/20276.html

分享到:

1.引言  还记得当年学数学、英语都有个窍门,那就是搞个错题集。经常复习一下这个错题集,就可以避免下次犯同样的错误。而几乎所有的程序员都是从犯错误开始的,我们也很有必要总结一下编程新手的常见错误,本文的目的在于此。文中所列出的都是笔者在项目开发中接触到的新手真实的言谈,笔者学学文革腔调,姑且称之为“错误语录”。2.语录(1)“我的程序都是对的,可结果不对”  想想你的周围,是不是也有人说这样的话?如果你也曾经说过,那就此打住,不要再说这句话,因为这句话只会显示说话者的无知。既然程序都是对的,那为什么结果不对?(2)“程序=算法+数据结构”  如果刚刚学完C语言,我们说这样的话,完全可以理解,而且可以说是正确的。但是如果你是一位即将从事C/C++编程的程序员,那么很遗憾,这个说法只能判错,殊不知,世界上还有另一种说法:  程序 = 对象 + 消息 “程序=算法+数据结构”只对面向过程的语言(C)成立,而对面向对象的语言(C++),则只能表述为“程序=对象+消息”。传统的过程式编程语言以过程为中心以算法为驱动,面向对象的编程语言则以对象为中心以消息为驱动。这里的消息是广义的,对象A调用了对象B的成员函数,可看作对象A给B发消息。(3)“程序编出来,运行正确就行了”  运行正确的程序并不一定是好程序,程序员时刻要牢记的一条就是自己写的程序不仅是给自己看的,要让别人也能轻易地看懂。很遗憾,许多的编程新手不能清晰地驾驭软件的结构,对头文件和实现文件的概念含糊不清,写出来的程序可读性很差。  C程序采用模块化的编程思想,需合理地将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求,在模块的划分上主要依据功能。模块由头文件和实现文件组成,对头文件和实现文件的正确使用方法是:  规则1 头文件(.h)中是对于该模块接口的声明,接口包括该模块提供给其它模块调用的外部函数及外部全局变量,对这些变量和函数都需在.h中文件中冠以extern关键字声明;  规则2 模块内的函数和全局变量需在.c文件开头冠以static关键字声明;  规则3 永远不要在.h文件中定义变量;  许多程序员对定义变量和声明变量混淆不清,定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如: /*模块1头文件:module1.h*/int a = 5; /* 在模块1的.h文件中定义int a *//*模块1实现文件:module1 .c*/#include “module1.h” /* 在模块1中包含模块1的.h文件 *//*模块2实现文件: module2.c*/#include “module1.h” /* 在模块2中包含模块1的.h文件 *//*模块2 实现文件:module3 .c*/#include “module1.h” /* 在模块3中包含模块1的.h文件 */以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这明显不符合编写者的本意。正确的做法是:/*模块1头文件:module1.h*/extern int a; /* 在模块1的.h文件中声明int a *//*模块1实现文件:module1 .c*/#include “module1.h” /* 在模块1中包含模块1的.h文件 */int a = 5; /* 在模块1的.c文件中定义int a *//*模块2 实现文件: module2 .c*/#include “module1.h” /* 在模块2中包含模块1的.h文件 *//*模块3 实现文件: module3 .c*/#include “module1.h”   /* 在模块3中包含模块1的.h文件 */   这样如果模块1、2、3操作a的话,对应的是同一片内存单元。 规则4 如果要用其它模块定义的变量和函数,直接包含其头文件即可。许多程序员喜欢这样做,当他们要访问其它模块定义的变量时,他们在本模块文件开头添加这样的语句: extern int externVar;    抛弃这种做法吧,只要头文件按规则1完成,某模块要访问其它模块中定义的全局变量时,只要包含该模块的头文件即可。(4)“数组名就是指针”  许多程序员对数组名和指针的区别不甚明了,他们认为数组名就是指针,而实际上数组名和指针有很大区别,在使用时要进行正确区分,其区分规则如下:  规则1 数组名指代一种数据结构,这种数据结构就是数组;  例如: char str[10];char *pStr = str;cout << sizeof(str) << endl;cout << sizeof(pStr) << endl;   输出结果为: 104  这说明数组名str指代数据结构char[10]。  规则2 数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改; char str[10]; char *pStr = str;str++; //编译出错,提示str不是左值 pStr++; //编译正确   规则3 指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址;  规则4 数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。例如: void arrayTest(char str[]){cout << sizeof(str) << endl;   //输出指针长度    str++; //编译正确 }int main(int argc, char* argv[]){ char str1[10] = "I Love U"; arrayTest(str1); return 0;} (5)“整形变量为32位”  整形变量是不是32位这个问题不仅与具体的CPU架构有关,而且与编译器有关。在嵌入式系统的编程中,一般整数的位数等于CPU字长,常用的嵌入式CPU芯片的字长为8、16、32,因而整形变量的长度可能是8、16、32。在未来64位平台下,整形变量的长度可达到64位。  长整形变量的长度一般为CPU字长的2倍。  在数据结构的设计中,优秀的程序员并不会这样定义数据结构(假设为WIN32平台): typedef struct tagTypeExample{unsigned short x;unsigned int y; }TypeExample;他们这样定义:#define unsigned short UINT16 //16位无符号整数#define unsigned int UINT32 //32位无符号整数typedef struct tagTypeExample{UINT16 x;UINT32 y; }TypeExample; 这样定义的数据结构非常具有通用性,如果上述32平台上的数据发送到16位平台上接收,在16位平台上仅仅需要修改UINT16、UINT32的定义: #define unsigned int UINT16 //16位无符号整数#define unsigned long UINT32 //32位无符号整数   几乎所有的优秀软件设计文档都是这样定义数据结构的。(6)“switch和if …else…可随意替换”  switch语句和一堆if…else…的组合虽然功能上完全一样,但是给读者的感受完全不一样。if…else…的感觉是进行条件判断,对特例进行特别处理,在逻辑上是“特殊与一般”的关系,而switch给人的感觉是多个条件的关系是并列的,事物之间不存在特殊与一般的关系,完全“对等”。譬如: //分别对1-10的数字进行不同的处理,用switchswitch(num){case 1:…case 2:…}//对1-10之间的数字进行特殊处理,用ifif(num < 10 && num > 1){…}else{…}   许多时候,虽然不同的代码可实现完全相同的功能,但是给读者的感觉是完全不同的。譬如无条件循环: while(1){}   有的程序员这样写: for(;;){}   这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C/C++语言中意味着无条件循环才明白其意。而不懂C/C++语言的读者看到while(1)也可猜到这是一个无条件循环。(7)“免得麻烦,把类里面的成员函数都搞成public算了”  许多人编C++程序的时候,都碰到这样的情况,先前把某个成员函数定义成类的private/protected函数,后来发现又要从外面调用这个函数,就轻易地将成员函数改为public类型的。甚至许多程序员为了避免访问的麻烦,干脆把自己添加的成员函数和成员变量都定义成public类型。  殊不知,这是一种规划的失败。在类的设计阶段,我们就要很清晰地知道,这个类的成员函数中哪些是这个类的接口,哪些属于这个类内部的成员函数和变量。一般的准则是接口(public成员)应在满足需求的前提下尽可能简单!  所以不要轻易地将private/protected成员改为public成员,真正的工作应该在规划阶段完成。 (8)“我想用malloc”、“我用不好malloc”  来看看一个变态程序: /* xx.c:xx模块实现文件 */ int *pInt;/* xx模块的初始化函数 */xx_intial() {pInt = ( int * ) malloc ( sizeof( int ) );...}/* xx模块的其他函数(仅为举例)*/xx_otherFunction(){*Int = 10;...}   这个程序定义了一个全局整型变量指针,在xx模块的初始化函数中对此指针动态申请内存,并将pInt指向该内存首地址,并在xx模块的其他函数中都使用pInt指针对其指向的整数进行读取和赋值。  这个程序让我痛不欲生了好多天,扼腕叹息!这是我母校计算机系一位硕士的作品!作者为了用上malloc,拼命地把本来应该用一个全局整型变量摆平的程序活活弄成一个全局整型指针并在初始化函数中“动态”申请内存,自作聪明而正好暴露自己的无知!我再也不要见到这样的程序。  那么malloc究竟应该怎么用?笔者给出如下规则:  规则1 不要为了用malloc而用malloc,malloc不是目的,而是手段;  规则2 malloc的真正内涵体现在“动态”申请,如果程序的特性不需动态申请,请不要用malloc;  上面列举的变态程序完全不具备需要动态申请的特质,应该改为: /* xx.c:xx模块实现文件 */ int example;/* xx模块的初始化函数 */xx_intial() {...}/* xx模块的其他函数(仅为举例) */xx_otherFunction(){example = 10;...}  规则3 什么样的程序具备需要动态申请内存的特质呢?包含两种情况:  (1)不知道有多少要来,来了的又走了  不明白?这么说吧,譬如你正在处理一个报文队列,收到的报文你都存入该队列,处理完队列头的报文后你需要取出队列头的元素。  你不知道有多少报文来(因而你不知道应该用多大的报文数组),这些来的报文处理完后都要走(释放),这种情况适合用malloc和free。  (2)慢慢地长大  譬如你在资源受限的系统中编写一文本编辑器程序,你怎么做,你需要这样定义数组吗?   char str[10000];   不,你完全不应该这么做。即使你定义了一个10000字节大的字符串,用户如果输入10001个字符你的程序就完完了。  这个时候适合用malloc,因为你根本就不知道用户会输入多少字符,文本在慢慢长大,因而你也应慢慢地申请内存,用一个队列把字符串存放起来。  那么是不是应该这样定义数据结构并在用户每输入一个字符的情况下malloc一个CharQueue空间呢? typedef struct tagCharQueue{char ch;struct tagCharQueue *next;}CharQueue;   不,这样做也不对!这将使每个字符占据“1+指针长度”的开销。  正确的做法是: typedef struct tagCharQueue{char str[100];struct tagCharQueue *next;}CharQueue;   让字符以100为单位慢慢地走,当输入字符数达到100的整数倍时,申请一片CharQueue空间。 规则4 malloc与free要成对出现  它们是一对恩爱夫妻,malloc少了free就必然会慢慢地死掉。成对出现不仅体现在有多少个malloc就应该有多少个free,还体现在它们应尽量出现在同一函数里,“谁申请,就由谁释放”,看下面的程序: char * func(void){char *p;p = (char *)malloc(…);if(p!=NULL)…; /* 一系列针对p的操作 */return p; }/*在某处调用func(),用完func中动态申请的内存后将其free*/char *q = func();…free(q);   上述代码违反了malloc和free的“谁申请,就由谁释放”原则,代码的耦合度大,用户在调用func函数时需确切知道其内部细节!正确的做法是: /* 在调用处申请内存,并传入func函数 */char *p=malloc(…);if(p!=NULL){ func(p);…free(p);p=NULL;}/* 函数func则接收参数p */void func(char *p){… /* 一系列针对p的操作 */}   规则5 free后一定要置指针为NULL,防止其成为“野”指针

阅读(2610) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册