C語言指針快速解惑

http://www.rupeng.com/forum/thread-233-1-1.html

http://www.rupeng.com/forum/thread-234-1-1.html

http://www.rupeng.com/forum/thread-236-1-1.html

 

 

 

 

 

 

 

1、
int a[3] = {2,5,8};
int* p = a;
把數組名賦值給指針表示將數組的首元素的地址賦予此指針。
2、
int a[3] = {2,5,8};
int* p = &a[0];
a[0]是a數組的首元素,而&則是取地址運算符,所以“ &a[0]”取得的同樣是a數組的首元素的地址,因此這段代碼的含義和代碼段是一致的。
3、
char * c1 = "Hello";
char c2[6] = "World";
這兩句不都是聲明一個字符串嗎?有什麼區別嗎?
"Hello"是一個字符串,然後讓char指針c1指向這個字符串的首元素的地址。
第二句是聲明一個長度爲6的char類型數組,並且設置數組的初始值爲"World"。注意末尾還有一個隱藏的“/0”,所以長度是6,而不是5。
4、數組指針的加減運算
對於指向數組的指針變量可以進行加減運算,比如對於指向數組的指針p,p++、p--、p+2等都是合法的。這裏的加減運算並不是針對數組元素的,而是針對指針位置的,比如p當前指在首元素上,那麼p++後就指在第二個元素上;比如p當前指在第5個元素上,那麼p=p-2後,p就指在第3個元素上。
例子:
char * c1 = "Hello";
printf("%c/n",*c1);//輸出H
c1++;
printf("%c/n",*c1);//輸出e
5、指針之間的運算
兩個指針之間可以進行減法運算,只有指向同一個數組的兩個指針之間進行減法運算纔有意義,而指向不同數組的兩個指針之間進行減法運算則沒有意義。爲什麼呢?
    指針其實就是內存中的地址,兩個指針的減法運算計算的就是兩個內存地址之間的元素的個數,比如整數指針p1指向內存地址2008H,整數指針p2內存地址2000H,而整數佔4個字節,所以p1-p2的結果就是(2008H-2000H)/4=2,也就是 p1和p2 之間相差 2 個元素。很顯然兩個指針進行加法運算或者兩個指向不同變量的指針進行減法運算都是沒有意義的。
例子:
int a1[5]={3,5,6,8,2};
int* p1 = a1;//p1是指向第0個元素的地址
int* p2 = &a1[3];//p2指向的是第3個元素的地址
p1++;//p1指向了第1個元素
printf("%d",p2-p1);//輸出2
6、指針之間的大小比較
指向同一個數組的兩個指針之間進行大小的比較是有意義的。比較結果爲地址高低的比較:
例子:
int a1[5]={3,5,6,8,2};
int* p1 = a1;
int* p2 = &a1[3];
p1++;
printf("%d/n",p2<p1);//輸出0,因爲p2的地址比p1高2位
p1=p1+2;//p1在數組內向高位移動兩位
printf("%d/n",p2==p1);//輸出1 ,因爲p1和p2的位相等

