01.30 C語言指針經典知識彙總

指針在C語言中是一塊很重要的內容,也是比較難理解的一塊內容,我們需要反覆理解反覆鞏固才可以對其有所瞭解。之前也分享過指針相關的筆記,但是都比較雜,本篇筆記彙總一下指針相關的內容,包含了挺多指針相關的基礎知識點。這篇筆記有點長,可以收藏下來慢慢閱讀。

複雜類型說明

以下這部分內容主要來自《讓你不再害怕指針》:

要了解指針,多多少少會出現一些比較複雜的類型,所以,先介紹一下如何完全理解一個複雜類型,要理解複雜類型其實很簡單。

一個類型裡會出現很多運算符,他們也像普通的表達式一樣,有優先級,其優先級和運算優先級一樣,所以我總結了一下其原則: 從變量名處起,根據運算符優先級結合,一步一步分析。

下面讓我們先從簡單的類型開始慢慢分析吧:

<code>int p; /<code>

這是一個普通的整型變量 。


<code>int *p; /<code>

首先從 P處開始,先與*結合,所以說明 P 是一個指針,然後再與 int 結合,說明指針所指向的內容的類型為 int 型。所以 P 是一個返回整型數據的指針。


<code>int p[3]; /<code>

首先從 P 處開始,先與[]結合,說明 P 是一個數組,然後與 int 結合,說明數組裡的元素是整型的,所以 P 是一個由整型數據組成的數組。


<code>int *p[3]; /<code>

首先從 P 處開始,先與[]結合,因為其優先級比 * 高,所以 P 是一個數組,然後再與 * 結合,說明數組裡的元素是指針類型,然後再與 int 結合,說明指針所指向的內容的類型是整型的,所以P 是一個由返回整型數據的指針所組成的數組 。


<code>int (*p)[3]; /<code>

首先從 P 處開始,先與 * 結合,說明 P 是一個指針然後再與[]結合與"()"這步可以忽略,只是為了改變優先級),說明指針所指向的內容是一個數組,然後再與 int 結合,說明數組裡的元素是整型的。所以 P 是一個指向由整型數據組成的數組的指針。


<code>int **p;/<code>

首先從 P 開始,先與後再與 * 結合,說明指針所指向的元素是指針,然後再與 int 結合,說明該指針所指向的元素是整型數據。由於二級以上的指針極少用在複雜的類型中,所以後面更復雜的類型我們就不考慮多級指針了,最多隻考慮一級指針。


<code>int p(int); /<code>

從 P 處起,先與()結合,說明 P 是一個函數,然後進入()裡分析,說明該函數有一個整型變量的參數然後再與外面的 int 結合,說明函數的返回值是一個整型數據。


<code>int (*p)(int);/<code>

從 P 處開始,先與指針結合,說明 P 是一個指針,然後與()結合,說明指針指向的是一個函數,然後再與()裡的int 結合,說明函數有一個 int 型的參數,再與最外層的int 結合,說明函數的返回類型是整型,所以 P 是一個指向有一個整型參數且返回類型為整型的函數的指針。


說到這裡也就差不多了,我們的任務也就這麼多,理解了這幾個類型,其它的類型對我們來說也是小菜了。不過我們一般不會用太複雜的類型,那樣會大大減小程序的可讀性,請慎用,這上面的幾種類型已經足夠我們用了。


分析指針的方法

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

<code>(1)int *ptr;
(2)char*ptr;
(3)int **ptr;
(4)int (*ptr)[3];
(5)int *(*ptr)[4];/<code>

1、指針的類型

從語法的角度看, 你只要把指針聲明語句裡的指針名字去掉, 剩下的部分就是這個指針的類型。 這是指針本身所具有的類型。 讓我們看看例一中各個指針的類型:

<code>(1)int*ptr;//指針的類型是 int*
(2)char*ptr;//指針的類型是 char*
(3)int**ptr;//指針的類型是 int**
(4)int(*ptr)[3];//指針的類型是 int(*)[3]
(5)int*(*ptr)[4];//指針的類型是 int*(*)[4]/<code>


