歡迎您光臨本站 登入註冊首頁

C/C++編程新手錯誤語錄

admin @ 2014-03-25 , reply:0

概述

1.引言  還記得當年學數學、英語都有個竅門,那就是搞個錯題集。經常複習一下這個錯題集,就可以避免下次犯同樣的錯誤。而幾乎所有的程序員都是從犯錯誤開始的,我們也很有必要總結一下編程新手的常見錯誤,本文……

1.引言
  還記得當年學數學、英語都有個竅門,那就是搞個錯題集。經常複習一下這個錯題集,就可以避免下次犯同樣的錯誤。而幾乎所有的程序員都是從犯錯誤開始的,我們也很有必要總結一下編程新手的常見錯誤,本文的目的在於此。文中所列出的都是筆者在項目開發中接觸到的新手真實的言談,筆者學學文革腔調,姑且稱之為“錯誤語錄”。

2.語錄

  1. "我的程序都是對的,可結果不對"
  2. "程序=演算法+數據結構"
  3. "程序編出來,運行正確就行了"
  4. "數組名就是指針"
  5. "整形變數為32位"
  6. "switch和if …else…可隨意替換"
  7. "免得麻煩,把類裡面的成員函數都搞成public算了"
  8. "我想用malloc"、"我用不好malloc"
  9. "函數add編譯生成的符號就是add"
  10. "沒見過在C語言中調用C++的函數"、"C/C++不能調用Basic、Pascal語言的函數"
  11. "英語、數學不好就學不好C/C++"
  12. "C++太難了,我學不會"
  13. "整型變數僅僅意味著一個整數"
  14. "值傳遞一定不會改變參數"
  15. "C不高級,學C++、JAVA、C#才夠味"
  16. "語言學得越多越好"

(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,防止其成為“野”指針。

(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;
}

  這個程序考慮了什麼?

  1. 程序要強大:為了實現鏈式操作,將目的地址返回,函數返回類型改為char *
  2. 程序要可讀:源字元串指針參數加const限制,表明為輸入參數
  3. 程序要健壯:驗證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只需要很短的一段時間。各種語言的發展歷史也體現了編程思想的發展史。我們學習一種語言,語法也許並不是最重要的,最重要的是蘊藏在語法外表下的深層特性和設計用意。


[admin via 研發互助社區 ] C/C++編程新手錯誤語錄已經有12787次圍觀

http://cocdig.com/docs/show-post-42327.html