1. 程式人生 > >曾讓我哭笑不得抓狂的C語言

曾讓我哭笑不得抓狂的C語言

1.關於+=以及-=

 這是兩個運算子,但你否有過這種經歷:

	int temp;
	char i
	for(i=0;i<MAX;i++)
	{
		...
		temp=+2;	//這裡本意是每次迴圈,temp都自增2,但是卻將'+='寫成了'=+',按照這種寫法,每次迴圈都為temp賦值正數2,與本意相差甚遠
	}

2. 關於意想不到的死迴圈

 unsigned char i;
​  for(i=0;i<256;i++)
​  {
​ ​        //something
​  }

        當我們用上述程式碼想實現一個小迴圈時,結果卻事與願違,這其實是死迴圈的另一種寫法,因為無符號變數i

最大隻有255,要命的是,編譯器並不會指出這個錯誤。

與之相類似的程式碼是:

​ unsigned char i;
​  for(i=10;i>=0;i--)
​  {
​ ​      //something
​  }

​ 這也是一個死迴圈,你看出什麼原因了嗎?無論i如何減,i都是大於等於0的。

        這就告訴我們對於每個變數型別的取值範圍要由清醒的認識。值得注意的是相同的變數型別對於不同的CPU構架和不同的編譯器會有不同的結果。比如int型別在大多數16CPU構架中佔用兩個位元組,但在32CPU中卻往往佔用4個位元組;char型別在絕大多數編譯器中都是有符號數,但在keil MDK中卻是無符號數,若是要在

keil MDK下定義有符號char型別變數,必須用signed顯式宣告。我曾讀過一本書,其中有一句話:“signed關鍵字也是很寬巨集大量,你也可以完全當它不存在,在預設狀態下,編譯器預設資料位signed型別”,這句話便是有異議的,我們應該對自己所用的CPU構架以及編譯器熟練掌握。

3. 關於'='和'=='

if(Value=0x01)
{
​      //something
}

​       當我們判斷一個變數是否等於0x01時,你是否也寫過類似上面的程式碼?C語言的創造者認為賦值運算子"="出現的概率要遠遠大於等於運算子"==",因此,我們正常邏輯中的"等於"符號(=)C語言中成了賦值運算子,而

C語言的"等於"運算子卻被兩個等於號(==)所代替。我之所以對這個事件耿耿於懷是因為我在大二的時候參加的C++二級上機考試,當我感覺很輕鬆的做完最後一道題後,卻發現運算的結果卻與邏輯相悖,經過除錯發現,有一個條件一直為真,我檢查了很多遍才發現出問題的邏輯將等於運算子寫成了賦值運算子。在if語句中給變數賦一個非零值,也難怪這個邏輯總是為真。

​ 編譯器同樣不對這個問題做出指導性建議,值得一提的是,如果你在Keilif語句中使用了賦值運算子,編譯器會給出警告。

​        避免這個問題的一個很好的辦法是使用良好程式設計習慣,比如上面的程式碼可寫為:

if(0x01==Value)
{
​      //something
}

        將常量值放到變數的前面,即使將等於運算子寫成賦值運算子,編譯器也能產生一個語法錯誤,因為將一個變數賦值給一個常量是非法的。

4. error: #7: unrecognized token

​        我在剛使用C語言以及Keil編譯器時,對於這個編譯器錯誤,有很深的印象。出現這個編譯錯誤的典型代表是在敲程式碼的時候輸入了中文標點!!

真是讓人感慨萬分的錯誤!我們這些與硬體打交道的程式設計師,為模數電生,為PCB死,為Debug奮鬥一輩子,吃需求的虧,上大小寫的當,最後死在標點上!!

5. 關於字母'O'和數字'0',以及字母'l'和數字'1' ,在嵌入式程式設計中很容易和暫存器打交道,一個CPU如果有兩個相同模組時,這些模組暫存器,往往使用數字0和數字1來區分模組0和模組1,比如,NXPARM7 串列埠模組的兩個接收緩衝暫存器分別為:U0RBRU1RBR,要命的是在鍵盤上字母O和數字0相距的還那麼近,你是否也有將上述暫存器寫成UORBRUlRBR的經歷,我是曾經在這方面糾結過一次,好在編譯器能指出這個未定義的字串。

6. sizeof()

        不知道有多少人和我曾經一樣,將這個關鍵字認為是一個庫函式。

              int i,j;
              j=sizeof(i); //對於這一句,當初壓根沒把它往關鍵字上想,這傢伙偽裝的實在夠好。

        既然提到它,不如多說一下,sizeof在計算變數所佔空間大小時,括號可以省略,而計算型別大小時,不能省略。什麼意思呢?還是上面的變數宣告,可以寫成j=sizeof(i)也可以寫成j=sizeof i,因為這是計算變數所佔空間大小;可以寫成j=sizeof(int),但不可以寫成j=sizeof int,因為這是計算資料型別大小。

         總體來說,關鍵字sizeof的具有一定的變態基礎的,在我還是小白的時候,曾經為下面的一道題傷過腦袋:

下面程式碼裡,假設在32位系統下,個sizeof計算的結果分別是多少?

             int *p=NULL;

             sizeof(p)的值是:

             sizeof(*p)的值是:

             int a[100]

            sizeof(a)的值是:

            sizeof(a[100])的值是:

            sizeof(&a)的值是:

            sizeof(&a[0])的值是:

            int b[100];

            void fun(int b[100])

            {

                    sizeof(b);

            }

          sizeof(b)的值為:

7 關於陣列越界

int a[30];
for(i=30;i>0;i--)
{
    a[i]=something;
}

這是個典型的陣列越界例子,最近我同事的一個程式中便出現了。不知道有多少同學遇到或將要遇到陣列越界問題,即便你定義了30個數組a[30],你也不可以為a[30]賦值,因為下標為30的元素已經越界了。所以說陣列下標定義的很奇特,它是從0開始的。但當我們還是新手的時候,最容易忽視這一點。幸好現在的有些編譯器會對這個越界產生警告資訊。

8. 關於巨集

#define MAX_TAST 4;

        這個錯誤編譯器會指出的,即便這樣,相信很多同學在最初的時候也不會在第一時間發現這句程式碼的最後多了一個分號。這個分號會導致一些編譯器報錯,因為巨集定義的結尾並不需要分號。

        同樣與define有關的是這樣一句:#define  "config.h",我便吃過類似暗虧,在編譯器的提示之下,看了幾遍才發現標頭檔案包含應該是#include "config.h"

        既然提到#define,還是說說它需要注意的幾個點,也是經常在資料上被提及的。

       a.使用#define時,括號一定要足夠。比如定義一個巨集函式,求x的平方:                

 #define SQR(x)  x*x   .............. 1

       或者這樣寫:               

#define SQR(x)  (x)*(x) ............... 2

       上面兩種都是有風險的,對於第一種定義,SQR(10+1)就會得到和我們的設想不一致的結果;第二種SQR(5*3)*SQR(5*3)也會得到和我們設想不一致的結果,因此更安全的定義方法是: 

#define SQR(x)  ((x)*(x))

       b.使用#define的時候,,意空格的使用。比如下面的例子:                

#define SQR  (x)  ((x)*(x))

       這已經不是SQR(x)函數了,編譯器會把認為定義了一個巨集SQR,代表(x)  ((x)*(x)),因為SQR(x)之間有空格。這點需要注意。

      c.使用'#'在字串中包含巨集引數。比如下面的例子:               

#define  SQR(x)  printf("The square of  x  is %d.\n",((x)*(x))")       
如果這樣使用巨集:           
   SQR(8)

       則輸出為:

                The square of  x  is 64.

        這個時候引號中的x被當做字串來處理了,而不是一個可以被巨集引數替換的符號.如果你想在字元中的x也被巨集引數替換,可以這麼來定義巨集:       

 #define  SQR(x)  printf("The square of "#x" is %d.\n",((x)*(x))")

       這樣得到的結果為:

               The square of 8 is 64.

        上面的這些例子,恐怕是網上隨處可見的,但真的會這麼用卻有待考證。下面給出一個我自己遇到的不加括號產生錯誤的例子。在嵌入式程式設計中,遇到讀取IO埠某一位的電平狀態的場合是在平常不過的了,比如在NXP的ARM7中,讀取埠P0.11的電平狀態並判斷是否為高電平,程式碼如下:

#define READSDA       IO0PIN&(1<<11)            //定義巨集,讀IO口p0.11的埠狀態,但並未使用足夠多的括號

//判斷p0.11埠是否為高電平,使用下述語句就是錯誤的:
if(READSDA==(1<<11))
{
     //是高電平,處理高電平的問題
}
編譯器在編譯後將巨集帶入,原if語句變為:
if(IO0PIN&(1<<11) ==(1<<11))
{
    //是高電平,處理高電平的問題
}
這樣的話,運算子'=='的優先順序是大於'&'的,從而IO0PIN&(1<<11) ==(1<<11))語句等效為IO0PIN&0x00000001,相當於判斷P0.1是否為高電平,與原意相差甚遠.

9. 陣列和指標

        在32位系統下,

        定義一個數組:

int a[10]={1,2,3,4,5,6,7,8,9,0};

        定義一個指標:

int *p;

        那麼,aa[0]&a&a[0]各表示什麼意思?

        那麼,sizeof(a)sizeof(a[0])sizeof(&a)sizeof(&a[0])的值各是什麼?

        如果,對指標p賦值:

       p=a

       並且通過編譯器模擬,得知現在p等於a等於0x0000 0200

       那麼,a+1=?

                &a+1=?

                p+1=?

                p[2]=?

               *(p+2)=?

              *(a+2)=?

        再如果