2、指針所指向的類型

當你通過指針來訪問指針所指向的內存區時, 指針所指向的類型決定了編譯器將把那片內存區裡的內容當做什麼來看待。

從語法上看, 你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉, 剩下的就是指針所指向的類型。例如:

<code>(1)int*ptr; //指針所指向的類型是 int
(2)char*ptr; //指針所指向的的類型是 char
(3)int**ptr; //指針所指向的的類型是 int*
(4)int(*ptr)[3]; //指針所指向的的類型是 int()[3]
(5)int*(*ptr)[4]; //指針所指向的的類型是 int*()[4]/<code>

在指針的算術運算中, 指針所指向的類型有很大的作用。


3、指針的值

指針的值是指針本身存儲的數值, 這個值將被編譯器當作一個地址, 而不是一個一般的數值。 在 32 位程序裡, 所有類型的指針的值都是一個 32 位 整數, 因為 32 位程序裡內存地址全都是 32 位長。

指針所指向的內存區就是從指針的值所代表的那個內存地址開始, 長度為 sizeof(指針所指向的類型)的一片內存區。

以後, 我們說一個指針的值是 XX, 就相當於說該指針指向了以 XX 為首地址的一片內存區域; 我們說一個指針指向了某塊內存區域,就相當於說該指針的值是這塊內存區域的首地址。

指針所指向的內存區和指針所指向的類型是兩個完全不同的概念。 在例一中, 指針所指向的類型已經有了, 但由於指針還未初始化, 所以它所指向的內存區是不存在的, 或者說是無意義的。

以後, 每遇到一個指針, 都應該問問: 這個指針的類型是什麼? 指針指向的類型是什麼? 該指針指向了哪裡? (重點注意) 。


4、指針本身所佔據的內存區

指針本身佔了多大的內存? 你只要用函數 sizeof(指針的類型)測一下就知道了。 在 32 位平臺裡, 指針本身佔據了 4 個字節的長度。指針本身佔據的內存這個概念在判斷一個指針表達式(後面會解釋) 是否是左值時很有用。


指針的算術運算

指針可以加上或減去一個整數。 指針的這種運算的意義和通常的數值的加減運算的意義是不一樣的, 以單元為單位。

這在內存上體現為:相對這個指針向後偏移多少個單位或向前偏移了多少個單位,這裡的單位與指針變量的類型有關。在32bit環境下,int類型佔4個字節,float佔4字節,double類型佔8字節,char佔1字節。

【注意】一些處理整數的操作不能用來處理指針。例如,可以把兩個整數相乘,但是不能把兩個指針相乘。

示例程序

<code>#include <stdio.h>

int main(void)
{
\tint a = 10, *pa = &a;
\tfloat b = 6.6, *pb = &b;
\tchar c = 'a', *pc = &c;
\tdouble d = 2.14e9, *pd = &d;

\t//最初的值
\tprintf("pa0=%d, pb0=%d, pc0=%d, pd0=%d\\n", pa, pb, pc, pd);
\t//加法運算
\tpa += 2;
\tpb += 2;
\tpc += 2;
\tpd += 2;
\tprintf("pa1=%d, pb1=%d, pc1=%d, pd1=%d\\n", pa, pb, pc, pd);
\t//減法運算
\tpa -= 1;
\tpb -= 1;
\tpc -= 1;

\tpd -= 1;
\tprintf("pa2=%d, pb2=%d, pc2=%d, pd2=%d\\n", pa, pb, pc, pd);

\treturn 0;
}/<stdio.h>/<code>

運行結果為:

<code>pa0=6422268, pb0=6422264, pc0=6422263, pd0=6422248
pa1=6422276, pb1=6422272, pc1=6422265, pd1=6422264
pa2=6422272, pb2=6422268, pc2=6422264, pd2=6422256/<code>

解析:

