**肆**
***數組與指針(二)***
**數組與指針的糾葛**
***以指針的形式訪問數組:***
下標表達式: 后綴表達式[表達式]
在C語言中,根據定義,**表達式e1[e2]準確地對應于表達式*((e1)+(e2))。**因此,要求表達式e1[e2]的其中一個操作數是指針,另一個操作數是整數。且這兩個操作數的順序可以顛倒。
故: a[4] 等同于 4[a] 等同于 *(a+4)
編譯器把所有的e1[e2]表達式轉換成*((e1)+(e2))。
所以,以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了!
**多維數組**
二維數組a[i][j]
**編譯器總是將二維數組看成是一個一維數組,而一維數組的每個元素又都是一個數組。**
多維數組定義的下標從前到后可以看做是 最宏觀的維到最微觀的維。例:三維數組a[i][j][k] 可理解為 共有i個大組,每個大組里有j個小組,每個小組里有k個元素。
故:
a 表示為整個三維數組,其值為&a[0][0][0],
&a+1為整個三維數組后面的第一個位置。(偏移整個三維數組的長度)
a+1? 為第二個大組的首位置處(偏移一個大組的長度)【數組名a代表的是數組首元素的首地址,即:第一個大組的首地址】
a[0]表示為三維數組的i個大組中的第一個大組【可看做一個二維數組】,其值為&a[0][0][0],
&a[0]+1為第二個大組的首位置處(偏移一個大組的長度)
a[0]+1為第一個大組中第二個小組的首位置處(a[0]可看做是一個二維數組名,故其代表的是第一個小組的首地址)(偏移一個小組的長度)
a[0][0]表示為第一個大組中的第一個小組【可看做一個一維數組】其值為&a[0][0][0],
&a[0][0]+1為第一個大組中第二個小組的首位置處(偏移一個小組的長度)
a[0][0]+1為第一個大組中第一個小組的第二個元素位置處(偏移一個元素的長度)
a[0][0][0]表示為第一個大組中的第一個小組中的第一個元素。其值為&a[0][0][0],a[0][0][0]+1為首元素值加1。(因為a[0][0][0]為元素值而不是地址)
數組的數組(即:二維數組名)退化為數組的(常量)指針,而不是指針的指針。
同理,**n維數組名退化為n-1維數組的(常量)指針。**
【**總結:指針代表的是誰的首地址 就以誰的長度為偏移單位。**】
【**規律:與定義比較,缺少幾對方括號,就是幾維數組的數組名**,如上例:a缺少3對方括號,即為3維數組的數組名(代表的是2維數組的地址);a[0]缺少2對方括號,即為2維數組的數組名(代表的是1維數組的地址);a[0][0]缺少1對方括號,即為1維數組的數組名(代表的是數組元素的地址)】
【數組名與整數相加,首先要轉換成數組的首元素地址與整數相加,而首元素的存儲大小就是整數的單位】
對多維數組的解析:
我們可以用上面那種從前到后的解析方式來思考,a:就表示整個多維數組。a[m]:就表示第m+1大組(大組即數組最大的維),a[m][n]:就表示第m+1大組中的第n+1小組。(小組即次大的維),以此類推,即多維數組的解析是層層細化的。
**◎☆指針數組與數組指針:**
指針數組:首先它是一個數組。數組的元素都是指針。它是“存儲指針的數組”的簡稱。
數組指針:首先它是一個指針。它指向一個數組。它是“指向數組的指針”的簡稱。
例:int * p1[10]; //它是指針數組。(因為[]的優先級比*高,p1先與[]結合,構成一個數組的定義)
int? (*p2)[10] ; //它是數組指針。(括號的優先級較高,*與p2構成一個指針的定義)
它指向一個包含10個int型數據的數組。
若有:int(*p)[10][5] ;? //則p指向一個int型的二維數組a[10][5]。
【**規律:數組指針,把定義中括號內的指針看成是一個普通的字母,則其表示的就是 數組指針所指的對象類型**】
◎☆
~~~
int a[5][5] ;
int (*p)[4] ;
p=a ;
問:&p[4][2]-&a[4][2]的值為多少?
~~~
設二維數組的首地址為0,則a[4][2]為第5組的第3個位置(以后見到多維數組要這么想,不要總想著是幾排幾列的模式),因為int a[5][5];即有5組,每組有5個元素。故:&a[4][2]是(4*5+2)*sizeof(int).
int (*p)[4] ; 指針指向一個含4個int型的元素的數組,故p[4]相對于p[0]向后移動了“4個int型數組”的長度,然后在此基礎上再向后移動2個int型的長度(即,其步長按維度逐步遞減,多維數組也可按此方式理解)。最后其值為(4*4+2)* sizeof(int)
最后**切記:地址值參與的加減運算(地址不能被乘),整數的單位是地址值代表的元素的存儲大小!**
&p[4][2]-&a[4][2]結果為-4。若分開比較&p[4][2]和&a[4][2]則相差4* sizeof(int)個字節**
【**◎☆規律:數組指針的連續解引用**
數組指針的定義提供了其逐次解引用時的偏移單位,例int (*p)[m][n][k],則意為:數組指針的第一次解引用的偏移單位是m*n*k個int型長度,再次解引用的偏移單位是n*k個int型長度,又一次解引用的偏移單位是k個int型長度,最后一次解引用的偏移單位是1個int型長度。它只能連續解引用4次。 故:p[2][3][4][5]與四維數組首地址相距(2*m*n*k + 3*n*k + 4*k + 5 )個int型長度】
故:**數組指針指向的是哪個數組,就可以把它當做那個數組的數組名來用。**
例:inta[3][10][5] ; int (*p)[10][5] ; p = a ; 則:p[1][2][3] == a[1][2][3]?;? p[1][2] ==a[1][2]
即:用數組指針訪問數組和用數組名訪問,效果是相同的。
**WHY?**以int(*p)[10][5]為例,它指向一個[10][5]的二維數組,故第一次解引用時以二維數組[10][5]的長度作為偏移單位,一次解引用后p[1]就是一個[10][5]二維數組了。(解引用就是提取出指針偏移后 指向的對象) 即為:一維數組[5]的首地址。故再次解引用就以一維數組[5]的長度作為偏移單位,二次解引用后p[1][2]就是一個[5]一維數組了,即是一維數組首元素的地址。所以三次引用后,偏移單位為1個元素。
**數組參數與指針參數:**
1,二維數組名做實參
~~~
int main(void)
{ int a[4][5] ;
……….
………
fun(a);
……….
}
被調函數:
①fun( inta[4][5] )
②fun( inta[ ][5] )
③fun( int(*a)[5] )
{ ……….
a[i][j]=……….
………
}
~~~
以上三種方式皆可。無論是那種方式,它們只是寫法不同,但編譯器的處理方式相同,都把它們看做是一維數組指針。
因為二維數組名退化為一個一維數組指針,故是以一維數組指針的形式來傳遞二維數組的。
2,指針數組做實參
~~~
int main(void)
{ int a[4][5] , i, *p[4] ;
for(i=0;i<4; i++)
p[i]= a[i] ;
……….
fun(p);
……….
}
被調函數:
①fun(int*q[4])
②fun(int *q[])
③fun(int **q)
{ ……….
q[i][j]=……….//取出指針數組中的第i個元素(為指針),再偏移j個單位
//也可從雙重指針的角度理解:[i]為第一次解引用,偏移量是i個指針的大小(因為雙重指針指向的是指針變量),[j]為第二次解引用,偏移量是j個int型變量大小(因為此時指針指向的是一個int型變量:某組的首元素)
………
}
~~~
以上三種方式皆可。無論是那種方式,寫法不同,但編譯器的處理方式相同,都把它們看做是二級指針。
因為指針數組名退化為數組首元素的地址,即二級指針,故是以二級指針的形式來傳遞指針數組的。
而多維數組名退化為次維數組的指針,即數組指針,故是以數組指針的形式來傳遞多維數組的。
【數組指針的連續解引用,其指針的步長對應數組的維度值 是逐漸減小的
多級指針的連續解引用,其指針的步長 前幾次解引用的步長為1個指針的長度,最后一次解引用的步長為最終指向的對象長度。(操作系統常用多級指針在多張表中做查詢操作)】
【C中函數實參與形參之間是傳值引用的,所以你要改變這個值,就傳遞它的地址(無需多言)】
**函數指針**:
函數指針就是函數的指針。它是一個指針,指向一個函數。
(即函數在內存中的起始位置地址)
實際上,所有的函數名在表達式和初始化中,總是隱式地退化為指針。
例:int? r , (*fp)( ) , func( ) ;
????? fp= func ;????? //函數名退化為指針
????? r= (*fp)( ) ;? //等價于r=fp( ) ;
**無論fp是函數名還是函數指針,都能正確工作。因為函數總是通過指針進行調用的!**
例:int? f(int) ; //函數聲明
????? int? (*fp)(int) = &f ?;//此取地址符是可選的。編譯器就把函數名當做函數的入口地址。
//在引用這個函數地址之前,f函數應先聲明。
????? int? ans ;
????? //以下三種方式可調用函數
????? ans= f(25) ; //函數名后的括號是“函數調用操作符”。
????? ans= (*fp)(25) ;
????? ans= fp(25) ;
**函數名就是一個函數指針常量,函數調用操作符(即一對括號)相當于解引用**
函數的執行過程:
函數名首先被轉換為一個函數指針常量,該指針指定函數在內存中的位置。然后函數調用操作符調用該函數,執行開始于這個地址的代碼。
**再說強制類型轉換:**
~~~
void fun() { printf("Call fun "); }
int main(void)
{
void(*p)( ) ;
*(int*)&p = (int)fun ;
(*p)() ;
return0 ;
}
~~~
參見前面文章的強制類型轉換。強制類型轉換只不過是改變了編譯器對位的解釋方法罷了。
*(int *)&p = (int)fun ;中的fun是一個函數地址,被強制轉換為int數字。左邊的(int*)&p是把函數指針p轉換為int型指針。*(int *)&p = (int)fun ;表示將函數的入口地址賦值給指針變量p。(*p)( ) ;表示對函數的調用。
**函數指針數組:**
即是存儲函數指針的數組。(有時非常有用)
例:char *(*pf[3])(char *) ;
**函數指針的用途:**
1,**轉移表**(轉移表就是一個函數指針數組)
即可用來實現“菜單驅動系統”。系統提示用戶從菜單中選擇一個選項,每個選項由不同的函數提供服務。
【若每個選項包含許多操作,用switch操作,會使程序變得很長,可讀性差。這時可用轉移表的方式】
例:void(*f[3])(int) = {function1, function2, function3} ; //定義一個轉移表
????? (*f[choice])( ) ; //根據用戶的選擇來調用相應的函數
2,**回調函數**(用函數指針做形參,用戶根據自己的環境寫個簡單的函數模塊,傳給回調函數,這樣回調函數就能在不同的環境下運行了,**提高了模塊的復用性**)
【回調函數實現與環境無關的核心操作,而把與環境有關的簡單操作留給用戶完成,在實際運行時回調函數通過函數指針調用用戶的函數,這樣其就能適應多種用戶需求】
例:C庫函數中的快速排序函數
????? voidqsort(void *base, int nelem, size_t width, int ?(*fcmp)(void*, void*) );
//base為待排序的數組基址,nelem為數組中元素個數,width為元素的大小,fcmp為函數指針。
這樣,由用戶實現fcmp的比較功能(用戶可根據需要,寫整型值的比較、浮點值的比較,字符串的比較 等)這樣qsort函數就能適應各種不同的類型值的排序。
**使用函數指針的好處在于:**
可以將實現同一功能的多個模塊統一起來標識,這樣一來更容易后期維護,系統結構更加清晰。或者歸納為:便于分層設計、利于系統抽象、降低耦合度以及使接口與實現分開。
**函數指針數組的指針:**(基本用不到)
例:char *(*(*pf)[3])(char *)
這個指針指向一個數組,這個數組里存儲的都是指向函數的指針。它們指向的是一種返回值為字符指針,參數為字符指針的函數。
[對于這種復雜的聲明,《C和指針》《C專家編程》中有專門的論述。我的方法就是:從核心到外層,層層分析。先找到這個聲明的核心,看他的本質是什么。就像本例,最內層的括號里是一個指針,再看外層來確定它是個什么指針。外層是一個3個元素的數組,再看這個數組的元素類型是什么。是一個函數指針。 故總體來說此聲明是一個函數指針數組的指針。]
**復雜指針的舉例:**
int* (*a[5])(int, char*);
void (*b[10]) (void (*)());
doube(*)() (*pa)[9];
讓我們一層一層剝開它的心。
第1個、首先找到核心,即標識符a,[ ] 優先級大于“*”,a與“[5]”先結合。所以a是一個數組,這個數組有5個元素,每一個元素都是一個指針。再往外層看:指針指向“(int,char*)”,對,指向一個函數,函數參數是“int, char*”,返回值是“int*”。完畢!
第2個、首先找到核心:b是一個數組,這個數組有10個元素,每一個元素都是一個指針,指針指向一個函數,函數參數是“void(*)()”【 這個參數又是一個指針,指向一個函數,函數參數為空,返回值是“void”】 返回值是“void”。完畢!
第3個、核心pa是一個指針,指針指向一個數組,這個數組有9個元素。再往外層看:每一個元素都是“doube(*)()”【也即一個指針,指向一個函數,函數參數為空,返回值是“double”】
**使用typedef簡化聲明:**
某大牛對typedef用法做過一個總結:“建立一個類型別名的方法很簡單,在傳統的變量聲明表達式里用類型名替代變量名,然后把關鍵字typedef加在該語句的開頭”。
舉例:
例1,void (*b[10]) (void (*)());
typedef void (*pfv)();? ????????????????????? //先把上式的后半部分用typedef換掉
typedef void (*pf_taking_pfv)(pfv);???? //再把前半部分用typedef換掉
pf_taking_pfv b[10]; ?????????? ????????????? //整個用typedef換掉
跟void (*b[10]) (void (*)());的效果一樣!
例2,doube(*)() (*pa)[9];?
typedef double(*PF)(); ??????? //先替換前半部分
typedef PF (*PA)[9];??????????? //再替換后半部分
PA ?pa; ??????? //跟doube(*)() (*pa)[9];的效果一樣!
**反思:**
1,我們為什么需要指針?
因為我們要訪問一個對象,我們要改變一個對象。要訪問一個對象,必須先知道它在哪,也就是它在內存中的地址。地址就是指針值。
所以我們有
函數指針:某塊函數代碼的起始位置(地址)
指針的指針:因為我要訪問(或改變)某個變量,只是這個變量是指針罷了
2,為什么要有指針類型?
因為我們訪問的對象一般占據多個字節,而代表它們的地址值只是其中最低字節的地址,我們要完整的訪問對象,必須知道它們總共占據了多少字節。而指針類型即向我們提供這樣的信息。
注意:一個指針變量向我們提供了三種信息**:**
①一個首字節的地址值
②這個指針的作用范圍(步長)
③對這個范圍中的數位的解釋規則(解碼規則)
【編譯器就像一個以步數測量距離的盲人。故你要告訴它從哪開始走,走多少步。】
3,強制類型轉換的真相?
學過匯編的人都知道,什么尼瑪指針,什么char,int,double,什么數組指針,函數指針,指針的指針,在內存中都尼瑪是一串二進制數罷了。**只是我們賦予了這些二進制數不同的含義,給它們設定一些不同的解釋規則,讓它們代表不同的事物。**(比如1000 0000 0000 0001 是內存中某4個字節中的內容,如果我們認為它是int型,則按int型的規則解釋它為-231+ 1;如果我們認為它是unsigned int ,則被解釋為231+ 1;當然我們也可把它解釋為一個地址值,數組的地址,函數的地址,指針的地址等)
如果我們使用匯編編程,我們必須根據上下文需要,用大腦記住這個值當前的代表含義,當程序中有很多這樣的值時,我們必須分別記清它們當前代表的含義。這樣極易導致誤用,所以編譯器出現了,讓它來幫我們記住這些值當前表示的含義。當我們想讓某個值換一種解釋的方案時,就用強制類型轉換的方式來告訴編譯器,編譯器則修改解釋它的規則,而內存中的二進制數位是不變的(涉及浮點型的強制轉換除外,它們是舍掉一些位,保留一些位)
4,涉及浮點型的強制轉
詳情參見《深入理解計算機系統》
5,難點
多維數組、數組指針、多級指針。
**抓住問題的核心:指針值是誰的地址,這個地址代表的是哪個對象。**
搞清楚這個問題,關于指針移動時偏移量(步長)的計算就不會出錯。
指針類型只是C語言提供的一種抽象,來幫助程序員避免尋址錯誤。