1. 程式人生 > >陣列、函式與指標

陣列、函式與指標

參考

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*

&n