1.引言
還記得當年學數學、英語都有個竅門,那就是搞個錯題集。經常複習一下這個錯題集,就可以避免下次犯同樣的錯誤。而幾乎所有的程序員都是從犯錯誤開始的,我們也很有必要總結一下編程新手的常見錯誤,本文的目的在於此。文中所列出的都是筆者在項目開發中接觸到的新手真實的言談,筆者學學文革腔調,姑且稱之為“錯誤語錄”。
2.語錄
(1)“我的程序都是對的,可結果不對”
想想你的周圍,是不是也有人說這樣的話?如果你也曾經說過,那就此打住,不要再說這句話,因為這句話只會顯示說話者的無知。既然程序都是對的,那為什麼結果不對?
(2)“程序=演算法+數據結構”
如果剛剛學完C語言,我們說這樣的話,完全可以理解,而且可以說是正確的。但是如果你是一位即將從事C/C++編程的程序員,那麼很遺憾,這個說法只能判錯,殊不知,世界上還有另一種說法:
程序 = 對象+ 消息
“程序=演算法+數據結構”只對面向過程的語言(C)成立,而對面向對象的語言(C++),則只能表述為“程序=對象+消息”。傳統的過程式編程語言以過程為中心以演算法為驅動,面向對象的編程語言則以對象為中心以消息為驅動。這裡的消息是廣義的,對象A調用了對象B的成員函數,可看作對象A給B發消息。
(3)“程序編出來,運行正確就行了”
運行正確的程序並不一定是好程序,程序員時刻要牢記的一條就是自己寫的程序不僅是給自己看的,要讓別人也能輕易地看懂。很遺憾,許多的編程新手不能清晰地駕馭軟體的結構,對頭文件和實現文件的概念含糊不清,寫出來的程序可讀性很差。
C程序採用模塊化的編程思想,需合理地將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求,在模塊的劃分上主要依據功能。模塊由頭文件和實現文件組成,對頭文件和實現文件的正確使用方法是:
(4)“數組名就是指針”
許多程序員對數組名和指針的區別不甚明了,他們認為數組名就是指針,而實際上數組名和指針有很大區別,在使用時要進行正確區分,其區分規則如下:
(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究竟應該怎麼用?筆者給出如下規則:
(9)“函數add編譯生成的符號就是add”
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;
}
這個程序考慮了什麼?
如果這三點中您只考慮到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只需要很短的一段時間。各種語言的發展歷史也體現了編程思想的發展史。我們學習一種語言,語法也許並不是最重要的,最重要的是蘊藏在語法外表下的深層特性和設計用意。
[admin via 研發互助社區 ] C/C++編程新手錯誤語錄已經有12338次圍觀
http://cocdig.com/docs/show-post-42327.html