參考
The C Programming Language-Chapter 5 Pointers and Arrays
前言
在上一篇文章動態數組(一維二維)探秘介紹了數組的一些知識,在最后碰到了一個如何申請二位數組的問題,這篇文章就延伸一下,介紹介紹數組、函數和指針更深層次的關系。
基礎知識
int a[10] 一維數組,數組中有連續的十個元素,每個元素都是int類型。
int *p 指針,保存的是一塊數據的地址,這塊數據是int類型,也就是當程序訪問到p指向的地址的時候,需要按照int類型把后面連續的幾塊數據按照一個整體讀取
int v int類型的數據
p = &v 把v的地址賦值給p,那么現在p指向的就是v的地址
p = &a[0] 把數組第一個元素的地址賦值給p,那么現在p指向的就是數組a第一個元素的地址
int *p到底是什么
按照The C Programming Language中介紹,這個表達式應該看成int (*p),也就是*p是個變量,它是一個int類型,與int v是等價的。*在變量前表示把當前的指針類型解析成它指向的數據類型,那么去掉*,就表示它是一個指針。
進一步說,就是,p是一個指針,*的作用是把p(指針)解析成它指向的數據,*p就是p指向的數據,類型是int,也就是我們說的p是一個指向int類型的指針
如果這樣理解的話,下面這條聲明變量的語句就很好理解了
int *p, v;
由於運算符優先級的關系,這個等價於
int *p; int v;
*p和v是等價的。這條語句相當於聲明了兩個int變量,*p和v。v無法進一步解析,所以v就是一個int變量;*p可以進一步解析,把*去掉,那么p就是一個指針,也就是指向int類型變量的指針。
int a[10]中的a又如何理解
由The C Programming Language中我們可以看到,a指向了a[10]數組開始的地址,與&a[0](數組第一個元素的地址)是一樣的,如下圖
設置p等於a或是a[0]的地址,這樣p和a的作用就一樣了。只不過a是一個常量,p是一個變量,a不能被賦值或是做加減乘除的運算,p可以
p = a; //或者 p = &a[0];
數組與指針有着密切聯系,數組可以看做指針指向了一塊連續的內存區域,實際上也確實如此。如下圖
a[0]就是*(a+0)也就是*(pa+0),從這里就可以理解為什么c/c++的索引是從0開始。
在c/c++中,對指針做加一的操作,相當於把指針移動到下一個數據的位置,移動多少,取決於指針指向的類型是什么,跨度就是這個類型占用的空間。
比如上圖,不管a是什么類型的數組,a+1或是p+1,移動的距離就是a數組中元素占用內存的字節數。比如a中是char(占用一個字節),a指向0x0001,那么a+1就指向0x0002;如果是int(占用四個字節),a指向0x0001,a+1就指向0x0005。
再談二維數組
如下圖數組a是一個二維數組,相當於一個數組指針的數組,這個數組有2個元素,每個元素又指向了一個一維數組,每個一維數組的元素個數是5
那么二維數組的指針是什么呢?我們知道一維數組相當於指針,那么二維數組相當於數組中保存了一維數組,也就是數組中保存了指針,也就是指針數組,那么二維數組就相當於指針的指針。
但是當我們編譯下面的代碼的時候,會提示error C2440: 'initializing': cannot convert from 'char [2][5]' to 'char **',鼠標放在出錯的地方提示a value of type "char (*)[5]" cannot be used to initialize an entity of type "char **"
char a[2][5]; char **p = a;
這是為什么呢?實際上二維數組,或是多維數組的指針不是這樣定義的,必須要指定這個數組指針數組中每個元素是什么樣的,如下才是正常的
char a[2][5]; char (*p)[5] = a;
實際上我們可以用一個指針操作二維數組,也可以用指針的指針操作二維數組,只需要強轉就行。不管是幾維的數組,在內存中都是連續的,我們只需指向數組的開始位置,一個個訪問就可以了。
char a1[2] char * a2[2] char (*a3)[5] char a4[2][5]傻傻分不清楚
在動態數組(一維二維)探秘 中我們碰到了一個問題,就是,從內存中看,申請一個一維數組和二維數組一樣,都是一塊連續的空間,但是如果用一維數組的方式申請一塊連續的空間,我們用指針加1,發現它並不會像真正的二維數組一樣,向前跳動一排的元素空間大小,還是跳動一個元素大小。如下示例
char a[2][5]; char *pa = new char[2*5]();
a的地址是0x004CFE00
a+1的地址是0x004CFE05
pa的地址是0x00227D68
pa+1的地址是0x00227D68
這是為什么呢?很明顯,pa不管怎么操作,它只是一個char的指針,那么按照c語言的規則,每次跳動只能是一個char的大小,也就是1;而a是一個char [5]的指針,那么每次跳轉就是char[5]的大小,也就是5.
上面的4個,有一個是特殊的,就是char (*a3)[5]。其他的都是數組,這個是指針:
- char a1[2]是一個一維數組,里面有兩個char元素
- char * a2[2]是一個數組,里面有兩個char*元素
- char a4[2][5]是一個二維數組,里面有2*5個char元素,或是說里面有2個char[5]
- 而char (*a3)[5]是一個指針,指向的類型是char[5]。我們可以用上面的方式拆分一下,char (*a3)[5]是一個數組,里面保存的是有5個char的連續數據,*a3就是這個數組,去掉*,那么a3就是一個指針,指向的是一個char [5]
char a1[5] char a2[2][5] char (*a3)[5]有什么聯系
char a1[5]和char a2[2][5]的指針形式都是char (*a3)[5],這就是它們之間的聯系。可能看上去有點懵,實際上這個與char b1和char b2[5]的指針形式都是char * b3是一樣的。
我們知道char b1和char b2[5]的指針形式都是char * b3,如果b3=b1,那么b3就指向了b1的地址;如果b3=b2,那么b3就指向b2第一個元素的地址,b3++就可以訪問b2的第二個元素。
在c語言中,沒有越界檢測,這即提供了方便,也增加了風險。c語言中最危險的陷阱之一就是越界和野指針。從邏輯或是實現上來說,這又是c語言中的精髓,底層邏輯簡單,應用執行速度快,不需要考慮任何額外的操作。如果是指針,那么指針加一,就是跳轉到下一塊數據,也就是把當前指向的這塊數據跳過去。
同樣a3=a1,就是指向了一塊數據,這個數據類型是一個char[5]。如果是多個char[5]呢?比如a3=a2,那就是一個一行5個元素或是說5列的二維數組了。a3++,就是跳轉5個元素的大小,這樣就可以直接用a3[1][2]的方式訪問了,這就是二維數組。
函數與指針
在c語言中,函數並不是一個變量,但是可以定義成一個指針,允許被賦值、傳遞等。
int fun1(int *a, int *b); int * fun2(int *a, int *b); int (*fun3)(int *a, int *b); int* (*fun4)(int *a, int *b);
- fun1是一個函數,函數的返回值是int,函數有兩個參數,每個參數都是int指針
- fun2是一個函數,函數的返回值是int指針,函數有兩個參數,每個參數都是int指針
- fun3是一個指針,指針的類型是一個函數,這個函數的返回值是int,函數有兩個參數,每個參數都是int指針
- fun4是一個指針,指針的類型是一個函數,這個函數的返回值是int指針,函數有兩個參數,每個參數都是int指針
我們可以看出fun3就是fun1的指針,fun4就是fun2的指針。
fun3 = fun1;
fun4 = fun2;
做了以上賦值后,調用fun3就相當於調用fun1,同理調用fun4就相當於調用fun2。調用方法如下
int a = 1; int b = 2; int ret = 0; int *pret = nullptr; ret = fun1(&a, &b); ret = (*fun3)(&a, &b); pret = fun2(&a, &b); pret = (*fun4)(&a, &b);
在這里我們看到好多用法定義都與數組和數組的指針類似。同樣fun3與fun1的區別,fun1和fun2是常量,不可以修改賦值,而fun3和fun4可以。
函數指針強轉
雖然這是一個小知識點,但是可以幫助我們進一步了解指針,比如我們有一個函數需要傳入函數指針,參數是void指針,需要我們把int指針參數的函數強轉傳入
int testcomp(int a, int b) { return a; } int testcomp1(long a, long b) { return b; }
void test(int (*comp)(int a, int b))
{
int a = (*comp)(111, 222);
cout << a;
} test(testcomp); test((int(*)(int, int))(testcomp1));
這里僅僅是為了測試說明,從long轉到int是被禁止的,防止溢出。
我們看到test是一個函數,函數的參數是一個函數指針,在c語言中想要傳遞函數,也只能通過指針的方式。這個函數指針返回值是一個int,有兩個int參數。
第一個調用就是把testcomp傳遞進去,函數形式與聲明的一致,所以不需要強轉
第二個調用,testcomp1與test定義的傳入的參數不一致,需要轉化一下,這里就可以看出來函數的指針形式如何定義
int(*)(int, int)這就是定義了一個函數指針的形式,int是返回值,(*)表示是一個指針,(int, int)表示傳入的參數是兩個int
令人頭暈的各種指針的聲明
在The C Programming Language[5.12 Complicated Declarations]中也介紹了,c語言有關指針的聲明,有時候非常迷惑,它並不能從左向右按照順序的解析,往往是從中間一個地方開始,向左右擴展,還要時不時的把一堆表達式看做一個整體。
char *a-char的指針
char **a-指向char指針的指針
char (*a)[13]-指向數組的指針,這個數組是包含13個char類型的數組
char *a[13]-含有13個char指針的數組
char (*a)()-函數指針,這個函數的返回值是char,傳入的參數是空
char (*(*x())[2])()-這是一個函數,函數的返回值是一個指向數組的指針,這個數組中包含的是一個函數指針,這個函數模型是返回char,傳入參數是空
char(*(*pa)[2])()-這就是上面函數返回值,這是一個數組指針,數組中有兩個元素,這個元素是一個函數指針,函數的模型是返回char,傳入參數是空
char (*(*x[3])())[5]-這是一個數組,數組中有3個元素,每個元素是一個函數指針,函數的模型是傳入參數是空,返回值是一個數組的指針,這個數組有5個元素,每個元素是char
再來一個
https://www.nowcoder.com/questionTerminal/87bba673cc844677baa0c12d32bdc330
int (*p[10])(int*)-這是一個數組,數組有10個元素,每個元素是一個函數指針,這個函數返回值是int,傳入參數是int指針
終極解釋
我們通過資料和示例可以總結一下關於指針或是c語言中定義類型的時候是如何拆分的了,首先與變量名最近的符號,表明了這個變量的類型,然后一層層向外增加額外的解釋,我們就一個個舉例實驗一下
int a-這里的a是變量名,從這里開始,向兩邊查找,只有int,那么a就是一個int
int *a-從a開始,有*,表示是一個指針,指針的原形呢?就是int,所以a是一個int指針
int a[10]-從a開始,a右邊是一個[],表示是一個數組,數組中有10個元素,元素的類型呢?把a[10]看做整體,就是int,所以a就是含有10個int元素的數組
int *a[10]-從a開始,a右邊是一個[],所以a是一個數組,數組中有10個元素,元素的類型呢?把a[10]看做整體,比如XX,那么就變成了int * XX,就與上面的int *a一樣了。有*,所以元素是指針,指針的類型是int,所以a就是包含10個int指針的數組
int (*a)[10]-從a開始,因為被()限制,所以a與*結合,那么a是一個指針,指針的類型呢?把(*a)看做一個整體,那么就是int[10],指針的類型就是一個數組,這個數組有10個元素,每個元素是int,所以a就是含有10個int元素數組的指針
int a[2][5]-從a開始,a的右邊是[][],所以a是一個二維數組,數組元素是int
int f()-從f開始,f右邊是(),所以f是函數,函數參數是空,返回值是int
int *f()-從f開始,f右邊是(),所以f是函數,函數的參數是空,返回值是int指針
int (*f)()-從f開始,因為被()限制,所以f與*結合,那么f是指針,指針的類型呢?把(*f)看做整體,那么就是一個函數,所以指針的類型是函數,函數的參數是空,返回值是int,所以f是一個指向參數是空返回值是int的函數的指針
char (*(*x())[2])()-從x開始,x右邊有(),所以x是函數。那么把x()看做整體,由於()限制,x()與*結合,表示函數返回值是一個指針。再把(*x())看做整體,與右邊的[2]結合,表示這是一個含有2個元素的數組,到這里的解釋是,x是一個返回值是一個數組指針的函數。再把(*x())[2]看做整體,前面又有一個*,表示這是一個指針,到這里的解釋是,x是一個返回值是一個數組指針的函數,數組中的元素是指針。再把(*(*x())[2])看做整體,就很明顯了,這是一個函數,類似於char XX(),函數的返回值是char,參數是空,最終這個表達式的意思是,x是一個函數,函數的返回值是一個數組指針,數組中有2個元素,元素的類型是一個函數指針,函數的返回值是char,參數是空。
char (*(*x[3])())[5]-從x開始,x右邊是[],所以x是數組。數組前面又*,所以數組元素是指針。指針后面有(),所以是函數指針。函數指針前面有*,函數返回值是指針。返回值指針后面有[5],所以函數返回值指針是數組。最后前面是char,所以x是一個含有3個元素的數組,數組的元素是一個函數指針,函數的參數是空,返回值是一個char[5]的數組指針。
int (*p[10])(int*)-p是數組,數組中的元素是指針,指針的類型是函數,函數的模型是返回值是int,參數是int*