int *ptr=(int *)(&a+1);

        那,*(ptr-1)=? 

        世上最曖昧、最糾纏不清的,莫過於陣列名和指標。這一方面源於大學的教材並沒有重視這一塊,也源於教學時硬生生的將C語言和硬體分開。一方面,教材和教這一門的老師在開始時便向我們灌輸了“陣列名和指標很像,可以等同”的思想;另一方面,在學C語言的時候,並沒有系統的學過計算機硬體(定址、儲存、彙編),C語言是一個很接近硬體的高階語言,如果沒有處理器(包括微控制器等微處理器)的基礎知識,會導致非常多的同學怎麼都理解不透C語言的指標和陣列。

        當我們定義一個數組int a[10]時,編譯器會分配一塊記憶體,這塊記憶體的名字命名為a,這個a只是一個名字,只是方便編譯器和程式設計者使用,編譯器並不為這個名字分配空間來儲存它。我們可以用a[0]a[1]來訪問陣列內的元素。a作為右值(位於等號的右邊)時,表示的是陣列第一個元素的地址(意義與&a[0]一樣,但&a[0]是一個指標變數,編譯器會為他分配空間,a卻不一樣,編譯器並不為它分配什麼空間),而並非陣列首地址,&a才表示陣列的首地址。

        所以,第一個問題,a是這個陣列所在的記憶體的名字,當它為右值時,表示陣列首元素的地址,a[0]是陣列的第一個元素,其值等於1&a是整個陣列的首地址,它是一個指標;&a[0]是陣列首元素的地址,它的值和a做右值時一樣,但意義不同,因為&a[0]是一個指標,編譯器要為它分配儲存空間,但a卻不會被分配儲存空間,a也不是指標型變數。

        明白了上面那些,關於sizeof的計算也就不會困難了:

                 sizeof(a)=4*10=40,因為a代表的是整塊陣列記憶體;

                 sizeof(a[0])=4,這相當於計算int的大小,在32位系統下,int4個位元組。

                 sizeof(&a)sizeof(&a[0])都是計算指標變數的大小,在32位系統下,指標變數佔4個位元組。

        對於最後一個問題,涉及到指標的加減。

       指標的加減中有一個重要的原則就是它的加減跟我們普通意義上的加減不是一個概念,它是按指標所指型別的記憶體大小來進行加減的。當我還是一個新手的時候,對於p++p+1這類指標運算的含義超出了我的意料之外,在上例中,若是p=0x0000 0200,那麼p++運算之後的p值應該為0x0000 0204。有多少同學,曾經把它計算成0x0000 0201! 

        陣列名a在做計算的時候表示陣列首元素的地址,這時候a等於0x0000 0200,所以a+1等於0x0000 0200+4=0x0000 0204,因為一個int型在32位系統下佔用4個位元組。&a是整個陣列的首地址,&a+1=0x0000 0200+4*10=0x0000 0228

        其它的也都比較好理解,p+1=0x0000 0200+04=0x0000 0204、 p[2]=3、 *(p+2)=3*(ptr-1)=0. 

10.  3/(-3)=?

       3%(-2)=?

   (-3)%2=?

         拋開它是否有實際的意義,這個看似簡單的語句,不知道有多少同學不確定結果到底是什麼。

         其實大多數的編譯器遵循這樣一個規定:餘數與被除數的正負號相同,被除數等於結果乘以除數加上餘數。所以,以上的三個結果分別為-11-1

11. 指標陣列與陣列指標

       有一段時間,我怎麼都不能區分指標陣列和陣列指標,就像下面的宣告:

int * p1[10];
int (*p2)[10];

        首先,要來解釋一下什麼是指標陣列,什麼是陣列指標:指標陣列首先是一個數組,它的成員都是指標型變數;陣列指標首先是一個指標,這個指標指向一個數組(它的值和陣列名錶示的值一樣,只是陣列指標是一個變數,編譯器要為它分配儲存空間,但陣列名類似於一個常亮,是編譯器在編譯階段就確定好的一個值,編譯器不會為它分配儲存空間)。

        對於p1,由於中括號的優先順序(關於優先順序,後面會專門提起)是大於*的,所以p1首先與'[]'相結合,構成一個數組,在這個陣列之前又有一個'*'運算子,說明這是定義一個指標陣列(int *a:定義一個指標a,這裡可以將p1[10]替換成a,就不難理解了),陣列的元素都是指向int型的指標。

         對於p2'()'雖然與'[]'為同一優先順序,但卻是表示式結合方向從左到右結合的,所以編譯器會先處理(*p2),這是典型的定義一個指標,只不過這個指標指向一個包含10int型資料的記憶體塊。為了加強理解,這裡給出兩個相同原理的函式宣告:

                  void * p1(void);  ---------------------- 宣告1,定義一個返回值是void型別指標的函式p1

                  void (*p2)(void); ----------------------- 宣告2,定義一個函式指標,該函式不返回任何值

       有了上面的鋪墊,現在定義一個高階C語言程式設計技巧中常用的函式指標陣列應該很容易了吧!首先這是一個數組,陣列的元素是指向一個函式的指標,以定義一個引數為空,返回值為int型別的函式指標陣列p1為例:

int (*p1[5])(void);

   分析如下:

       定義一個返回值為int型別的函式指標p1應該是:

int (*p1)(void);

        那麼將這類指標放到一個數組中不正是我們需要的定義嗎,套用指標陣列的定義方法,返回值為int型別函式指標陣列定義為:int (*p1[5])(void);

12 .運算子的優先順序

        C語言有32個關鍵字卻有44個運算子!運算子之間有固定的優先順序,雖然它們可以分成15類優先順序,但如果讓一個程式設計師完全記住這些運算子之間的優先順序關係,怕是老手也是不容易的吧。如果你的程式只是語法錯誤,這類錯誤是最容易解決的,編譯器就會幫你檢測出來;如果是你自己的邏輯出現錯誤,那麼根據執行結果仔細檢查一下程式碼可能也不難發現;但若是你的邏輯正確卻記錯了運算子的優先順序關係,導致程式執行結果和你設想的不同,這種錯誤就很難查出了,因為在你的潛意識裡,已經把這種錯誤點當成理所當然不用關注的。

        請看下面一句程式碼代表什麼意思:

*string ++;

          由於*++但是單目運算子,優先順序相同,但結合方向卻是自右向左,那麼*string++應該就是*string++),取出當前字元後將指標後移。不知道有沒有人把它認為是(*string)++,即取指標string所指向的物件,然後將該物件增1.  

        我曾經在程式碼中不止一次的出現過因為優先順序問題而導致的程式邏輯錯誤,那個時候我並沒有完整的記過優先順序,二十使用了一種“偷巧”的方法:只是簡單記住前幾級優先順序,其它自己沒把握的一律使用括號。這種方法我現在是不推薦的,一是因為大量的括號影響程式碼閱讀和程式的簡潔,二是總有時候我們稍微一鬆懈,就忘記了加括號,而後一種情況,正是很多人可能會遇到的。比如下面一句程式碼,無符號8位變數ucTimeValue 中存放十進位制編碼的資料23,我想將十進位制編碼轉成16進位制編碼,程式碼為:

temp8=(ucTimeValue>>4)*10+ucTimeValue&0x0F;     //十進位制轉化為16進位制,但忽略了運算子'+'的優先順序是大於運算子'&'的

        像這類程式碼編譯肯定可以通過,但執行的結果卻出乎我的意料,而且由於我先入為主的錯誤思想,要在一大段程式碼中發現這個錯誤著實要花費一番功夫。 

再例如,如果我想判斷一個暫存器的某一位是否為零,假如是判斷暫存器IO0SETbit17是否為零,但程式碼卻寫成了這樣:

 if(IO0SET&(1<<17)==0) 

        這樣寫其實是得不到正確的結果的,因為我忽略了"=="的優先順序是大於"&".按照上面的程式碼分析:因為"=="的優先順序大於"&",所以程式先判斷(1<<17)是否等於0?發現這是不相等的,所以(1<<17)==0表示式的值為假,即為00(&)上任何一個數都是0,所以IO0SET&(1<<17))==0整個表示式的值永遠為0,這與原意相差甚遠。 

      按照原意,應該這樣寫:

 if((IO0SET&(1<<17)))==0)

       其實,運算子的優先順序是有一定的規律可循的,下面給出優先順序口訣(注:口訣來源於網際網路)

優先順序口訣

       括號成員第一;                 括號運算子[]() 成員運算子.  ->

      全體單目第二;                  所有的單目運算子比如++ -- +(正) -(負) 指標運算*&

      乘除餘三,加減四;             這個"餘"是指取餘運算即%

      移位五,關係六;              移位運算子:<< >> ,關係:> < >= <= 等

      等於(與)不等排第七;       即== !=

      位與異或和位或;             這幾個都是位運算: 位與(&)異或(^)位或(|) 

      "三分天下"八九十;  

      邏輯或跟與;                     邏輯運算子:|| 和 &&

      十二和十一;                     注意順序:優先順序(||)  底於 優先順序(&&) 

      條件高於賦值,                    三目運算子優先順序排到 13 位只比賦值運算子和","高

     逗號運算級最低!              逗號運算子優先順序最低