7、數組做爲參數傳遞給函數
可以將數組做爲傳遞給函數,比如下面的代碼就是將傳入輸入的每個元素乘以2:
void makeDoule(int arr[],int len)
{
int i=0;
for(i=0;i<len;i++)
{
  arr[i]= arr[i]*2;
}
}
int main(int argc, char *argv[])
{
int a1[5]={3,5,6,8,2};
int i = 0;
int len=sizeof(a1)/sizeof(int);
makeDoule(a1,len);
for(i=0;i<len;i++)
{
  printf("%d ",a1[i]);
}
}
運行結果:
6 10 12 16 4傳遞給makeDoule函數的是a1,我們知道,一個數組的名字其實就是指向數組首元素的地址。所以makeDoule函數得到的arr就是數組a1的指針,那麼在函數內部對arr進行操作的話就會改變a1數組。
所以makeDoule函數也可以寫成下面的樣子,這兩個函數是等價的:
void makeDoule(int* arr,int len)
{
int i=0;
for(i=0;i<len;i++)
{
  arr[i]= arr[i]*2;
}
}
當然寫成下面的形式更符合指針的使用習慣,因此推薦這種用法:
void makeDoule(int* arr)
{
int i=0;
for(i=0;i<5;i++)
{
  //arr+i指向的值設置爲arr+i指向的值的二倍。
  *(arr+i)= *(arr+i)*2;
}
}
有的同學可能會問,爲什麼makeDoule函數還需要傳遞數組的長度len呢?makeDoule函數這樣寫不就行了嗎?
void makeDoule(int* arr)
{
int i=0;
for(i=0;i<5;i++)
{
  arr[i]= arr[i]*2;
}
}
這樣寫在這個函數中是沒有問題的。但是這個makeDoule函數不一定爲只爲a1服務,函數的最重要的特徵是可以複用,也就是可以被其他地方調用,如果我們想用makeDoule函數爲另外一個長度爲20的數組進行“每個元素乘以2”的操作,那麼將數組長度5寫死在函數中就會有問題了。
那麼爲什麼不在函數內部計算數組的長度呢?省得還傳遞一個len參數
void makeDoule(int arr[])
{
int i=0;
int len = sizeof(arr)/sizeof(int);
for(i=0;i<len;i++)
{
  arr[i]= arr[i]*2;
}
}
int main(int argc, char *argv[])
{
int a1[5]={3,5,6,8,2};
int i = 0;
int len=sizeof(a1)/sizeof(int);
makeDoule(a1);
for(i=0;i<len;i++)
{
  printf("%d ",a1[i]);
}
}
運行以後程序竟然輸出了:6 5 6 8 2
只有第一個元素被“乘以2”。爲什麼呢?
運行下面的程序試一試:
void test(int arr[])
{
printf("%d/n",sizeof(arr)/sizeof(int));
}
int main(int argc, char *argv[])
{
int a1[5]={3,5,6,8,2};
printf("%d/n",sizeof(a1)/sizeof(int));
test(a1);
}
運行結果竟然是:
5
1爲什麼在main函數中計算數組的大小是5,在test函數中計算數組arr的大小就變成了1了呢?在C語言中可以通過sizeof的方式取得一個數組的尺寸,這是我們已經知道的。但是一旦把這個數組傳遞給函數的時候,到了函數內部使用的就是指向這個數組的指針了,雖然在test函數中arr聲明的是數組,但是這裏的arr只是一個指針而已了,arr本質上只是一個int類型的指針,而int類型的指針的大小是4,所以sizeof(arr)/sizeof(int)的結果就是1。這點是經常容易犯錯的,需要特別注意。如果對指針還有什麼不清楚的,可以到這個網址查詢指針的學習資料:[url]http://www.rupeng.com/forum/forumdisplay.php?fid=4[/url]   ,指針是比較不好理解的,因此學習過程中要多試驗,多思考,不要氣餒。8、多維數組的指針
設有一個二維數組int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}}
(1)C語言允許把一個二維數組分解爲多個一維數組來處理。因此數組 a 可分解爲三個一維數組,即 a[0],a[1],a[2]。每一個一維數組又含有四個元素。例如 a[0]數組,含有 a[0][0],a[0][1],a[0][2],a[0][3]四個元素。
(2)從二維數組的角度來看,a 是二維數組名,a 代表整個二維數組的首地址,也是二維數組 0 行的首地址。
特別注意a+1表示第1行的首地址,而不是第0行第第1列的地址,這是初學者最容易犯錯的地方。
同樣a[1]也是第1行一維數組的數組名和首地址。
所以a+1、*(a+1)、[1]是等價的。
(4)在二維數組中不能把&a[i]理解爲元素 a[i]的地址,因爲a[i]不是一個數組元素,a[i]是一種地址計算方法,它本身就表示數組 a 第 i 行首地址。所以&a[i]和 a[i]是等價的。這一點也是初學者最容易犯錯的地方。
(5)從上邊的分析我們得知a[0]+1是 a[0]的 1 號元素首地址,由此可得出 a[i]+j 則是一維數組 a[i]的 j 號元素首地址,它等於&a[i][j]。
由 a[i]=*(a+i)得 a[i]+j=*(a+i)+j。由於*(a+i)+j 是二維數組 a 的 i 行 j 列元素的首地址,所以,該元素的值等於*(*(a+i)+j)。
理解了下面的算法也就理解了多維數組的指針問題:
int main(int argc, char *argv[])
{
int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}};
printf("%d/n",*(*(a+1)+2));
printf("%d/n",*(a[1]+2));
}
程序輸出如下:
6
6兩個表達式都輸出a[1][2]的值。*(a+1)則表示二維數組a的第1行,也就是等價於a[1]。a[1]、*(a+1)都表示數組的第1行。所以*(a[1]+2))和*(*(a+1)+2)都表示數組的第1行的第2個元素的值。
有的同學會問了,既然“*(a+1)”和“a+1”是等價的,爲什麼“printf("%d/n",*((a+1)+2)))”輸出結果是錯誤的呢?
編譯器在編譯“*((a+1)+2)))”的時候會把“(a+1)+2”優化成“a+3”,因此“*((a+1)+2)))”就變成了“*(a+3)”,也就是a數組第3個一維數組的首地址,顯然這個只是一個地址,並不是我們想像中的a[1][2]的值,所以輸出了一個非常大的數。爲了避免編譯器的這種誤解,9、函數指針
    在C語言中,一個函數總是佔用一段連續的內存區,而函數名就是該函數所佔內存區的首地址。我們可以把函數的這個首地址(或稱入口地址)賦予一個指針變量,使該指針變量指向該函數。然後通過指針變量就可以找到並調用這個函數。我們把這種指向函數的指針變量稱爲“函數指針變量”。
