定義了char**p,為什麼能用p[i][j]的形式?p又不是二維數組?

定義一個結構體:

typedef struct _tag_Mat

{

int rows;

int cols;

char **img_char;

}Mat;

在這個函數中用到調用結構體變數:

void rebit(Mat *srcImg,Mat *dstImg)

{

int i,j;

for(i =0;i&rows;i++)

{

for(j=0;j&cols;j++)

{

dstImg-&>img_char[i][j] = (char)(srcImg-&>img_char[i][j]&>&>0);

}

}

}

問題:在rebit函數中,dstimg-&>img_char在結構體定義中是個指向指針的指針,沒說它指向某數組,那它就不是一個二維數組,怎麼可以直接用dstimg-&>img_char[i][j]的形式,不理解。


因為[]操作符是這麼定義的:

C++11 §5.2.1:E1[E2]等同於*((E1)+(E2))

這裡E1是指針類型。

C++11 §4.2規定數組名在一些場合可以隱式轉換成指向其首元素地址的指針(有sizeof之類的例外),這才是你可以用

數組名[序號]

訪問數組成員的原因。


我們可以分步去看這個問題。

首先是char* p1:指向char的指針。我們可以用p1[i]去訪問以p1[0]為起始點的內存區域,默認使用char類型操作,i自增一次內存偏移sizeof(char)=1個位元組。

接下來我們定義char**p2=p1:指向char型指針p1地址的(指針的)指針。於是我們若使用p2[j]的方式偏移,j自增時必然每次偏移sizeof(char*)個位元組。那麼,我們使用p2[j]的方式訪問的當然就是不同的char型指針了。

這樣來看,第一步我們創建了char數組,第二部我們創建了char*數組。於是我們若使用p2[i][j]的方式,就實現了二維數組(N個一維數組不就是二維數組咯)。

簡單來說,就是(p2[i])[j]


數組類型訪問下標也沒有長度檢查的,顯然[]的意思永遠是在做指針運算,數組都是先轉成指針然後再[]的。看編譯後的彙編代碼馬上明白。


C語言裡面,大多數情況下,指針就是數組,數組就是指針,僅僅在少數情況下有區別。

這少數的情況用得並不算多,等你遇到了就知道了。

另外,就算是數組,經過函數傳遞後也退化成指針,於是跟指針還是一回事。

如果有更多需要了解的,可以在評論中提問。


因為type* p可以進行p[i]。


數組和指針的事其他答案說的也差不多了

其實就算從運算符看這個事也是很正常的啊,定義了double a,為啥可以a+1,a又不是int……

不同類型用同樣的運算符(當然一般而言,設計為實現了類似功能,而不會用+來做乘法之類)是很正常的事情


內建的 [] 運算符本只是為指向完整對象的指針類型定義的,而非為數組定義。

有內建下標運算符的地方,都要求內建數組隱式轉換成指針。


@蔣晟 朋友的是最正確最標準的答案。不過有點晦澀,過於微言大義,所以大多人看不明白。

簡單說,就是C/C++標準存在兩個不同的規則,這兩個不同規則使得指針/數組的訪問在大多數場合表現一致(這是刻意為之,並非「恰好」)。

第一個是關於如何定義數組以及數組到指針的退化規則:

char array1[2]

定義了一個有兩個char元素的數組;

當要給一個要求char *的函數傳參(或其它場合)時,array1可隱式退化為類型為char *的指針。

char *array2[2]

定義了一個有兩個char *元素的數組

array2可隱式退化為char** ——注意它和上一個類型並不一樣。

char array3[3][2]

定義了一個有三個元素的數組,這個數組的每個元素是一個有兩個元素的char型數組。

注意這裡的特殊之處。array3退化成的指針是char[2] *,並不是char**!

如果你這樣定義一個函數:

void func(char array[][]) {},編譯器會報錯,因為不知道array[]指向的一維數組長度,無法生成有效代碼。

正確的定義方法是:

void func(char array[][2]) {} —— 現在它可以接受array3了。

另一個更啰嗦但等價的做法是:

void func(char array[2][2]) {} —— 你仍然可以傳給它array3!

這是因為數組隱式退化規則把參數列表中的array[2][2]當作一個指向char[2]的指針了,它並不檢查指針本身的數組長度。

但是,如果我想定義一個接受 char *array2[2]的函數,該如何聲明呢?

你可以: void func(char *array[2])

相當於: void func(char **array)

也就是說,char *array[2]實質上是退化到了char **。

換言之,char array[m][n]可以解釋為「一個有m個元素的數組,數組元素類型是char[n]」

相對應的,char *array[m] 可以解釋為「一個有m個元素的數組,數組元素類型是char*」。

此時,元素類型char[n]是不可以隱式退化為char*的。

————————————————————

為什麼要搞得如此複雜呢?

為了用起來簡單而統一。

這就是第二個規則:對指針type * p,p[n]的含義是*(p+n)

其中,指針加法的定義是 p指向的內存地址 + n * sizeof(type)。

這什麼意思呢?

