前言:對於C語言的學習,我記得最初就有人說最難莫過於指針,當初我們的學習呢,又是先學習了數組,然後接觸的就是指針,指針和數組被很多書籍放在一塊進行教學,指針是C/C++中的精華,而難點就在於對指針和數組的掌握以及內存的管理,所以在這裡,我們要對這個問題,進入一些研討:
在探究之前我們要弄清楚指針的概念,
指針做什麼
int *p;
學過C的人都應該知道,這定義了一個指針,在這裡p到底是個什麼東西呢?
其實,p也就是一個變量,而對於變量,就可以理解為一個左值,會開辟一塊內存空間,然後在這塊空間中儲存內容。
int *p;
int a=10;
p=&a;
在這幾句話,也不難理解,正因為像咱們在上面說的。開辟好了p這塊空間,所以咱們現在就要在這塊空間中,存放內容,而這個內容,在這裡&a,也就是a的地址了。
如果你還不能理解,那麼我用圖來給你解釋:
我想這樣你應該會理解指針的作用,即存放地址。對於上面那個例子,p這塊存放地址的空間我們叫做指針變量,p中存放的內存地址處的內存我們就稱為p所指向的內存,通常我們也會說p指向了a。
“*”的作用
對於上面的理解,我們留下一個問題,就是“* ”,在定義一個指針時,我們需要加“ *”,而在使用時,我們也會加“ *”。
比如下面這段程序:
int *p;
int a=10;
p=&a;
*p=20;
“* ”我們就可以理解為,當一個基本的數據類型加上它時,這樣就會構成一個數據類型的指針,這個指針我們要記住,大小永遠是4個字節。而當我們寫成 p 這樣的形式時,我們在這裡,稱呼 *為解引用。就好像你現在手中會有一把鑰匙,可以去通過指針來改變它所指向的那塊空間的內容。
數組,我們通常會理解為一組類型相同元素的集合。
比如:
int arr[]={1,2,3,4,5};
學過C的人應該都知道,這意思就是定義了一個數組,其中包含了5個int類型的元素。
當定義數組時,編譯器會根據元素的個數和元素類型確定大小開辟空間,比如,上面的arr,我們就會分配20字節大小個空間。在這裡,這塊空間是不能再次改變的。
數組名的左值右值
左值和右值,簡單說就是可以出現在“=”右邊的就是右值,可以出現在“=”左邊的就是左值。左值,他要是一塊可以被修改的空間,右值,所說的值一塊空間中所帶的內容。
接下來我們要探討一個問題了,數組名可以做左值嗎?右值呢?
當arr作為右值時,我們要清楚,arr就是代表首元素的地址,在這裡的arr就相當於arr[0]。
而當arr作為左值呢,在這我們就要根據左值右值的概念考慮了,左值必須是一塊可以修改的空間,而這裡,arr待變的是arr數組首元素的地址,地址不能被修改,所以,arr是不能作為左值的。
在我當初學習指針和數組時,總會覺得他們之間有種關系,並且好像彼此之間是一回事,在這裡,我要告訴你,指針和數組之間是沒有任何關系的!!!
很多人容易把這兩個概念弄混淆很大的原因就是應為在訪問時,指針和數組他們總是可以達到一樣的目的。
例如:
char *p="abcdef";
指針形式和下標形式訪問指針
對於上面的的指針變量p,根據前面咱們分析的可以知道,p是一塊4個字節大小的空間,裡面存放了首字符的地址。
比如我們需要訪問’c’:
指針形式:*(p+2);這個意思就是先取出p的地址,然後給它按照字符型進行偏移2次,然後把所指向的地址進行解引用,訪問其中所存放的內容。
數組下標形式:p[2];這裡編譯器會把下標形式解析成為指針形式的操作。這裡的意思就是先取出p中存儲的地址值,再加上其中括號中2個元素的偏移,得出新地址,然後再取出內容。
這兩種方法最後使得我們所得到的結果都是一樣的,兩種方式本質上都是以指針為形式進行訪問。
指針形式和下邊形式訪問數組
char arr[]="123456";
在這裡我們需要訪問’3’
指針形式:*(arr+2);這裡就是得到arr的首字符地址,然後偏移兩個字符,然後得到所指向地址,然後解引用,得到其中的內容。
數組下標形式:arr[2];這裡編譯器會把下標形式解析成為指針形式的操作。首先得到arr首元素的地址,然後偏移兩個字符,得到新的地址,再取出內容。
經過了上述兩個過程的分析,我們應該都知道指針和數組就是兩個不一樣的東西,他們只是可以”以指針的形式“和”以數組下標形式“來進行訪問。
區分a與&a
#include
int main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d,*(a+1),*(ptr-1)");
return 0;
}
在這裡我們要分析:
&a+1:這個說的就是取了a的地址,然後向後偏移一個int,也就是到了這個數組最後一個位置的下一個地址。
(int )(&a+1):這個意思是將上述的地址強制轉化為int 類型的地址。
所以執行完第二句話後,這裡的ptr中存放的就是a這個數組的下一個數組的首地址。
*(a+1):這個的意思參考上面咱們分析的,所以也就可以得到這個的結果就是數組第二個元素。
所以,最終輸出的結果也就是2和5;
這裡我們依然強調一下&a說的是數組的地址,對他+1指的是偏移整個數組。
a這裡代表的是a[0],首元素的地址。
指針數組:這裡強調的是數組,然後數組中的元素都是指針,數組的大小是通過數組本身來決定。
數組指針:這裡強調的是指針,就是說它還是一個指針變量,只不過他指向數組,至於他所指向的數組多大,這個就不知道了。
int *p1[5];
int (*p2)[5];
在這裡我總結下來,指針數組和數組指針主要的區別主要是看優先級結合。”()”的優先級是最高的,下來是”[ ]”,再下來是”*“;
int* p1[5]:我們可以理解為,先於[10]結合,就是一個數組p1,然後是和”int*“結合,就表示數組中的內容全部都是int*類型的,所以這就是一個指針數組,數組中包含的是10個指向int類型數據的指針。所以他就是一個指針數組。
int (*p2)[5]:這裡首先優先級最高的是”()“,所以先結合”()“,就是強調了這是個指針,然後和”[10]”進行結合,所以p2就是一個指向一個包含10個int大小的數組的指針。所以他就是一個數組指針。
通過剛才對於指針數組和數組指針的理解,接下來我們來探討一下函數數組和函數指針。
函數指針:顧名思義,這裡強調的依然是指針,函數指針也就是說函數的指針,他是一個指向函數的指針。
char *fun3(char *p1,char *p2);
這個我想大家再熟悉不過了,函數為fun3,兩個char* 類型的形參 p1,p2,函數返回值char *。
char * *fun2(char *p1,char *p2);
這裡函數為fun2,兩個char 類型的形參 p1,p2,函數返回值char **。
char *(*fun1)(char * p1,char *p2);
在這,我們就可以用優先級來進行判斷,它先於()結合,可以知道fun1是一個指針,然後它指向的是一個函數。而這個函數呢,就是一個有兩個char* 類型的形參 p1,p2,函數返回值char *的函數。
函數指針的使用
#include
int max(int x,int y)
{
return (x>y? x:y);
}
int main()
{
int (*ptr)(int, int);
int a, b, c;
ptr = max;
scanf("%d%d", &a, &b);
c = (*ptr)(a,b);
printf("a=%d, b=%d, max=%d", a, b, c);
return 0;
}
在這一段程序中,ptr是指向函數的指針變量,所以可把函數max()賦給ptr作為ptr的值,即把max()的入口地址賦給ptr,以後就可以用ptr來調用該函數。
接下來看下一個例子:
void fun()
{
printf("CALL Fun!\n");
}
int main()
{
void (*p)();
*(int *)&p=(int)fun;
(*p)();
return 0;
}
分析:
void (*p)():這一句說的就是我在這裡定義了一個指針變量p,p用來指向函數,函數的返回值和參數都是void類型的。
*(int )&p=(int)fun:在這,就是說將fun函數的地址強制轉換成int類型,然後賦給了p指針變量。
(*p)():表示對函數fun進行調用。
所以我們要清楚,函數指針還是一個指針,裡面存放的是函數的首地址,然後我們通過首地址來調用函數。
(*(void (*)())0)();
這個是什麼呢?
對於這個,其實咱們也可以通過分析進行解決。
void (*)():這個是一個void類型的一個函數指針,這個函數返回值為空,並且參數也為空。
(void (*)())0:這個就是將0強制轉換成這個函數指針的類型,也就是說一個函數保存在首地址為0的一段區域。
(* ( void (*)())0):這是取得首地址為0的的內容,就是保存在首地址為0的的函數。
(* (void (*)())0)():這是最終對這個函數進行調用,因為函數的參數為空,所以調用時參數也是空的。
接下來我們再進行分析一個類似的:
(*(char * *(*)(char **,char **))0)(char * *,char * *);
是不是看這有些暈呢,咱們繼續一層一層進行分析。
char * * ( * )(char * * ,char * * ):這個所說的就是一個函數指針,這個函數指針指向返回值為char * * * ,參數為兩個char * *的參數。
(char * * ( * )(char * * ,char * * ))0:這是將0強制轉換為這個函數指針的類型,也就是說一個函數保存在首地址為0的一段區域。
( * (char * * ( * )(char * * ,char * * ))0 ):這是取得首地址為0的的內容,就是保存在首地址為0的的函數。
( * (char * * ( * )(char * * ,char * * ))0)(char * * ,char * * ):這是最終對這個函數進行調用因為函數的參數為(char * * ,char * * ),所以調用也是給(char * * ,char * * )參數。
函數指針數組:我們也可以參考前面的分析方法,它是一個數組,這個數組中的元素就是我們前面所提到的函數指針。
char *(* p[3])(char *p);
上面這個就是一個函數指針數組,首先它是有3個元素的一個數組,裡面存放的是指向函數的指針,這些指針指向一些返回值類型為指向字符的指針。
在這裡的關鍵你要知道這個是數組,數組中的元素是函數指針。
到這裡,我想許多人看見以後肯定非常暈了,一層套一層的,其實,沒什麼復雜的,萬變不離其宗,咱們還是一層一層來進行分析。
首先給出一個函數指針數組指針
char *(*(*p)[3])(char *p);
在這裡,首先按照優先級從個最裡面看,最裡面的(*p)這是一個指針。
(*( *p)[3])這個說了這個指針是指向含有三個元素的數組的指針。
char*( *(*p)[3])(char *p)這個說了這個數組中存放的元素是3個函數指針,這些指針指向一些返回值為指向字符的指針。然後這個函數的參數是指向字符的指針。