str++;//编译正确
}
intmain(intargc,char*argv[])
{
charstr1[10]="ILoveU";
arrayTest(str1);
return0;
}
(5)“整形变量为32位”
整形变量是不是32位这个问题不仅与具体的CPU架构有关,而且与编译器有关。
在嵌入式系统的编程中,一般整数的位数等于CPU字长,常用的嵌入式CPU芯片的字长为8、16、32,因而整形变量的长度可能是8、16、32。
在未来64位平台下,整形变量的长度可达到64位。
长整形变量的长度一般为CPU字长的2倍。
在数据结构的设计中,优秀的程序员并不会这样定义数据结构(假设为WIN32平台):
typedefstructtagTypeExample
{
unsignedshortx;
unsignedinty;
}TypeExample;
他们这样定义:
#defineunsignedshortUINT16//16位无符号整数
#defineunsignedintUINT32//32位无符号整数
typedefstructtagTypeExample
{
UINT16x;
UINT32y;
}TypeExample;
这样定义的数据结构非常具有通用性,如果上述32平台上的数据发送到16位平台上接收,在16位平台上仅仅需要修改UINT16、UINT32的定义:
#defineunsignedintUINT16//16位无符号整数
#defineunsignedlongUINT32//32位无符号整数
几乎所有的优秀软件设计文档都是这样定义数据结构的。
(6)“switch和if…else…可随意替换”
switch语句和一堆if…else…的组合虽然功能上完全一样,但是给读者的感受完全不一样。
if…else…的感觉是进行条件判断,对特例进行特别处理,在逻辑上是“特殊与一般”的关系,而switch给人的感觉是多个条件的关系是并列的,事物之间不存在特殊与一般的关系,完全“对等”。
譬如:
//分别对1-10的数字进行不同的处理,用switch
switch(num)
{
case1:
…
case2:
…
}
//对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模块实现文件*/
intexample;
/*xx模块的初始化函数*/
xx_intial()
{
...
}
/*xx模块的其他函数(仅为举例)*/
xx_otherFunction()
{
example=10;
...
}
规则3 什么样的程序具备需要动态申请内存的特质呢?
包含两种情况:
(1)不知道有多少要来,来了的又走了
不明白?
这么说吧,譬如你正在处理一个报文队列,收到的报文你都存入该队列,处理完队列头的报文后你需要取出队列头的元素。
你不知道有多少报文来(因而你不知道应该用多大的报文数组),这些来的报文处理完后都要走(释放),这种情况适合用malloc和free。
(2)慢慢地长大
譬如你在资源受限的系统中编写一文本编辑器程序,你怎么做,你需要这样定义数组吗?
charstr[10000];
不,你完全不应该这么做。
即使你定义了一个10000字节大的字符串,用户如果输入10001个字符你的程序就完完了。
这个时候适合用malloc,因为你根本就不知道用户会输入多少字符,文本在慢慢长大,因而你也应慢慢地申请内存,用一个队列把字符串存放起来。
那么是不是应该这样定义数据结构并在用户每输入一个字符的情况下malloc一个CharQueue空间呢?
typedefstructtagCharQueue
{
charch;
structtagCharQueue*next;
}CharQueue;
不,这样做也不对!
这将使每个字符占据“1+指针长度”的开销。
正确的做法是:
typedefstructtagCharQueue
{
charstr[100];
structtagCharQueue*next;
}CharQueue;
让字符以100为单位慢慢地走,当输入字符数达到100的整数倍时,申请一片CharQueue空间。
规则4 malloc与free要成对出现
它们是一对恩爱夫妻,malloc少了free就必然会慢慢地死掉。
成对出现不仅体现在有多少个malloc就应该有多少个free,还体现在它们应尽量出现在同一函数里,“谁申请,就由谁释放”,看下面的程序:
char*func(void)
{
char*p;
p=(char*)malloc(…);
if(p!
=NULL)
…;/*一系列针对p的操作*/
returnp;
}
/*在某处调用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*/
voidfunc(char*p)
{
…/*一系列针对p的操作*/
}
规则5 free后一定要置指针为NULL,防止其成为“野”指针
(9)“函数add编译生成的符号就是add”
intadd(intx,inty)
{
returnx+y;
}
floatadd(floatx,floaty)
{
returnx+y;
}
即便是在C语言中,add函数被多数C编译器编译后在符号库中的名字也不是add,而是_add。
而在C++编译器中,intadd(intx,inty)会编译成类似_add_int_int这样的名字(称为“mangledname”),floatadd(floatx,floaty)则被编译成_add_float_float,mangledname包含了函数名、函数参数数量及类型信息,C++依靠这种机制来实现函数重载。
所以,在C++中,本质上intadd(intx,inty)与floatadd(floatx,floaty)是两个完全不同的函数,只是在用户看来其同名而已。
这就要求初学者们能透过语法现象看问题本质。
本质上,语言的创造者们就是在玩各种各样的花样,以使语言具备某种能力,譬如mangledname花样的目的在于使C++支持重载。
而C语言没有玩这样的花样,所以intadd(intx,inty)与floatadd(floatx,floaty)不能在C程序中同时存在。
(10)“没见过在C语言中调用C++的函数”、“C/C++不能调用Basic、Pascal语言的函数”
这又是一个奇天下之大怪的问题,“打死我都不相信C、C++、basic、pascal的函数能瞎调来调去”,可是有句话这么说:
没有你见不到的,只有你想不到的!
既然芙蓉姐姐也有其闻名天下的道理,那么C、C++、Basic、Pascal的函数为什么就不能互相调用呢?
能!
你可以用VisualC++写一个DLL在VisualBasic、Delphi(Pascal的孙子,ObjectPascal的儿子)中调用,也可以在VisualBasic、Delphi中写一个DLL在VisualC++中调用不是?
让我们来透过现象看本质。
首先看看函数的调用约定(以VisualC++来说明):
(1)_stdcall调用
_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。
WIN32Api都采用_stdcall调用方式,这样的宏定义说明了问题:
#defineWINAPI_stdcall
按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number。
(2)_cdecl调用
_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。
_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。
由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。
关于C/C++中变长参数(…)的问题,笔者将另文详述。
由于VisualC++默认采用_cdecl调用方式,所以VC中中调用DLL时,用户应使用_stdcall调用约定。
按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
(3)_fastcall调用
_fastcall调用较快,它通过CPU内部寄存器传递参数。
按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数,形如@functionname@number。
关键字_stdcall、_cdecl和_fastcall可以直接加在函数前,也可以在VisualC++中设置,如图1。
图1在VC中设置函数调用约定
在创建DLL时,一般使用_stdcall调用(Win32Api方式),采用_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,intpriority,intoptions,intstacksize,FUNCPTRfunction,intarg1,..,intarg10),它只接受整型参数,我该怎么办才能给它传一个结构体(在32位PowerPC平台下)?
答:
可以传入结构体的指针,在32位PowerPC平台下,指针本质上就是一个32位整数,在函数体内将整型强制转化为结构体指针就可访问结构体的每一个元素。
如:
//启动任务1
taskSpawn(“task1”,180,NULL,10000,Task1Fun,&pStructAr,0,0,0,0,0,0,0,0,0);
//task1函数
Task1Fun(intarg1)
{
struct_x*pStructx=(struct_x*)arg1;//将整型强制转化为结构体指针
…
}
在此提出“泛整型”的概念,(unsigned)char、(unsigned)shortint、(unsigned)int、(unsigned)longint等都属于这个范畴,指针必然属于“泛整型”的范围。
用指针的高超境界,也为将其看做一个“泛整型”。
看看软件的详细设计文档,其数据结构定义部分经常看到“INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64”或“BYTE、WORD、DWORD”等数据类型,它们在本质上都是(unsigned)char、(unsigned)shortint、(unsigned)int、(unsigned)longint宏定义的结果,都属于“泛整型”。
所以,“泛整型”的概念真实地体现在日常的软件设计当中。
正因为各种指针类型在本质上都是“泛整型”,因此它们可以互相转化:
inta,b;
memset((char*)&a,(char*)&b,sizeof(int));
等价于:
inta,b;
a=b;
从来没有人会用memset((char*)&a,(char*)&b,sizeof(int))来代替a=b,这里只是为了说明问题。
下面的代码则经常用到:
int*p=(int*)malloc(100*sizeof(int));
memset(p,0,100*sizeof(