意思就是,對 short *p,p+1總是指向緊挨著排列的第二個short元素——short是2位元組,故p+1實際上指向了p所指元素首地址之後兩個位元組的那個位置,如下:

[11][22][33][44][55][66]

p指向[11]這個格子,short有2位元組,所以1122是真實存儲的short值;p+1必須跳過[11][22]這兩個格子,才能正確取到第二個short(值為3344)——這裡假設為網路位元組序,不知道這是在說什麼的可忽略。

類似的,當我們需要取出第三個short元素(即值5566)時,應該把p指向位置後移兩個short大小(2 * sizeof(short));第n個元素就是後移(n * sizeof(short))。

你可以不知道這些,只要告訴編譯器,你要p[2](或者*(p+2)),編譯器會自動根據指針類型,幫你跳過合適大小個位元組,讓你取出正確的元素。

前面提到過,char array3[][n]是一個元素類型為char[n]的數組;所以array3+2是什麼意思呢?

意思就是:

1、取array3的首地址,把它視為一個指向char[n]類型的指針

2、array3+2表示取第三個元素

3、前面提到過,取第三個元素實際上是array3所指的地址後移了兩個sizeof(char[n])位元組

4、所以,array3+2就指向了 (char*)array3 + 2 * sizeof(char[n])這個位置,這裡剛好就是array3[2][0]

類似的,對char *array2[2],array2+1的含義則是:

1、array2退化為一個指向char*的指針,即char**

2、array2+1表示取第2個元素

3、array2存的是一系列char*指針,所以第2個char*指針需要後移一個sizeof(char*)位元組

4、所以,array2+1就指向了array2[1]的首地址

你看,利用指針加法規則、把數組視為類型,我們就可以把「指向指針的指針」和「二維數組」的訪問形式統一起來了。

類似的,你可以把這套東西推廣到三維數組(指向指針的指針的指針)乃至N維數組……

只不過,接受三維數組的函數,你必須精確的聲明為 type array[][m][n]——除了第一維,別的可不能有半點含糊哦。


方括弧記法是指針運算的語法糖,就是為了寫起來方便才有的。所以所有符合該語法糖結構的東西都可以這麼用。


C語言里,數組名字就是數組第一個元素的地址,數組的下標是相對於第一個元素的偏移量。數組名可以當指針使用,指針也可以用數組的語法來引用後面的數據。


講個笑話,在C/C++裡面你聲明了數組a[5]

然後你寫1[a],

IDE可能會給你報錯,

但是編譯器不會……

*(a+1)和*(1+a)沒區別23333

畢竟加法滿足交換律Σ(|||▽||| )


先說結論手動測試會crash,不知道是不是姿勢不對,img_char[i][j]怎麼確定二維上j的大小,這只是一個char** img_char的指針,二維上j的大小不能確定,即img_char並不是指向數組的指針,怎麼知道*(*(img_char+i)+j)在+i上需要跨過多少內存地址。

明確兩點:

  1. []代表解引用
  2. 解引用的前提是編譯器知道需要被解引用的起始地址,被解引用的大小,即一次解引用的地址長度。

char type_char_array[2] = {"z", "h"};
char* type_char_ptr = type_char_array;
std::cout &<&< type_char_ptr[1] &<&< std::endl; // h std::cout &<&< *(type_char_ptr + 1) &<&< std::endl; // h

可以看到[]可以代替*進行解引用,但有一個前提,編譯器需要知道所指類型的大小,這裡是char*,那麼大小就是sizeof(char)

測試如下:

#include &

int type_array[2][2] = {{1,2}, {3,4}};

int **type_second_ptr;
int (*type_array_ptr)[2];
int *type_int_ptr;

