指針閒談
本文將採用按部就班的方式,來簡要談一談C語言中指針的定義和分析。算法
談到c語言,就繞不開c語言中的一把利器--指針。數組
指針能夠直接指向物理內存地址,對內存進行操做。在計算機中,咱們把內存劃分爲一個個小的單元,每一個單元對應一個編號(或者地址),而指針能夠利用地址,直接找到該地址對應的變量值。通俗的理解就是指針像門牌號,咱們能夠經過門牌號找到對應房間,從而找到房間裏的人。ide
所以,指針也是一個變量,用於存放地址的變量。在《c語言數據存儲》一文中已經分析了,物理內存中是以一個字節爲一個單元,所以,咱們能夠把內存想象成一輛在鐵軌上的火車,每節車箱就至關於一個內存單元,在車箱內部,咱們安裝上八張連續的椅子。對每一個車箱進行編號,第一節車箱爲0X00000000,第二節車箱爲0X00000001,依次類推,該編號爲16進制,最後一節車箱編號爲0XFFFFFFFF。能夠看出一個指針變量所佔的空間爲8字節。(以32位平臺爲例,下文若是沒有特殊指明,均按32位平臺)函數
1指針類型
1.1如何定義指針變量?
首先,讓咱們來看一看c語言中是如何定義變量的,例如:spa
int i = 10;
咱們定義了一個變量,其中i爲變量的名稱,int爲該變量的類型,10爲該變量的值。能夠看出,在等號的左邊,咱們去掉變量的名稱i,剩下的即爲變量的類型。變量的類型決定了該變量所佔字節大小。int類型的變量佔用4個字節。指針
一樣的,讓咱們來定義一個數組:code
int arr[5] = {0,1,2,3,4};
按照上述分析,在等號左邊,arr爲該變量的名稱,去掉arr後剩下int [5],此即爲變量arr的類型,其中[]表示變量arr爲一個數組,[5]裏面的5表示該數組有5個元素,int表示這5個元素都爲整型,因此int [5]類型所佔的字節數是4×5=20個字節。因爲"[]"裏的數字能夠任意指定,因此咱們稱數組爲自定義類型數據,與這種定義方式類似的還有結構體,枚舉體以及聯合體。遊戲
若是咱們要定義一個指針變量,只需在變量名前加上*,因此咱們能夠定義以下指針變量:內存
int *pi = &i;
同上述分析,在等號的左邊,pi爲變量名稱,去掉pi剩下的int *爲該變量的類型,其中*表示變量爲指針,int表示該指針變量指向的元素是整型。等號右邊,"&"表示取地址的意思,即取到i所在的車箱號,而後把該號碼(地址)放到指針變量pi中。字符串
同理,對於其餘類型的變量(如:char,short,long,long long,float,double以及它們的無符號(unsigned)類型)也有上述定義方式:
type *指針變量名 = &與type對應類型的變量;
對於字符指針char *,除上述用法外,還有另一種用法,例如:
char *pc = "Hello World";
此段代碼不能簡單理解爲把"Hello World"放入變量pc中,而是在內存中有一塊連續的內存,存放了字符串"Hello World",這句代碼的意思是把其中字符"H"的地址放入到指針pc中。
1.2如何使用地址?(解引用)
按照正常的思惟,拿到地址後只需按圖索驥經過地址上的車箱號找到對應的車箱,而後對其進行其餘操做,因此有
* pi = 0;
這句代碼的含義是把指針pi指向的變量的值改成0,pi存放的地址是i的地址,因此此句代碼的含義是把i的值改成0。即等價於i = 0。*在此處表示的含義爲解引用,指針的類型決定了對應指針的權限,如此處pi爲int *,因此pi能夠訪問和操做四個字節的內容,對於char *類型的指針,則每次只能訪問和操做一個字節的內容。
指針類型不只僅能夠決定該指針一次能訪問字節的大小,也決定了每次可以跨多大步子,例如,對於一個int *類型的指針pi,若是pi裏存放的地址是0X11FF2350,那麼pi+1
對應的地址就爲0X11FF2354,對於一個char *類型的指針pc,若是pc裏存放的地址是0X11FF2350,那麼pi+1對應的地址就爲0X11FF2351,其餘類型同理,即指針加上(或減去)一個整數,那麼指針對應的地址就移動(該整數)乘(該指針指向類型所佔字節數)。
1.3野指針
對於指針,在定義時必須對其進行初始化,即必須給指針一個明確的地址,不然會造成野指針。野指針就是指針指向位置是不可知的(隨機的、不正確的、沒有明確限制的),未初始化的指針就是隨機的。而此時若是對該隨機值指向的地址進行訪問和操做,就會形成非法訪問。正如你在大街上撿到一張地址(隨機),而你本身也不清楚該地址裏具體有誰,若是強行進入該地址,就會形成非法訪問。
所以咱們要避免野指針:對指針進行初始化,防止指針越界訪問,指針指向的空間釋放時把指針及時置NULL,使用指針以前檢查指針的有效性。
2指針與數組
首先,數組名錶示首元素的地址(有兩個例外,一個是數組名單獨放在sizeof()函數的括號裏,一個是放在&後邊。這兩種狀況都表示整個數組的地址),即
int arr[5]={0,1,2,3,4}; int *p = arr;
此時p裏存放的是數組的第一個元素arr[0]的地址。而p+1就表示數組的第二個元素arr[1]的地址,依次類推。即p+i=&arr[i]。所以咱們能夠經過指針來訪問數組中的元素,即*(p+i)就等價於arr[i]。前文說到,arr是數組名錶示的是首元素的地址,p也是地址,arr+1表示的是第二個元素的地址,因此*(arr+1)與arr[1]等價,同理,*(p+1)也與p[1]等價。所以,下文中,咱們不區分*(p+i)與p[i],由於二者是等價的。
如前文所述,當咱們想獲取某個整型i的地址時,直接&i,那麼,&arr則取到的是整個數組的地址,對整型的指針變量的定義,咱們直接在變量名稱前添加*,即int *pi = &i,同理,對於數組的指針變量定義,咱們也直接在變量名稱前添加*,可是因爲[]的優先級高於*,即在計算機編譯代碼時,首先把變量名與[]結合在一塊兒進行處理(而先與[]結合,編譯器就會認爲該變量是個數組),爲了防止這種狀況出現,咱們將*和變量名用()括起來,即
int (*parr)[5] = &arr;
用上邊的分析:parr爲變量名,parr前邊有*,因此parr是一個指針,去掉變量名parr,剩餘的即爲該變量的類型:int (*) [5],*表示爲指針類型,從(*)向右看,是[5],說明該指針指向的是一個大小爲5的數組,從()向左看,是int,說明這個數組裏的5個元素類型都爲int。對於其餘類型的數組,分析也同上,例如
char str[3] = {'a', 'b', 'c'}; char(* pstr)[3] = &str;
再來思考另外一個問題,對於數組,首元素的地址和整個數組的地址同樣,即
int *p = arr; int (*parr)[5] = &parr;
若是對p和parr進行輸出,顯然二者的值相同,那麼二者又有何區別?前邊在引入和討論整型指針變量時,咱們提到過,對於不一樣類型的指針,類型決定了該指針每次可以訪問和操做字節數的大小。在這裏,p的類型爲int *,即該指針指向的元素爲整型,是4個字節,parr的類型爲int (*) [5],即該指針指向的元素爲數組,有4×5=20個字節,因此p每次只能訪問和操做4個字節,而parr能夠訪問和操做20個字節,對指針進行加減整數操做,移動的字節數也不相同,假設該數組的地址爲0X1FC65804,那麼p+1的值就爲0X1FC65808,parr+1的值爲0X1FC65818(20的十六進制爲0X14)。
對於多維數組,分析同一維數組,以二維數組爲例。假設有這樣一個兩行三列的二維數組:
int arr[2][3] ={{1,2,3},{4,5,6}};
對於多維數組在內存中能夠當作按行存放(實際上由於內存是連續的因此實際中內存沒有行的概念),即該二維數組能夠認爲是由兩個一維數組組合而成的,其中第一個一維數組爲{1,2,3}(記爲a1),第二個一維數組爲{4,5,6}(記爲a2)。因此二維數組的首元素爲arr[0]=a1={1,2,3}。數組名錶示首元素地址即arr爲arr[0]={1,2,3}的地址,也就是說該地址指向的的是一個存有有三個元素一維數組,因此對應的指針變量爲一維數組指針變量,即:
int (*pa1) [3]= arr;//那麼pa1+1即爲a2的地址。
把arr[0]看做數組名,那麼它表示的是arr[0]首元素的地址,即{1,2,3}中1的地址。同理arr[1]表示{4,5,6}中4的地址。因此*(arr[0])就和arr[0][0]等價,*(arr[0]+1)和arr[0][1],依次類推。
按照上邊的定義方法,該二維數組的指針就可定義爲
int (*parr)[2][3] = &arr;
其含義爲:parr爲變量名稱,類型爲int (*)[2][3],*表示parr爲指針,從(*)向右看,是[2][3],說明了指針指向的是一個兩行三列的二維數組,從(*)向左看,是int,說明這個二維數組的元素爲int。
3函數指針
3.1函數指針定義
函數指針,顧名思義,是用來存放函數地址的指針。那麼函數指針該如何來定義?首先讓咱們從定義函數看起:例如咱們要定義一個算兩個整數加法的函數add,那麼咱們須要給這個函數傳入兩個參數,而函數算完加法後,則向咱們返回計算結果(整數)。因此:
int add(int x, inty) { //實現主要功能的代碼,不是本主題的討論關鍵,略去不寫 }
上述代碼中,add爲函數名,add後邊的()說明add爲一個函數,()裏兩個int變量說明該函數接收兩個類型爲int的變量,add前邊的int說明函數的返回參數類型爲int。
同數組的指針定義方式同樣,咱們採起一樣的方式定義函數指針變量,即在變量前加上*,一樣的因爲()優先級更高,因此咱們須要把*函數指針變量放在一個()裏,即
int (*padd) (int,int) = &add;
上述代碼意思:等號左邊padd爲一個變量,去掉變量名padd後剩下int (*) (int,int),此即爲變量的類型,(*)說明變量padd是一個指針,(*)的右邊爲(int,int)說明了該指針指向的是一個函數,這個函數接收兩個類型爲int的變量,(*)的左邊是int,說明該指針指向的函數返回類型爲int。等號右邊取函數add的地址表示指針變量padd裏存放的是函數add的地址。(因爲不一樣函數功能不一樣,函數內部定義的變量不一樣,而函數的做用主要是用來被調用以實現其功能,因此咱們不討論函數指針能夠訪問和操做的字節數)。
在c語言中,函數名錶示函數地址,因此上述代碼也能夠寫爲
int (*padd) (int,int) = add;
咱們調用函數時通常直接使用函數名即add(x,y),咱們在使用地址時通常要解引用,即(*padd)(x,y)。既然函數名錶示函數地址,而padd也爲地址,因此也能夠寫爲(*add)(x,y),padd(x,y)。即這幾種狀況含義相同,都是調用函數add。所以下文將不區分(*padd)(x,y)和padd(x,y)這兩種表達方式。
3.2案例
3.2.1案例1
有了上述基礎,讓咱們來看一下以下代碼:
(*(void (*)( ))0)( );//來源《c陷阱與缺陷》一書
首先,讓咱們來梳理一下()在C語言中的含義:
(1).改變優先級,即在算數運算中,例如一個有加減乘除的式子,先算乘除,再算加減,若是有()則先算()裏的,再算其它的。這咱們在初等數學中都學過,所以再也不贅述。
(2).強制類型轉換,通常放在變量或者數據的前邊,把變量或數據強制轉換爲括號裏對應的類型。例如(float)3,含義爲把整數3強制轉換爲浮點數3;假設p爲整型指針,(char *)p即把p強制轉換爲字符型指針。
(3).跟在控制語句後,例如if()…,while()…,for()…等。
(4).跟在函數名後,()裏放函數的參數。例如add(x,y)。
(5).c語言中,有一類表達式叫作逗號表達式,即括號裏有一系列以逗號隔開的表達式,運算方式從左到右。例如:
a=1; b=2; a=(3,5,b=7,6);
運用逗號表達式的算法,最後a=6,b=7。
顯然代碼(*(void (*)( ))0)( );中的()沒有跟在控制語句後面,也不是逗號表達式。在上邊分析函數指針padd時提到過去掉padd後剩下的部分是變量類型,因此void(*)()是一個函數指針類型,讓咱們從中間的(*)開始分析,(*)說明是個指針類型,從(*)向它的後邊看,緊跟着一個(),說明這個指針指向的是一個函數,這個函數不須要傳參,從(*)向前看,是void,說明這個函數返回的參數類型是void(即沒有返回參數)。因此void(*)()是一個"指向沒有參數,返回值爲void的函數指針"類型。它加了一個括號放在0前面,即(void(*)())0,含義是把0強制轉換爲該類型的函數指針,咱們能夠把此記爲p1,因此p1是個函數指針。
在c語言中,*有以下兩種含義:
(1).在定義變量時放在變量前邊跟變量結合表示變量爲指針;例如: int *p = &i;
(2).在使用指針變量時放在指針變量前表示解引用。例如:*p=2。
能夠看出,這裏咱們沒有定義變量,而是放在了指針變量(void(*)())0的前面,即*(void(*)())0也就是*p1,前邊分析函數指針變量時,提到過對函數指針解引用,至關於調用函數,調用的這個函數沒有參數,返回類型爲void,即*p1()。而因爲()優先級較高,會先與0結合,因此要把*(void(*)())0括起來即(*(void(*)())0)以防止0和後邊的()先結合。
綜上所述,由於0是數字,強制類型轉換爲指針後就表示地址,因此這句代碼的含義爲調用0地址處的函數,這個函數不須要傳入參數,這個函數的返回類型是void。
3.2.2案例2
再來看另外一個案例:
void (*signal(int,void(*)(int)))(int); //來源《c陷阱與缺陷》一書
初看代碼感受很複雜,可是能夠由內到外逐層分析。讓咱們再次回憶一下add函數的定義,咱們在定義add函數時,其格式以下int add(int x,int y),其中add爲函數名,(int x,int y)爲給函數傳入的參數類型,去掉函數名add和(int x,int y),剩下的int即爲函數add的返回類型。
一樣的,在上邊這段代碼中,因爲()的優先級更高,因此signal先與()結合,代表signal是個函數,即signal即爲函數名,該函數接收兩個參數(int,void(*)(int)),這兩個參數的類型一個是int,一個是void(*)(int)(能夠看出這是個函數指針類型,其指向的函數接收一個int類型的參數,返回值爲void,即這個類型是「指向一個接收int類型返回值爲void的函數指針類型」),去掉函數名signal和其接收的參數類型(int,void(*)(int))後,剩下void (*)(int),因此signal函數返回值的類型爲void (*)(int)。因此上述代碼是一個函數聲明。
4指針數組
4.1整型指針數組
數組指針,指針數組,聽起來像是在玩文字遊戲,但由前邊的介紹,數組指針是用來存放數組地址的指針,函數指針是用來存放函數地址的指針,整型數組是用來存放一組整型的數組,因此指針數組就是用來存放一組指針的數組。能夠看出來,誰放在前面,誰就是一個修飾做用。那麼,指針數組該如何定義?
讓咱們來回憶一下整型數組的定義,例如定義一個能夠放五個整型變量的數組,咱們有以下代碼:
int arr[5] = {1,2,3,4,5};
其中arr是數組名,[5]表示數組放了五個元素,int表示這些元素的類型是整型。因此,若是咱們要定義一個能夠存放三個整型指針的數組,則有:
int* arr1[3] = {&i,&j,&k};
arr1表示變量名,由於[]的優先級更高,因此arr1優先與[]結合,代表arr1是個數組,去掉數組名arr1,剩下的int * [3]即爲arr1的類型,從arr1向右看是[3],代表這個數組放了3個元素,向左看是int *,代表這三個元素的類型都是int *即整型指針。對於其餘數據指針類型定義同理。
4.2數組指針數組
顧名思義,是用來存放多個數組指針的數組,如何定義呢?
讓咱們從新審視一下整型數組以及整型指針數組的定義。當咱們須要定義一個整型變量時:
int i = 2;
當咱們須要定義一組整型變量時,即要把一組整型變量放在一塊兒變成數組時,在上述代碼的基礎上,只需在緊挨着變量名右側加上[],在[]裏放上咱們須要的整型個數,即
int arr1[3] = {0, 1, 2};
咱們採用了一樣的方式定義了整型指針數組:
int *pa = &a;
pa爲一個整型指針,咱們定義整型指針數組,只需在緊跟着變量名後邊加上[]和須要的個數,即
int *parr[3] = {&a, &b, &c};
parr[3]即爲一個含有三個整型指針的整型指針數組。
所以,咱們能夠用一樣的方法定義一個數組指針數組。例如:
int arr1[3] = {0, 1, 2}; int arr2[3] = {0, 1, 2}; int arr3[3] = {0, 1, 2};
這是三個元素個數相等的整型數組,它們對應的數組指針類型都相等(數組只能放同類型的數據),即爲int (*)[3];數組arr1的數組指針就能夠定義爲
int (*p1)[3] = &arr1;
咱們緊跟着變量名加上[]便可構造數組指針數組:
int (*parr[3])[3] = {p1, p2, p3};
上述代碼的含義:[]優先級較高,因此parr先和[3]結合,說明parr是個數組,去掉變量名parr後剩下int (*[3])[3],這個即爲parr的類型,與parr緊挨的[3]說明數組裏有三個元素,去掉parr和與parr緊挨的[3],剩下int (*)[3],這即爲parr裏邊的元素類型,顯然這個元素類型是數組指針類型。
4.3函數指針數組
函數指針數組是用來存放函數指針的一個數組。有了上邊的分析,讓咱們來快速的寫出一個函數指針數組(數組是用來存放同一類型的數據,因此存放的指針也要是同一類型)。
如今有四個函數,分別是加減乘除,它們的功能分別是用來計算兩個整數的加減乘除,並將計算結果返回(返回值爲整型),即:
int add(int x, int y); int sub(int x, int y); int mul(int x, int y); int div(int x, int y);
這四個函數的參數接收類型都爲兩個int,返回值爲int。因此它們的函數指針類型相同,都爲int (*)(int, int),因此add的指針能夠寫爲
int (*padd)(int, int) = add;
在緊挨着變量名的右側加上[]便可變爲函數指針數組:
int (*pfun[4])(int, int) = {&add, sub, mul, div};//在上邊介紹函數指針時提到過取地址函數名和函數名自己都表明函數地址,因此這裏寫成不一樣的就是爲了再次提醒讀者二者等價,實際使用時按一種風格書寫便可。
5指向數組指針數組的指針
這句話乍一讀非常拗口,讓咱們來細細分析,首先最後落向了指針,因此這是一個指針,而後這個指針指向的是一個(數組指針數組)。如何定義呢?
前文說過,在定義某數據類型變量對應的指針時,只需在變量名前加上*便可,因此對於上文介紹的數組指針數組
int (*parr[3])[3] = {&arr1, &arr2, &arr4};
只需在變量名前加上*便可變成對應類型的指針,即(因爲[]優先級高一點,因此要將*和變量名括起來提升優先級)
int (*(*pparr)[3])[3] = &parr;
分析:*與pparr結合,說明pparr是一個指針,去掉變量名pparr,剩下的int (*(*)[3])[3]即爲pparr的類型從(*)向右看是[3]說明pparr指向了一個含有三個元素的數組,去掉(*)[3]剩下的int (*)[3]即爲該數組裏的數組元素類型(是指針,指向一個有三個元素的數組,元素類型爲int)。
一樣,咱們能夠定義一個指向數組指針數組的指針數組,假設有三個與pparr同類型的指針pparr1,pparr2,pparr3.那麼只需在指向數組指針數組的指針的變量後邊加上[3]便可定義一個指向數組指針數組的指針數組:
int (*(*pparr1[3])[3])[3] = {pparr1,pparr2,pparr3};
....
咱們能夠按照上述方式,不斷的「套娃」下去,但此時已經意義不大,由於實際操做中很難會寫出這樣的代碼,即便寫出來,也會很難維護(容易把人繞暈)。
6指向函數指針數組的指針
同指向數組指針數組的方法同樣,咱們快速的寫出
int (*pfun[4])(int, int) = {add, sub, mul, div};
數組pfun[4]的指針:
int (*(*pfun1)[4])(int, int) = &pfun;
相似的,咱們也能夠寫出指向函數指針數組的指針數組,此處就不在贅述。
7多級指針
聊完了上述讓人頭大的東西,讓咱們再來聊一些輕鬆愉快的東西。咱們說,對於一個整型,能夠定義一個指針,即
int a = 2; int *pa = &a;
咱們稱pa裏存放了a的地址,那咱們也想把pa的地址存起來,是否能夠呢?答案是固然能夠,按照咱們前述的定義方式,咱們在變量前加上*便可表示一個指針:
int **ppa = &pa;
那麼ppa裏存放的就是指針pa的地址。pa存放了變量的地址,咱們把pa稱做一級指針,ppa存放了一級指針pa的地址,咱們把ppa稱做二級指針,同理,咱們把存放ppa地址的指針就稱爲三級指針,以此類推。
咱們對ppa進行解引用,拿到了pa,而後對pa再解引用就能夠找到a,即
**ppa=3;
就等價於
a = 3;
因爲時間問題,本文到此就結束了,對於結構體指針即其餘指針相關的知識,有時間再敘。