1. 程式人生 > >【c基礎知識】C指標詳解(經典,非常詳細)

【c基礎知識】C指標詳解(經典,非常詳細)

前言:複雜型別說明

    要了解指標,多多少少會出現一些比較複雜的型別,所以我先介紹一下如何完全理解一個複雜型別,要理解複雜型別其實很簡單,一個型別裡會出現很多運算子,他們也像普通的表示式一樣,有優先順序,其優先順序和運算優先順序一樣,所以我總結了一下其原則:從變數名處起,根據運算子優先順序結合,一步一步分析.下面讓我們先從簡單的型別開始慢慢分析吧:
  1. int p; //這是一個普通的整型變數
  2. int *p; //首先從P 處開始,先與*結合,所以說明P 是一個指標,然後再與int 結合,說明指標所指向的內容的型別為int 型.所以P是一個返回整型資料的指標
  3. int p[3]; 
    //首先從P 處開始,先與[]結合,說明P 是一個數組,然後與int 結合,說明數組裡的元素是整型的,所以P 是一個由整型資料組成的陣列
  4. int *p[3]; //首先從P 處開始,先與[]結合,因為其優先順序比*高,所以P 是一個數組,然後再與*結合,說明數組裡的元素是指標型別,然後再與int 結合,說明指標所指向的內容的型別是整型的,所以P 是一個由返回整型資料的指標所組成的陣列
  5. int (*p)[3]; //首先從P 處開始,先與*結合,說明P 是一個指標然後再與[]結合(與"()"這步可以忽略,只是為了改變優先順序),說明指標所指向的內容是一個數組,然後再與int 結合,說明數組裡的元素是整型的.所以P 是一個指向由整型資料組成的陣列的指標
  6. int **p; //首先從P 開始,先與*結合,說是P 是一個指標,然後再與*結合,說明指標所指向的元素是指標,然後再與int 結合,說明該指標所指向的元素是整型資料.由於二級指標以及更高階的指標極少用在複雜的型別中,所以後面更復雜的型別我們就不考慮多級指標了,最多隻考慮一級指標.
  7. int p(int); //從P 處起,先與()結合,說明P 是一個函式,然後進入()裡分析,說明該函式有一個整型變數的引數,然後再與外面的int 結合,說明函式的返回值是一個整型資料
  8. Int (*p)(int); //從P 處開始,先與指標結合,說明P 是一個指標,然後與()結合,說明指標指向的是一個函式,然後再與()裡的int 結合,說明函式有一個int 型的引數,再與最外層的int 結合,說明函式的返回型別是整型,所以P 是一個指向有一個整型引數且返回型別為整型的函式的指標
  9. int *(*p(int))[3]; //可以先跳過,不看這個型別,過於複雜從P 開始,先與()結合,說明P 是一個函式,然後進入()裡面,與int 結合,說明函式有一個整型變數引數,然後再與外面的*結合,說明函式返回的是一個指標,,然後到最外面一層,先與[]結合,說明返回的指標指向的是一個數組,然後再與*結合,說明數組裡的元素是指標,然後再與int 結合,說明指標指向的內容是整型資料.所以P 是一個引數為一個整資料且返回一個指向由整型指標變數組成的陣列的指標變數的函式.
說到這裡也就差不多了,我們的任務也就這麼多,理解了這幾個型別,其它的型別對我們來說也是小菜了,不過我們一般不會用太複雜的型別,那樣會大大減小程式的可讀性,請慎用,這上面的幾種型別已經足夠我們用了.

一、細說指標

指標是一個特殊的變數,它裡面儲存的數值被解釋成為記憶體裡的一個地址。要搞清一個指標需要搞清指標的四方面的內容:指標的型別、指標所指向的型別、指標的值或者叫指標所指向的記憶體區、指標本身所佔據的記憶體區。讓我們分別說明。

先宣告幾個指標放著做例子:
例一:
  1. (1)int*ptr;  
  2. (2)char*ptr;  
  3. (3)int**ptr;  
  4. (4)int(*ptr)[3];  
  5. (5)int*(*ptr)[4];  

1.指標的型別

從語法的角度看,你只要把指標宣告語句裡的指標名字去掉,剩下的部分就是這個指標的型別。這是指標本身所具有的型別。讓我們看看例一中各個指標的型別:
(1)int*ptr;//指標的型別是int*
(2)char*ptr;//指標的型別是char*
(3)int**ptr;//指標的型別是int**
(4)int(*ptr)[3];//指標的型別是int(*)[3]
(5)int*(*ptr)[4];//指標的型別是int*(*)[4]
怎麼樣?找出指標的型別的方法是不是很簡單?

2.指標所指向的型別

當你通過指標來訪問指標所指向的記憶體區時,指標所指向的型別決定了編譯器將把那片記憶體區裡的內容當做什麼來看待。
從語法上看,你只須把指標宣告語句中的指標名字和名字左邊的指標宣告符*去掉,剩下的就是指標所指向的型別。例如:
(1)int*ptr; //指標所指向的型別是int
(2)char*ptr; //指標所指向的的型別是char
(3)int**ptr; //指標所指向的的型別是int*
(4)int(*ptr)[3]; //指標所指向的的型別是int()[3]
(5)int*(*ptr)[4]; //指標所指向的的型別是int*()[4]

在指標的算術運算中,指標所指向的型別有很大的作用。
指標的型別(即指標本身的型別)和指標所指向的型別是兩個概念。當你對C 越來越熟悉時,你會發現,把與指標攪和在一起的"型別"這個概念分成"指標的型別"和"指標所指向的型別"兩個概念,是精通指標的關鍵點之一。我看了不少書,發現有些寫得差的書中,就把指標的這兩個概念攪在一起了,所以看起書來前後矛盾,越看越糊塗。

3.指標的值----或者叫指標所指向的記憶體區或地址

指標的值是指標本身儲存的數值,這個值將被編譯器當作一個地址,而不是一個一般的數值。在32 位程式裡,所有型別的指標的值都是一個32 位整數,因為32 位程式裡記憶體地址全都是32 位長。指標所指向的記憶體區就是從指標的值所代表的那個記憶體地址開始,長度為si zeof(指標所指向的型別)的一片記憶體區。以後,我們說一個指標的值是XX,就相當於說該指標指向了以XX 為首地址的一片記憶體區域;我們說一個指標指向了某塊記憶體區域,就相當於說該指標的值是這塊記憶體區域的首地址。指標所指向的記憶體區和指標所指向的型別是兩個完全不同的概念。在例一中,指標所指向的型別已經有了,但由於指標還未初始化,所以它所指向的記憶體區是不存在的,或者說是無意義的。
以後,每遇到一個指標,都應該問問:這個指標的型別是什麼?指標指的型別是什麼?該指標指向了哪裡?(重點注意)

4 指標本身所佔據的記憶體區

指標本身佔了多大的記憶體?你只要用函式sizeof(指標的型別)測一下就知道了。在32 位平臺裡,指標本身佔據了4 個位元組的長度。指標本身佔據的記憶體這個概念在判斷一個指標表示式(後面會解釋)是否是左值時很有用。


二、指標的算術運算

指標可以加上或減去一個整數。指標的這種運算的意義和通常的數值的加減運算的意義是不一樣的,以單元為單位。例如:
例二:
  1. char a[20];  
  2. int *ptr=(int *)a; //強制型別轉換並不會改變a 的型別
  3. ptr++;  
在上例中,指標ptr 的型別是int*,它指向的型別是int,它被初始化為指向整型變數a。接下來的第3句中,指標ptr被加了1,編譯器是這樣處理的:它把指標ptr 的值加上了sizeof(int),在32 位程式中,是被加上了4,因為在32 位程式中,int 佔4 個位元組。由於地址是用位元組做單位的,故ptr 所指向的地址由原來的變數a 的地址向高地址方向增加了4 個位元組。由於char 型別的長度是一個位元組,所以,原來ptr 是指向陣列a 的第0 號單元開始的四個位元組,此時指向了陣列a 中從第4 號單元開始的四個位元組。我們可以用一個指標和一個迴圈來遍歷一個數組,看例子:
例三:
  1. int array[20]={0};  
  2. int *ptr=array;  
  3. for(i=0;i<20;i++)  
  4. {  
  5.     (*ptr)++;  
  6.     ptr++;  
  7. }  

這個例子將整型陣列中各個單元的值加1。由於每次迴圈都將指標ptr加1 個單元,所以每次迴圈都能訪問陣列的下一個單元。

再看例子:
例四:
  1. char a[20]="You_are_a_girl";  
  2. int *ptr=(int *)a;  
  3. ptr+=5;  

在這個例子中,ptr 被加上了5,編譯器是這樣處理的:將指標ptr 的值加上5 乘sizeof(int),在32 位程式中就是加上了5 乘4=20。由於地址的單位是位元組,故現在的ptr 所指向的地址比起加5 後的ptr 所指向的地址來說,向高地址方向移動了20 個位元組。
在這個例子中,沒加5 前的ptr 指向陣列a 的第0 號單元開始的四個位元組,加5 後,ptr 已經指向了陣列a 的合法範圍之外了。雖然這種情況在應用上會出問題,但在語法上卻是可以的。這也體現出了指標的靈活性。如果上例中,ptr 是被減去5,那麼處理過程大同小異,只不過ptr 的值是被減去5 乘sizeof(int),新的ptr 指向的地址將比原來的ptr 所指向的地址向低地址方向移動了20 個位元組。
下面請允許我再舉一個例子:(一個誤區)

例五:
  1. #include<stdio.h>
  2. int main()  
  3. {  
  4.     char a[20]=" You_are_a_girl";  
  5.     char *p=a;  
  6.     char **ptr=&p;  
  7.     //printf("p=%d\n",p);
  8.     //printf("ptr=%d\n",ptr);
  9.     //printf("*ptr=%d\n",*ptr);
  10.     printf("**ptr=%c\n",**ptr);  
  11.     ptr++;  
  12.     //printf("ptr=%d\n",ptr);
  13.     //printf("*ptr=%d\n",*ptr);
  14.     printf("**ptr=%c\n",**ptr);  
  15. }  
誤區一、輸出答案為Y 和o
誤解:ptr 是一個char 的二級指標,當執行ptr++;時,會使指標加一個sizeof(char),所以輸出如上結果,這個可能只是少部分人的結果.
誤區二、輸出答案為Y 和a誤解:ptr 指向的是一個char *型別,當執行ptr++;時,會使指標加一個sizeof(char *)(有可能會有人認為這個值為1,那就會得到誤區一的答案,這個值應該是4,參考前面內容), 即&p+4; 那進行一次取值運算不就指向陣列中的第五個元素了嗎?那輸出的結果不就是陣列中第五個元素了嗎?答案是否定的.
正解: ptr 的型別是char **,指向的型別是一個char *型別,該指向的地址就是p的地址(&p),當執行ptr++;時,會使指標加一個sizeof(char*),即&p+4;那*(&p+4)指向哪呢,這個你去問上帝吧,或者他會告訴你在哪?所以最後的輸出會是一個隨機的值,或許是一個非法操作.
總結一下:
一個指標ptrold 加(減)一個整數n 後,結果是一個新的指標ptrnew,ptrnew 的型別和ptrold 的型別相同,ptrnew 所指向的型別和ptrold所指向的型別也相同。ptrnew 的值將比ptrold 的值增加(減少)了n 乘sizeof(ptrold 所指向的型別)個位元組。就是說,ptrnew 所指向的記憶體區將比ptrold 所指向的記憶體區向高(低)地址方向移動了n 乘sizeof(ptrold 所指向的型別)個位元組。指標和指標進行加減:兩個指標不能進行加法運算,這是非法操作,因為進行加法後,得到的結果指向一個不知所向的地方,而且毫無意義。兩個指標可以進行減法操作,但必須型別相同,一般用在陣列方面,不多說了。

三、運算子&和*

這裡&是取地址運算子,*是間接運算子。
&a 的運算結果是一個指標,指標的型別是a 的型別加個*,指標所指向的型別是a 的型別,指標所指向的地址嘛,那就是a 的地址。
*p 的運算結果就五花八門了。總之*p 的結果是p 所指向的東西,這個東西有這些特點:它的型別是p 指向的型別,它所佔用的地址是p所指向的地址。
例六:
  1. int a=12; int b; int *p; int **ptr;  
  2. p=&a; //&a 的結果是一個指標,型別是int*,指向的型別是
  3. //int,指向的地址是a 的地址。
  4. *p=24; //*p 的結果,在這裡它的型別是int,它所佔用的地址是
  5. //p 所指向的地址,顯然,*p 就是變數a。
  6. ptr=&p; //&p 的結果是個指標,該指標的型別是p 的型別加個*,
  7. //在這裡是int **。該指標所指向的型別是p 的型別,這
  8. //裡是int*。該指標所指向的地址就是指標p 自己的地址。
  9. *ptr=&b; //*ptr 是個指標,&b 的結果也是個指標,且這兩個指標
  10. //的型別和所指向的型別是一樣的,所以用&b 來給*ptr 賦
  11. //值就是毫無問題的了。
  12. **ptr=34; //*ptr 的結果是ptr 所指向的東西,在這裡是一個指標,
  13. //對這個指標再做一次*運算,結果是一個int 型別的變數。