int main() {
using namespace std;
cout &<&< sizeof(int) &<&< endl; cout &<&< sizeof(type_array[0]) &<&< endl; cout &<&< sizeof(type_array_ptr) &<&< endl; cout &<&< sizeof(type_int_ptr) &<&< endl; cout &<&< sizeof(type_second_ptr) &<&< endl; type_second_ptr = (int**)type_array; // cout &<&< type_second_ptr[0][1] &<&< endl; // 崩潰 /* 兩種解決辦法: 1、利用數組指針進行定址 2、利用內存連續的性質,一維指針定址 */ // 數組指針 type_array_ptr = (int(*)[2])type_array; cout &<&< type_array_ptr[0][1] &<&< endl; // 連續地址,所以可以用int*指針連續取值 type_int_ptr = (int*)type_array; cout &<&< *(++type_int_ptr) &<&< endl; } /* 4 8 4 4 4 2 2 */


多維數組在定址的時候是要轉化到一位地址上去的,畢竟內存地址只是一維的,


出於教學的目的(因為數組理解起來簡單),大部分人先接觸數組,就以為下標運算符直接作用在數組上。其實數組隱式轉換成指針,才能用這個下標運算符。

好的書會提及這個,但是基本上你理解也得等一年之後了。

數組和指針的關係,你可以類比 int 和 double。他倆功能相似,內存布局不同,一個可以隱式轉換成另一個,但反過來不行。


其他回答基本解釋了數組類型的一些特性,我補充三點我認為很重要的內容。

一是定義數組類型,和下標引用操作符,是語義有關聯但不完全一樣。

如下代碼:

char a[2][3];

char **p;

a[1][2]="c";

*(*(a+1)+2)="c";

前2行定義的a和p類型是不同的,a是char [2][3]數組類型,p則是char **指針類型,但後2行表達式是等價的,即[]操作符是*操作符的一種簡寫,*p和p[0]也是等效的,p[m]即是*(p+m)。

二是數組類型出現在表達式或函數參數定義中時,大部分情況下,會退化成指向最左一維元素(而非整個數組)的指針。

如以上表達式a[1][2]="c"中的a,會退化成char (*)[3]類型,即指向擁有3個char元素數組類型的指針類型。

如int a[2][3][4];後,a[1][2][3]中的a,會退化成int (*)[3][4]類型。這樣設計就是為了方便在指針和「數組最左一維以後的類型構成的一維數組」之間,方便的相互轉化,利於建立對數組非傳值而傳指針操作,避免複製數組帶來的開銷。

以此類推:

a[1]等價於*(a+1),在做指針加法時,a已由int [2][3][4]退化成int (*)[3][4],+1會在首地址的基礎上,跳1*3*4*sizeof(int),再*解引用時,類型為int [3][4]。

a[1][2]等價於在a[1]位置的int [3][4]類型上(假定我們叫x),再做*(x+2)。結果是int [3][4]退化成int (*)[4],指針加法會在x地址上,跳2*4*sizeof(int),再*解引用,類型為int [4]。

a[1][2][3]等價於在a[1][2]位置的int [4]類型上(假定我們叫y),再做*(y+3)。結果是int [4]退化成int *,指針加法會在y地址上,跳3*sizeof(int),再*解引用,類型為int,即具體的一個int值。

數組不退化為指針的情況主要有:一是sizeof時。如上例中若有sizeof(a)肯定和sizeof(p)不一樣,後者也就是個普通指針變數所佔位元組數,前者是整個數組所佔位元組。二是取地址時,如上例中若有a則返回一個int (*)[2][3][4]類型,這個指針若做+1再引用,即(a)[1],或*(a+1),則跳過了整個數組的邊界之後,解引用訪問可能會出問題,但這與一般類型的取地址後加1是一致的。

可以看出C語言這麼設計,是在數組內元素在內存中連續存放這個假定下,方便以指針的增減,來操縱數組內元素。一旦習慣,其實也挺方便。

數組類型由於需要在編譯時可以確定最初定義的固定大小,因此各個中括弧里的值也必須是編譯時可確定的(如宏,字面量,而不能是變數)。少量場合可以省略首個中括弧中的數值,但是後續的中括弧中不能省略。

三是指向數組的指針不等價於指向指針的指針。

數組指針相互可賦值通常採用這樣的模式:

char a[2][3][4],(*p)[3][4];

p=a;

注意char **p與char (*p)[4]是不同的類型:前者是個二級char指針,即一次*解引用後,指向的類型是char *,沒有數組的大小做指導信息,而是要求所指之處真正有個放置指針值的地方;後者是真正的數組類型,解引用會自動確定每一維的實際最大長度。C的main函數的命令行參數argv,類型寫成char *argv[],其實即是char **。


char **p;

p[i][j]相當於*(*(p+i)+j)


請用指針數組 比如 char *p[3]。

二維數組不過是數組的數組在內存中依舊是線性的,但是被分的更細了(但是單位長度還是char的大小1個位元組),也就是說p[0],p[1]都是指針,指向char*類型,即字元串。

p[0],p[1]之差(前提是你把指針強轉成unsigned int)為p[0]指向的字元串的長度


C里是沒有string這個東西的,所以所有的string本質上都是char array,而c里的array本質上是一個指向array第一個element的pointer,所以你可以把string理解成char *

那如果一個string array呢

就是一個string pointer,which is char ** p

所以一個string array,的p[i][j] is the i-th string in p, and the j-th char in that string. Which clearly is a 2D array


指針才是本質,數組也是先轉換成指針,然後和char **p進行相同的處理。


如果學習過彙編語言,哪怕是學過單片機的彙編指令,就會知道,無論是c語言的指針還是數組存取數據,被編譯成彙編指令後,都是「變址定址」。所以,建議題主大致看一下彙編語言。


推薦閱讀:

int a=9;printf("%d,%d",a,a++)?
在c語言中寫出變數=1./0.是啥意思?
有哪些比譚浩強主編的《C 語言程序設計(第四版)》能讓人更快學會 C 語言,且適合新手的書籍?
有C語言基礎,想把C語言學到極致,有什麼比較好的書籍推薦么?
老師讓我學習C,彙編,看c編譯成的彙編代碼,說以後幹什麼都難不倒。?

TAG:C編程語言 | C | CC | VisualC | C語言入門 |