舉例說明pa0→pa1→pa2的過程,其他類似。pa0+2*sizeof(int)=pa1,pa1-1*sizeof(int)=pa2。因為pa為int類型的指針,所以加減運算是以4字節(即sizeof(int))為單位地址向前向後偏移的。看下圖:

C語言指針經典知識彙總

如圖:pa1所指向的地址在pa0所指向地址往後8字節處,pa2指向地址在pa1指向地址往前4字節處。

從本示例程序中,還可以看出:連續定義的變量在內存的存儲有可能是緊挨著的,有可能是分散著的。


數組和指針的聯繫

數組與指針有很密切的聯繫,常見的結合情況有以下三種:

  • 數組指針
  • 指針數組
  • 二維數組指針

1、數組指針

數組指針:指向數組的指針。如:

<code>int arr[] = {0,1,2,3,4};
int *p = arr; //也可寫作int *p=&arr[0]/<code>

也就是說,p,arr,&arr[0]都是指向數組的開頭,即第0個元素的地址。

如果一個指針p指向一個數組arr[]的開頭,那麼p+i為數組第i個元素的地址,即&arr[i],那麼*(p+i)為數組第i個元素的值,即arr[i]。

同理,若指針p指向數組的第n個元素,那麼p+i為第n+1個元素的地址;不管 p 指向了數組的第幾個元素,p+1 總是指向下一個元素,p-1 也總是指向上一個元素。

下面示例證實了這一點:

<code>#include <stdio.h>

int main(void)
{
int arr[] = {0, 1, 2, 3, 4};
int *p = &arr[3]; //也可以寫作 int *p = arr + 3;

printf("%d, %d, %d, %d, %d\\n",
*(p-3), *(p-2), *(p-1), *(p), *(p+1) );
return 0;
}/<stdio.h>/<code>

運行結果為:

<code>0, 1, 2, 3, 4/<code>


2、指針數組

指針數組:數組中每個元素都是指針。如:

<code>int a=1,b=2,c=3;
int *arr[3] = {&a,&b,&c};/<code>

示例程序:

<code>#include <stdio.h>
int main(void)
{
int a = 1, b = 2, c = 3;

//定義一個指針數組
int *arr[3] = {&a, &b, &c};//也可以不指定長度,直接寫作 int *parr[]
//定義一個指向指針數組的指針
int **parr = arr;
printf("%d, %d, %d\\n", *arr[0], *arr[1], *arr[2]);
printf("%d, %d, %d\\n", **(parr+0), **(parr+1), **(parr+2));

return 0;
}/<stdio.h>/<code>

第一個 printf() 語句中,arr[i] 表示獲取第 i 個元素的值,該元素是一個指針,還需要在前面增加一個 * 才能取得它指向的數據,也即 *arr[i] 的形式。


第二個 printf() 語句中,parr+i 表示第 i 個元素的地址,*(parr+i) 表示獲取第 i 個元素的值(該元素是一個指針),**(parr+i) 表示獲取第 i 個元素指向的數據。

指針數組還可以和字符串數組結合使用,請看下面的例子:

<code>#include <stdio.h>
int main(void)
{
char *str[3] =
{
"hello C",
"hello C++",
"hello Java"
};
printf("%s\\n%s\\n%s\\n", str[0], str[1], str[2]);
return 0;
}/<stdio.h>/<code>

運行結果為:

<code>hello C
hello C++
hello Java/<code>


3、二維數組指針

二維數組指針:指向二維數組的指針。如:

<code>int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;/<code>

a [3] [4]表示一個3行4列的二維數組,其所有元素在內存中是連續存儲的。

請看如下程序:

<code>#include <stdio.h>
int main(void)
{
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int i,j;
for( i = 0; i < 3; i++ )
{
for( j = 0; j < 4; j++ )
{
printf("a[%d][%d]=%d\\n", i, j, &a[i][j]);
}
}

return 0;
}/<stdio.h>/<code>

運行結果為:

<code>a[0][0]=6422216
a[0][1]=6422220
a[0][2]=6422224
a[0][3]=6422228
a[1][0]=6422232

a[1][1]=6422236
a[1][2]=6422240
a[1][3]=6422244
a[2][0]=6422248
a[2][1]=6422252
a[2][2]=6422256
a[2][3]=6422260/<code>

可見,每個元素的地址都是相差4個字節,即每個連續在內存中是連續存儲的。


按照以上定義可歸納出如下4個結論:

(1)p指向數組a的開頭,也即第1行;p+1前進一行,指向第2行。

(2)*(p+1)表示取第2行元素(一整行元素)。

(3)*(p+1)+1表示第2行第2個元素的地址。

(4)((p+1)+1)表示第2行第2個元素的值。

綜上4點,可得出如下結論:

<code>a+i == p+i
*(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j)== *(*(p+i)+j)/<code>

以上就是數組與指針常用的三種結合形式。


指針與數組的區別

數組與指針在多數情況是可以等價的,比如:

<code>int array[10]={0,1,2,3,4,5,6,7,8,9},value;
value=array[0]; //也可寫成: value=*array;
value=array[3]; //也可寫成: value=*(array+3);
value=array[4]; //也可寫成: value=*(array+4) /<code>


但也有不等價的時候,比如如下三種情況:

  • 數組名不可以改變,而指向數組的指針是可以改變的。
  • 字符串指針指向的字符串中的字符是不能改變的,而字符數組中的字符是可以改變的。
  • 求數組長度時,借用數組名可求得數組長度,而借用指針卻得不到數組長度。

1、區別一

數組名的指向不可以改變,而指向數組的指針是可以改變的。

請看如下代碼:

<code>#include <stdio.h>

int main(void)

{
int a[5] = {0, 1, 2, 3, 4}, *p = a;
char i;

// 數組遍歷方式一
for ( i = 0; i < 5; i++ )
{
printf("a[%d] = %d\\n", i, *p++);
}

// 數組遍歷方式二
for ( i = 0; i < 5; i++ )
{
printf("a[%d] = %d\\n", i, *a++);
}

return 0;
}/<stdio.h>/<code>

數組遍歷方式一:使用指針遍歷數組元素,* p++等價於*(p++),即指針指向的地址每次後移一個單位,然後再取地址上的值。這裡的一個單位是sizeof(int)個字節。

數組遍歷方式二:使用數組名自增遍歷數組元素,編譯出錯,錯誤如下:

<code>error: value required as increment operand/<code>

因為數組名的指向是不可以改變的,使用自增運算符自增就會改變其指向,這是不對的,數組名只能指向數組的開頭。但是可以改為如下遍歷方式:

<code>for ( i = 0; i < 5; i++ )
{
printf("a[%d] = %d\\n", i, *(a+i));
}/<code>

這可以正確遍歷數組元素。因為*(a+i)與a[i]是等價的。


2、區別二

字符串指針指向的字符串中的字符是不能改變的,而字符數組中的字符是可以改變的。

請看如下代碼:

<code>//字符串定義方式一
char str[] = "happy";

//字符串定義方式二
char *str = "happy";/<code>

字符串定義方式一:字符串中的字符是可以改變的。如可以使用類似str[3]='q'這樣的語句來改變其中的字符。原因就是:這種方式定義的字符串保存在全局數據區或棧區,是可讀寫的。


字符串定義方式二:字符串中的字符是不可以改變的。原因就是:這種方式定義的字符串保存在常量區,是不可修改的。


2、區別三

求數組長度時,借用數組名可求得數組長度,而借用指針卻得不到數組長度。

請看如下代碼:

<code>#include <stdio.h>

int main(void)
{
int a[] = {0, 1, 2, 3, 4}, *p = a;
char len = 0;

// 求數組長度方式一
printf("方式一:len=%d\\n",sizeof(a)/sizeof(int));

// 求數組長度方式二
printf("方式二:len=%d\\n",sizeof(p)/sizeof(int));

return 0;
}/<stdio.h>/<code>

