一、指針的基本常識
1.什麼是指針?
對於指針需要掌握以下幾個基本的概念:
<1>指針是一種變量類型,也是具有空間、內容、地址三個屬性。空間屬性值得是:在定義指針時,內存會給指針變量分配指定類型大小的空間,空間的大小爲四個字節(32位平臺下)。內容屬性指的是:指針變量內部存儲的數據,即存放的是地址。因此,通常也會說指針就是地址,地址就是指針,要注意的是這裏的指針指的是指針的內容。
<2>指針是有類型的,其類型決定了指針訪存時的步長,即指針解引用操作時的權限。比如:char * 類型的指針在通過指針間接訪存(對其解引用)時的步長爲1字節,而 int * 、float * 類型的指針爲4字節,double * 爲8字節。
<3>一個地址唯一標識一塊內存空間。
2.正確規避野指針(懸垂指針)
什麼是野指針?野指針指的是指針指向不明確的指針。
野指針形成的原因:<1>定義指針時未初始化。
<2>指針訪問內存時,進行 越界訪問。
<3>指針指向的空間被釋放(malloc函數開闢空間後,使用結束時會free掉開闢的空間)。
正確規避野指針:<1>定義指針時要初始化,沒有明確的指向空間時,賦值爲NULL。
<2>使用指針對內存進行訪問時,注意防止越界。
<3>指針指向的空間被釋放後,及時將指針置爲NULL。
<4>使用指針前,檢查指針的有效性(在使用指針進行函數傳參時,在函數內部使用assert()函數對傳入的指針進行校驗)。
3.指針的運算
<1>指針與整數的加減:對指針變量加上或者減去一個整數,實際上加/減去的是其所指向類型的大小。
type *a + n == a + sizeof(type),這裏n是常數。、
<2>指針減指針:我們通常對指針進行減法運算,是在同一個字符串或者數組下進行,其結果代表的是兩個指針之間元素的個數。
<3>指針的大小關係比較:允許指向數組元素的指針與指向數組最後一個元素後面的那個內存位置的指針比較,但是不允許
與指向第一個元素之前的那個內存位置的指針進行比較,同時不能直接對指向末位元素後的內存進行賦值。
4.指針和數組
#include <stdio.h>
#include <windows.h>
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
system("pause");
return 0;
}
由結果可以看出,打印的三個地址的結果在數值上是完全相等的。
arr(數組名)代表的是數組首元素的地址,因此前兩個打印的結果相同;而 &arr 代表的是整個數組的地址,它在數值上是與數組首元素的地址是相等的。如何能快速鑑別兩個的差別,我們可以對其+1,結果如下:
從結果可以看出,對 arr+1 ,其地址之間的差值爲4byte,正好是數組中一個元素的大小;而對 &arr+1,其地址之前的差值爲40byte,正好是整個數組的大小。究其原因是由於兩個(地址)指針的類型不相同,arr是一個整形指針,而&arr是數組指針,下邊將對數組指針加以說明,這也與我們上邊所說的對指針+1,加的是其所指向類型的大小。
二、指針數組、數組指針、函數指針、多級指針
1.指針數組、數組指針
指針數組、數組指針,這兩個到底是怎麼一回事呢?
在C語言中,這是兩種不相同的變量類型。數組指針是指針,通常在數組傳參的時候會應用到此概念;而指針數組是數組,是用來存儲指針的數組,其元素爲指針。
<1>指針數組
定義指針數組的方式爲(以整型指針數組爲例):int *arr[5];
解釋:arr先和[]結合,說明arr是一個數組,其類型爲 int *。
如下圖所示,是一個包含5個元素的整型數組,其元素類型爲:int * 。
<2>數組指針
①定義數組指針的方式爲:int (*arr)[5];
解釋:arr先和*結合,說明p是一個指針變量,然後指着指向的是一個大小爲10個整型的數組。所以p是一個指針,指向一個數組,叫數組指針。
注:[]的優先級要高於*號的,所以必須加上()來保證p先和*結合。
②數組指針的應用:數組指針的應用場景多爲數組作爲參數傳遞給另一個函數。我們都知道,在函數傳參時有兩種方式:傳值傳參、傳址傳參。當數組作爲參數傳遞時是傳址傳參,數組降維成內部元素類型的指針。
一維數組傳參:以整型爲例,降維成 int * 類型的指針。
二維數組傳參:降維成 int (*arr)[size] 類型的數組指針。這裏需要注意的是,二維數組可以看成是一個內部元素爲一維數組的一維數組。以此類推,三維數組可看成是一個內部元素爲二維數組的一維數組。
③二維數組降維傳參,形參定義的集中形式,以及訪問數組元素的幾種方式
void example1(int(*arr)[5], int x,int y){
for (int i = 0; i < x; i++){
for (int j = 0; j < y; j++){
printf("%d\n", arr[i][j]);//建議使用
printf("%d\n", *(*(arr + i) + j));
printf("%d\n", *(arr[i] + j));
}
}
}
void example2(int arr[][5]){
}
2.函數指針
如上所述,指針是用來唯一標識一塊內存空間的,空間的大小由指針的類型決定。在我們練習寫代碼的過程中,如果你去打印一個變量的地址,這樣會得到一個唯一的地址,當你打開內存查看變量佔空間的情況時,你會發現打印出來的地址與該變量所佔用的內存中最低的地址的值相等(起始地址)。所以當CPU進行運算,從內從中讀取變量的內容時,是通過變量的地址(即這裏所打印出來的地址)唯一的標識出變量所在的空間,你可以理解爲相當於門牌號。
函數指針也是一樣,也用來標識一塊唯一的內存空間。這裏就需要介紹函數和函數名,函數的本質是一個代碼塊,會包含多組的代碼,也就決定了一個連續的地址序列;函數名則是用來標識當前函數的內存空間,它的值在大小上等於該函數所開闢連續空間中地址最低的序列。函數指針則是用來指向該連續空間的起始地址,即函數名。
函數指針變量的定義與初始化:
#include <stdio.h>
void example(int(*arr)[5], int x,int y){
for (int i = 0; i < x; i++){
for (int j = 0; j < y; j++);
}
}
int main()
{
int arr[][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
void (*p)(int (*)[5],int ,int) = example;
return 0;
}
如上代碼所示,我們定義了一個函數指針變量 p ,它所指向的函數接受三個參數,分別是一個數組指針和兩個整型值。
如何通過函數指針去調用函數,將例子中的數組打印出來?以下介紹兩種形式:
void (*p)(int (*)[5],int ,int) = example;
//example(arr, 2, 5);
(*p)(arr, 2, 5); //1
p(arr, 2, 5); //2
首先是第一種 ,對指針p進行間接訪問操作,把函數指針轉換成一個函數名,該語句在效果上與註釋掉的語句是一樣的,但是這個轉換是不必要的,編譯器在執行函數調用時又會將它轉換回去。第二種與第一種的效果相同,間接訪問操作並非必需,因爲編譯器需要的是一個函數指針來標識空間。
3.多級指針
這裏通過一個面試題來具體說明問題。
int main()
{
char *c[] = { "ENTER", "New", "POINT", "FIRST" };
char **cp[] = { c + 3, c + 2, c + 1, c };
char ***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
如上所示,分別定義了一個字符型數組指針、一個二級數組指針、三級指針。
注:字符型指針的指向有兩種:①字符,②字符串。當指向字符串時,拿到的是字符串的起始地址,對其進行打印輸出時比較特殊,不需要解引用,從起始地址開始往後讀取打印,遇見 '\0' 停止。
①**++cpp:開始時,cpp 指向 cp[0],自增後指向 cp[1],第一次解引用得到 c[2] 的地址,第二次解引用得到字 c[2] 的內容,即符串 "POINT" 的起始地址,打印輸出爲:POINT。
②*--*++cpp + 3:可以看成是 (*(--(*(++cpp)))) + 3 第一次打印cpp自增指向 cp[1],此時再次自增則指向 cp[2],解引用得到 c[1] 的地址,然後再自減得到 c[0] 的地址,解引用得到"ENTER" 的起始地址,然後 +3 得到 "ENTER" 中字符E的地址,打印輸出爲:ER。
③*cpp[-2] + 3:可以轉換成 *(*(cpp-2))+3; 經過②之後,cpp指向 cp[2],減2後指向 cp[0],解引用得到 c[3] 的地址,再次解引用得到 "FIRST" 的起始地址,+3 得到S的地址,打印輸出爲:ST。需要注意的是這裏執行完之後,cpp的指向仍爲 cp[2]。
④cpp[-1][-1] + 1:可以轉換成 *(*(cpp-1)-1)+1。cpp-1指向 cp[1],解引用得到 c[2] 的地址,-1後得到 c[1] 的地址,解引用得到 "New" 的起始地址,+1得到E的地址,打印輸出爲:ew。
總結: *(三級指針) = 二級指針的內容 = 一級指針的地址;
*(二級指針) = 一級指針的內容 = 變量的地址;
*(一級指針) = 變量的內容;