函數指針變量定義的一般形式爲:
函數的返回值的類型  (*指針變量名)(參數列表);
其中“參數列表”可以省略,不過建議明確標明“參數列表”。
例子:
void PrintIt(int i)
{
printf("%d/n",i);
}
int main(int argc, char *argv[])
{
int i = 0;
int arr[5] = {3,5,8,2,1};
void (*myAction)(int);
myAction = PrintIt;
for(i=0;i<sizeof(arr)/sizeof(int);i++)
{
  myAction(arr);
}
}
上面的程序遍歷數組arr的所有元素,並且打印每個元素。
有的同學會說,這樣做有什麼意義嗎?把“myAction(arr)”直接換成“PrintIt(arr)”不就得了嗎?
這麼替換在這裏是非常合理,也是非常正確的,但是有一天我發現很多地方都要遍歷數組做不同的事情,爲了避免每次都寫for循環,我將遍歷數組的功能抽取到一個單獨的公共函數中完成
void eachItem(int* pArray,int len,void (*action)(int))
{
int i = 0;
for(i=0;i<len;i++)
{
  //調用函數指針
  action(*(pArray+i));
}
}
注意函數eachItem的最後一個參數爲函數指針類型。
這樣main函數就可以簡化成如下的樣子了:
void PrintIt(int i)
{
printf("%d/n",i);
}
int main(int argc, char *argv[])
{
int arr[5] = {3,5,8,2,1};
int len = sizeof(arr)/sizeof(int);
eachItem(arr,len,PrintIt);
}
以後在其他的地方想對int數組做其他處理,那麼只要寫不同的函數就可以了,比如說要將數組中的奇數輸出:
void PrintOdd(int i)
{
if((i%2)==1)
{
  printf("%d是奇數/n",i);
}
}
在main函數中如下調用即可:
eachItem(arr,len,PrintOdd);
可以看到通過函數指針,將循環遍歷算法和具體的處理算法隔離了,實現了代碼的複用。
函數指針在編程中有非常多的應用。函數指針有時候又被稱爲回調,在事件機制、模板算法等場合有着廣泛應用,因此一定要掌握好。MFC、STL等流行的框架中都大量的應用了函數指針,在rupeng.com的《C語言也能幹大事》系列在線教學中也將進一步講解函數指針的更生動的應用。
附錄:本小節用到的代碼:
void PrintIt(int i)
{
printf("%d/n",i);
}
void PrintOdd(int i)
{
if((i%2)==1)
{
  printf("%d是奇數/n",i);
}
}
void eachItem(int* pArray,int len,void (*action)(int))
{
int i = 0;
for(i=0;i<len;i++)
{
  action(*(pArray+i));
}
}
int main(int argc, char *argv[])
{
int arr[5] = {3,5,8,2,1};
int len = sizeof(arr)/sizeof(int);
eachItem(arr,len,PrintIt);
eachItem(arr,len,PrintOdd);
}
10、指針數組、指針的指針。
指針數組的經典用途就是聲明字符串數組:
  static char *name[]={"Monday",
                        "Tuesday",
                        "Wednesday",
                        "Thursday",
                        "Friday",
                        "Saturday",
                        "Sunday"};
這兩部分請同學們參考教材。
11、結構指針
對結構的訪問一般形式爲:
結構變量.成員名
    (*結構指針變量).成員名
或爲:
    結構指針變量->成員名
    應該注意(*pstu)兩側的括號不可少,因爲成員符“.”的優先級高於“*”。
12、結構指針變量作函數參數
允許用結構變量作函數參數進行整體傳送。但是這種傳送要將全部成員逐個傳送,特別是成員爲數組時將會使傳送的時間和空間開銷很大,嚴重地降低了程序的效率。因此最好的辦法就是使用指針,即用指針變量作函數參數進行傳送。
13、動態存儲分配
注:“C語言中不允許動態數組”是C89標準中的規定,所以TC、VC6等C89等老的編譯器會有這個問題,新的C99中已經不存在這個問題。
C語言中不允許動態數組類型。例如下面的代碼是錯誤的:
int n;
scanf("%d",&n);
int a[n];
但是在實際的編程中,往往會發生這種情況,即所需的內存空間取決於實際輸入的數據。對於這種問題,用數組的辦法很難解決。爲了解決上述問題,C語言提供了一些內存管理函數,這些內存管理函數可以按需要動態地分配內存空間,也可把不再使用的空間回收。
(1)分配內存空間函數 malloc
調用形式:
  (類型說明符*)malloc(size)
  功能:在內存的動態存儲區中分配一塊長度爲"size"字節的連續區域。函數的返回值爲該區域的首地址。
(2)釋放內存空間函數 free
調用形式:
free(void*ptr);
建議大家表示“二維數組a的第1行”的時候用a[1]或者*(a+1),而儘量不要用(a+1)因爲很容易出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章