運行結果

<code>方式一:len=5
方式二:len=1/<code>

求數組長度方式一:借用數組名來求數組長度,可求得數組有5個元素,正確。


求數組長度方式二:借用指針求數組長度,求得長度為1,錯誤。原因是:

p只是一個指向int類型的指針,編譯器不知道其指向的是一個整數還是指向一個數組。sizeof(p)求得的是p這個指針變量本身所佔用的字節數,而不是整個數組佔用的字節數。


下面還需要注意數組名的一個問題: 聲明瞭一個數組 TYPE array[n] , 則數組名是一個常量指針, 該指針的值是不能修改的, 即類似 array++的表達式是錯誤的。


指針函數與函數指針

函數、指針這兩個詞結合的順序不同其意義也不同,即指針函數與函數指針的意義不同。

1、指針函數

指針函數的本質是一個函數,其返回值是一個指針。示例如下:

<code>int *pfun(int, int);/<code>

由於“*”的優先級低於“()”的優先級,因而pfun首先和後面的“()”結合,也就意味著,pfun是一個函數。即:int *(pfun(int, int));

接著再和前面的“*”結合,說明這個函數的返回值是一個指針。由於前面還有一個int,也就是說,pfun是一個返回值為整型指針的函數。

指針函數示例程序如下:

<code>#include <stdio.h>
//這是一個指針函數的聲明
int *pfun(int *arr, int n);

int main(void)
{
int array[] = {0, 1, 2, 3, 4};
int len = sizeof(array)/sizeof(array[0]);
int *p;
int i;

//指針函數的調用
p = pfun(array, len);

for (i = 0; i < len; i++)
{
printf("array[%d] = %d\\n", i, *(p+i));
}

return 0;
}

//這是一個指針函數,其返回值為指向整形的指針
int *pfun(int *arr, int n)
{
int *p = arr;

return p;
}/<stdio.h>/<code>

程序運行結果如下:

C語言指針經典知識彙總

主函數中,把一個數組的首地址與數組長度作為實參傳入指針函數pfun裡,把指針函數的返回值(即指向數組的指針)賦給整形指針p。最後使用指針p來遍歷數組元素並打印輸出。


2、函數指針

函數指針其本質是一個指針變量,該指針變量指向一個函數。C程序在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。函數指針示例:

<code>/*聲明一個函數指針 */
int (*fptr) (int, int);
/* 函數指針指向函數func */
fptr = func; // 或者fptr = &func;/<code>

func是一個函數名,那麼func與&func都表示的是函數的入口地址。同樣的,在函數的調用中可以使用:方式一:func(),也可以使用方式二:(*fun)()。這兩種調用方式是等價的,只是我們平時大多都習慣用方式一的調用方法。

至於為什麼func與&func的含義相同,《嵌入式Linux上的C語言編程實踐》這本書中有如下解釋:

對於函數func來說,函數的名稱就是函數代碼區的常量,對它取地址(&func)可以得到函數代碼區的地址,同時,func本身也可以視為函數代碼區的地址。因此,函數名稱和對其取地址其含義是相同的。

函數指針示例程序如下:

<code>#include <stdio.h>

int add(int a, int b);

int main(void)
{
int (*fptr)(int, int); //定義一個函數指針
int res;
fptr = add; //函數指針fptr指向函數add

/* 通過函數指針調用函數 */
res = (*fptr)(1,2); //等價於res = fptr(1,2);
printf("a + b = %d\\n", res);

return 0;
}

int add(int a, int b)
{
return a + b;
}/<stdio.h>/<code>

程序運行結果如下:

C語言指針經典知識彙總

以上就是關於指針函數與函數指針的簡單區分。其中,函數指針廣泛應用於嵌入式軟件開發中,其常用的兩個用途:調用函數和做函數的參數。


以上就是本次的分享,如有錯誤,歡迎指出!謝謝


分享到:


相關文章: