1.引言
还记得当年学数学、英语都有个窍门,那就是搞个错题集。经常复习一下这个错题集,就可以避免下次犯同样的错误。而几乎所有的程序员都是从犯错误开始的,我们也很有必要总结一下编程新手的常见错误,本文的目的在于此。文中所列出的都是笔者在项目开发中接触到的新手真实的言谈,笔者学学文革腔调,姑且称之为“错误语录”。
2.语录
- "我的程序都是对的,可结果不对"
- "程序=算法+数据结构"
- "程序编出来,运行正确就行了"
- "数组名就是指针"
- "整形变量为32位"
- "switch和if …else…可随意替换"
- "免得麻烦,把类里面的成员函数都搞成public算了"
- "我想用malloc"、"我用不好malloc"
- "函数add编译生成的符号就是add"
- "没见过在C语言中调用C++的函数"、"C/C++不能调用Basic、Pascal语言的函数"
- "英语、数学不好就学不好C/C++"
- "C++太难了,我学不会"
- "整型变量仅仅意味着一个整数"
- "值传递一定不会改变参数"
- "C不高级,学C++、JAVA、C#才够味"
- "语言学得越多越好"
(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;
输出结果为:
10
4
这说明数组名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的数字进行不同的处理,用switch
- switch(num)
- {
- case 1:
- …
- case 2:
- …
- }
//对1-10之间的数字进行特殊处理,用if
- if(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,防止其成为“野”指针。
- int add(int x,int y)
- {
- return x + y;
- }
- float add(float x,float y)
- {
- return x + y;
- }
即便是在C语言中,add函数被多数C编译器编译后在符号库中的名字也不是add,而是_add。而在C++编译器中, int add(int x,int y)会编译成类似_add_int_int这样的名字(称为“mangled name”),float add(float x,float y)则被编译成_add_float _float,mangled name包含了函数名、函数参数数量及类型信息,C++依靠这种机制来实现函数重载。
所以,在C++中,本质上int add( int x, int y )与float add( float x, float y )是两个完全不同的函数,只是在用户看来其同名而已。
这就要求初学者们能透过语法现象看问题本质。本质上,语言的创造者们就是在玩各种各样的花样,以使语言具备某种能力,譬如mangled name花样的目的在于使C++支持重载。而C语言没有玩这样的花样,所以int add( int x, int y )与float add( float x, float y )不能在C程序中同时存在。
(10)“没见过在C语言中调用C++的函数”、“C/C++不能调用Basic、Pascal语言的函数”
这又是一个奇天下之大怪的问题,“打死我都不相信C、C++、basic、pascal的函数能瞎调来调去”,可是有句话这么说:
没有你见不到的,只有你想不到的!
既然芙蓉姐姐也有其闻名天下的道理,那么C、C++、Basic、Pascal的函数为什么就不能互相调用呢?
能!
你可以用Visual C++写一个DLL在Visual Basic、Delphi(Pascal的孙子,Object Pascal的儿子)中调用,也可以在Visual Basic、Delphi中写一个DLL在Visual C++中调用不是?
让我们来透过现象看本质。首先看看函数的调用约定(以Visual C++来说明):
(1) _stdcall调用
_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。
WIN32 Api都采用_stdcall调用方式,这样的宏定义说明了问题:
#define WINAPI _stdcall
按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number。
(2) _cdecl调用
_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。
由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。关于C/C++中变长参数(…)的问题,笔者将另文详述。
由于Visual C++默认采用_cdecl 调用方式,所以VC中中调用DLL时,用户应使用_stdcall调用约定。
按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
(3) _fastcall调用
_fastcall调用较快,它通过CPU内部寄存器传递参数。
按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数,形如@functionname@number。
关键字_stdcall、_cdecl和_fastcall可以直接加在函数前,也可以在Visual C++中设置,如图1。
图1 在VC中设置函数调用约定
在创建DLL时,一般使用_stdcall调用(Win32 Api方式),采用_functionname@number命名规则,因而各种语言间的DLL能互相调用。也就是说,DLL的编制与具体的编程语言及编译器无关,只要遵守DLL的开发规范和编程策略,并安排正确的调用接口,不管用何种编程语言编制的DLL都具有通用性。
推而广之,如果有这样一个IDE开发环境,它能识别各种语言,所有语言采用相同的调用约定和命名规则,一个软件内各种语言书写的函数将能互相调用!
这个世界上可能永远不需要这样一个IDE。
(11)“英语、数学不好就学不好C/C++”
这也许是20世纪最大的谎言,这句话最先是哪位大师的名人名言已无可考证,可此后一批批的人被它误导。许多初学者因为这句话被吓倒,放弃了做程序员的理想。还有许多后来成为优秀程序员的人,在他们的成长过程中并没有依靠深奥的数学,可他们还是在总结经验时制造恐慌,号称一定要具备高深的数学知识,唯恐别人笑话其学术水平不高。
在下则认为,大多数情况下,程序设计不需要太深奥的数学功底,除非你所从事的程序设计涉及特定的专业领域(如语音及图像处理、数字通信技术等)。在下这一观点也许是革旧立新,而革命必然要流血牺牲(谭嗣同),所以恭候大家板砖。
那么英语在C/C++的学习中处于什么地位呢?那就是能看懂资料,看懂MSDN。
学编程的终极之道不在看书,而在大量地不断地实践。
(12)“C++太难了,我学不会”
又不知是谁的悲观论调,许多初学者被C++吓倒,“太难了,我学不好”,如弱者自怜。如果C++真的难到学不会,那么C++的创造者们所从事的工作岂不是“非人力所能及也”?
在下认为,学习C++的态度应该是:战略上藐视它,战术上重视它,要敢于胜利(《毛主席语录》)。当然也不可轻敌,不能因为掌握了一点皮毛就以为自己牛B轰轰了(笔者曾经牛B轰轰了好一阵子,现在想来,甚觉当时幼稚)。
如果你征服了C++,透彻理解了C++的语言特性及STL,那么,其他语言想不被你征服都难了。
(13)“整型变量仅仅意味着一个整数”
当我们还是一个新手,看整型就是整数;
当我们成为高手,看什么都是整型。
整型,在所有C/C++基本数据类型中最富有艺术魅力和奇幻色彩。
我们从某著名论坛的一篇帖子开始一窥整型的奥妙。
问:Vxworks操作系统启动一个任务的函数是taskSpawn(char* name, int priority, int options, int stacksize, FUNCPTR function, int arg1,.. , int arg10),它只接受整型参数,我该怎么办才能给它传一个结构体(在32位PowerPC平台下)?
答:可以传入结构体的指针,在32位PowerPC平台下,指针本质上就是一个32位整数,在函数体内将整型强制转化为结构体指针就可访问结构体的每一个元素。如:
- //启动任务1
- taskSpawn(“task1”, 180, NULL, 10000, Task1Fun, &pStructAr,0,0,0,0,0,0,0,0,0);
- //task1函数
- Task1Fun ( int arg1 )
- {
- struct_x * pStructx = (struct_x *) arg1; //将整型强制转化为结构体指针
- …
- }
在此提出“泛整型”的概念,(unsigned)char、(unsigned)short int、(unsigned)int、(unsigned)long int等都属于这个范畴,指针必然属于“泛整型”的范围。用指针的高超境界,也为将其看做一个“泛整型”。
看看软件的详细设计文档,其数据结构定义部分经常看到“INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64”或“BYTE、 WORD、DWORD”等数据类型,它们在本质上都是(unsigned)char、(unsigned)short int、(unsigned)int、(unsigned)long int宏定义的结果,都属于“泛整型”。所以,“泛整型”的概念真实地体现在日常的软件设计当中。
正因为各种指针类型在本质上都是“泛整型”,因此它们可以互相转化:
- int a, b;
- memset( (char*) &a, (char*) &b, sizeof(int) );
等价于:
- int a, b;
- a = b;
从来没有人会用memset( (char*) &a, (char*) &b, sizeof(int) )来代替a = b,这里只是为了说明问题。下面的代码则经常用到:
- int *p = (int *) malloc(100*sizeof(int));
- memset ( p, 0, 100*sizeof(int) ); //将申请的内存空间清0
我们看memset的函数原型为:
- void * memset ( void * buffer, int c, size_t num );
实际上它接受的第一个参数是无类型指针,在memset函数体内,其它任意类型的指针都向void *转化了。类似的内存操作函数memcpy所接受的源和目的内存地址也是无类型指针。
char *转化为int *后的值虽然不变(还是那个地址),但是其++、--等操作的含义却发生了变化,这也是要注意的。
- char *p;
- ++p;
与
- char *p;
- ++(int *)p;
的结果是不一样的,前者的p值加了1,而后者的则增加了sizeof(int)。
下面来剥Windows程序设计中消息传递函数两个参数的皮,看看它们究竟是什么:
- typedef UINT WPARAM;
- typedef LONG LPARAM;
原来,WPARAM和LPARAM其实都属于“泛整型”,所以不要报怨消息处理函数只能接受“泛整型”。实际上,从指针的角度上来讲,在C/C++中,可以获得任何类型实例(变量、结构、类)的指针,所以Windows的消息处理函数实际上可以接受一切类型的参数。
惊天动地一句话:“泛整型”可表征一切。
(14)“值传递一定不会改变参数”
理论而言,值传递的确不会改变参数的内容。但是,某年某月的某一天,隔壁office的硕士mm写了这么一段程序,参数的值却被改变了:
- int n = 9;
- char a[10];
- example ( n, a ); //调用函数example(int n,char *pStr)
- printf (“%d”, n ); //输出结果不是9
大概整个office的人都被搞懵了,都说编译器瞎搞,有问题。找到笔者,笔者凭借以往的经常,一眼就看出来不是什么编译器出错,而是在函数example内对字符串a的访问越界!
当在函数example内对a的访问越界后,再进行写操作时,就有可能操作到了n所在的内存空间,于是改变了n的值。
给出这个语录,并非为了推翻“值传递不会改变参数”的结论,而是为了从侧面证明在C/C++语言中,数组越界是多么危险的错误!
下面的两个函数有明显的数组越界:
- void example1()
- {
- char string[10];
- char* str1 = "0123456789";
- strcpy( string, str1 );
- }
- void example 2(char* str1)
- {
- char string[10];
- if( strlen( str1 ) <= 10 )
- {
- strcpy( string, str1 );
- }
- }
而这个函数的越界就不这么明显:
- void example3()
- {
- char string[10], str1[10];
- int i;
- for(i=0; i<10; i++)
- {
- str1 = 'a';
- }
- strcpy( string, str1 );
- }
其实,这个函数危险到了极点。因为对于strcpy函数而言,拷贝的时候要碰到’\0’才结束,str1并没有被赋予结束符,因而你根本就不知道strcpy( string, str1 )的结果究竟会是拷贝多大一片内存!
遗憾的是,C/C++永远不会在编译和连接阶段提示数组越界,它只会在运行阶段导致程序的崩溃。
数组越界,是大多数C/C++编程新手常犯的错误,而它又具有极大的隐蔽性,新手们一定要特别注意。
(15)“C不高级,学C++、JAVA、C#才够味”
也许谭浩强老师的C语言教材是绝大多数高校学生学习的第一门编程课程,所以在许多学生的心目中,觉得C是一种入门级的语言,他们舍弃基础而追逐花哨的Visual XXX、Java、ASP、PHP、.net,他们以为这样做“赚大了”。
非也!
C是一种多么富有魅力的语言!在今时的绝对多数底层开发中,仍然几乎被C完全垄断。这些领域包括操作系统、嵌入式系统、数字信号处理等。舍弃C的经济基础搭.net的高层建筑实在是危险。
我们总是以为自己掌握了C,那么请写一个strcpy的标准函数。您的答案若是:
- void strcpy( char *strDest, char *strSrc )
- {
- while( (*strDest++ = * strSrc++) != ‘\0’ );
- }
很遗憾,您的程序只能拿到E。看看拿A的strcpy:
- char * strcpy( char *strDest, const char *strSrc )
- {
- assert( (strDest != NULL) && (strSrc != NULL) );
- char *address = strDest;
- while( (*strDest++ = * strSrc++) != '\0' );
- return address;
- }
这个程序考虑了什么?
- 程序要强大:为了实现链式操作,将目的地址返回,函数返回类型改为char *
- 程序要可读:源字符串指针参数加const限制,表明为输入参数
- 程序要健壮:验证strDest和strSrc非空
如果这三点中您只考虑到0点或1点,那么请回家好好修炼一下C。因为这个最简单的strcpy已验证出您的C语言基础只能叫做“入门”。
再写个简单的strlen,这么写就好了:
- int strlen( const char *str ) //输入参数为const
- {
- assert( strt != NULL ); //断言字符串地址非0
- int len;
- while( (*str++) != '\0' )
- {
- len++;
- }
- return len;
- }
由此可见,写好这些简单的函数也需要深厚的基本功,永远不要放弃对基本功的培养。
(16)“语言学得越多越好”
许多的初学者都经历过这样的一个阶段,面对大量的编程语言和开发环境,他们俩感到难以取舍,不知道自己究竟应该学习什么。于是他们什么都学,今天看一下 Visual Basic,明天看学一下C++,后天在书点看到了本Java便心血来潮买回来翻翻,大后天又发现必须学.net了。他们很痛苦,什么都在看,结果什么都没学会,忙忙碌碌而收获甚微。
我们真的没有必要在什么语言都不甚精通的情况下乱看一气。认准了一种真正语言就应该坚持不懈地努力。因为任何一门语言的掌握都非一朝一夕一事,笔者从六年前开始接触C++,直到现在,每一阶段仍有新的启发,在项目开发的过程中也不断有新的收获。今日我还是绝对不敢宣称自己“精通”这门语言。
许多刚毕业的大学生,动不动就在简历上写上自己精通一堆语言。与之相反,大多数优秀的工程师都不敢这么写。也许,研究越深,便越敢自身的无知。
在下认为,一个成熟的语言体系应该是:
程序员的语言体系 = 一种汇编 + C + 一种面向对象(C++、JAVA、C#等)
如果还要加,那就加一种解释型语言,perl或tcl(也许其它)。
语言具有极大的相似性,从C++过渡到JAVA只需要很短的一段时间。各种语言的发展历史也体现了编程思想的发展史。我们学习一种语言,语法也许并不是最重要的,最重要的是蕴藏在语法外表下的深层特性和设计用意。