標籤:

C語言中定義int a[10][10],a是什麼類型?

我的理解是a是指向二維整型數組的指針

a[0]是指向一維整型數組的指針

a[0][0]是整型

這樣對嗎?


藉助Clang的AST來講解這個問題。

對於這樣的代碼例子:

int foo() {
int a[10][10];
return a[2][3];
}

在Clang中上述代碼對應的AST長這樣:

`-FunctionDecl 0x102829c20 & foo "int ()"
`-CompoundStmt 0x102829fa0 & |-DeclStmt 0x102829e08 & | `-VarDecl 0x102829db0 & a "int [10][10]"
`-ReturnStmt 0x102829f80 & `-ImplicitCastExpr 0x102829f68 & "int" &
`-ArraySubscriptExpr 0x102829f40 & "int" lvalue
|-ImplicitCastExpr 0x102829f28 & "int *" &
| `-ArraySubscriptExpr 0x102829eb0 & "int [10]" lvalue
| |-ImplicitCastExpr 0x102829e98 & "int (*)[10]" &
| | `-DeclRefExpr 0x102829e20 & "int [10][10]" lvalue Var 0x102829db0 "a" "int [10][10]"
| `-IntegerLiteral 0x102829e48 & "int" 2
`-IntegerLiteral 0x102829ed8 & "int" 3

可以看到,Clang所理解的是:

  • a的類型就是int [10][10],不多不少
  • 在把a當作指針使用時,它的類型會通過隱式類型轉換來退化為一個指針類型,int (*)[10]
  • 然後,第一維下標訪問後的表達式類型為 int [10]
  • 然後跟前面過程同理,把這個值當作指針使用會導致它的類型被退化為 int *
  • 然後,第二維下標訪問後的表達式類型為 int。


a的類型就是int[10][10],a[0]的類型是int[10],a[0][0]的類型是int

數組類型參與運算可能會自動轉換為指針,int[10][10] -&> int (*)[10],int[10] -&> int *。也就是說a會變成一個指向一維數組的指針,a[0]會變成一個指向int的指針


0. 如何理解

不過如果要更加系統化地理解C的類型表達式,可以使用下面的方法

一個聲明本身可以看作是一個不斷嵌套括弧的「表達式」,例如int a[10][10]這個語句,其實它的構成是:

int (((a)[10])[10])

編譯器在遍歷抽象語法樹的時候是這麼考慮的:

1. 首先,令x1 =(((a)[10])[10]),這時相當於int x1;x1的類型是int,記作x1.type = int。

2. 接著,領x2 = ((a)[10]),這時有x1 = x2[10];這說明x2是一個長度為10,元素類型為x1.type的數組,其類型x2.type = array(10, x1.type) = array(10, int);

3. 然後,令x3 = a,則有x2 = x3[10];則有x3.type = array(10, x2.type);代入2)中的結果得:x3.type = array(10, array(10, int))。

其中,array(length, type)表示一個長度為length,元素類型為type的數組類型。

用遞歸方法表示就是:

1)a.type = array(10, t1)

2)t1 = array(10, t2);

3)t2 = int

1. 舉一反三

那麼好了,用這個方法,我們可以解釋相當一部分很難理解的類型,例如這個詭異的聲明語句:

int (*(x[10]))(char, char)

這是個什麼鬼呢?首先,我們按照層次順序來看:

1)x1 = (*(x[10]))(char, char);這時候聲明等價於int x1;說明x1.type = int;

2)x2 = (*(x[10]));這時由x1 = x2(char, char),說明x2是一個返回類型為x1.type,輸入兩個char類型參數的函數,我們記作x2.type = func(x2.type, [char, char]);代入x1.type得到:x2.type = func(int, [char,char]);

3)x3 = (x[10]);這時候有x2 = *x3,說明x3是指向x2類型的指針,因此x3其實就是個函數指針啦!有:x3.type = ptr(x2.type) = ptr(func(int, [char,char]))

4)x4 = x;這時候有x3 = x4[10],說明x4是一個指向x3類型的長度為10的數組,即:x4.type = array(10, x3.type) = array(10, ptr(func(int, [char,char])))

那麼x的類型是什麼呢?用自然語言說起來有點繞口,那就是:

1)x是一個長度為10的數組,它的元素類型為t1;

2)類型t1是一個指針類型,t1指針內容的類型為t2;

3)類型t2是一個函數類型,該函數輸入兩個char類型參數,返回類型為t3;

4)類型t3為int

2. 如何使用?

聲明一個複雜類型的變數容易,但是能合法地使用並不是很簡單。以章節1部分的那個聲明為例:

int (*(x[10]))(char, char)

這個變數的一個合法使用是:x[1]("a", "b")

怎麼理解這個表達式?也是按照層次來分析:

1)首先x[1],由於x.type = array(10, ptr(func(int,[char,char])))),

因此x[1]的類型是ptr(func(int, [char, char])),即一個指向函數的指針,指向的函數接收兩個char類型值,返回int類型。

2)接著(x[1])("a","b"),相當於在x[1]的類型上再調用一次函數,由於x[1]是一個函數指針,因此可以直接作為函數調用,它接受了兩個字元輸入,因此這個表達式其實就是一個函數調用!

那麼簡單的如何解釋呢?

x[1]首先獲取x數組位置1的函數指針;

接著用參數("a","b")調用x[1]所指向的函數;

最後,調用完成後,返回這個函數在("a","b")下的返回值。

3. 應用

在弄明白了C語言這些聲明語句的層次關係後,現在請設計一個函數表類型,這個函數表輸入函數名稱,返回一個該函數名稱的重載函數列表的指針;這些函數都有相同的聲明形式,如下:

int func(void *, void *)

在C++中,這麼聲明(本人沒嘗試過,哪位閑著無聊可以試一下能不能通過編譯。。。):

using namespace std;

map& func_map;

例如,當我要使用名稱為「compare_100」函數名的函數列表時,通過:

auto list = func_map["compare_100"];

接著,我們選取這個list的第一個函數指針

auto fptr = list[0];

最後,調用該函數:

auto retval = fptr(x, y);

當然你也可以一步寫完:

auto retval = func_map["compare_100"][0](x, y);

4. 用C寫函數式編程範式程序

最後各位可以試一下下面這個定義聲明,是不是能通過編譯?理解一下它神馬意思?

int (*(select_function(char *))(char, char));

給你們一點提示,下面是使用這個變數select_function的例子:

auto fptr = select_function("min_char");

auto a_b_min = fptr("a", "b");

是不是有點像函數式編程範式?根據名稱返回一個函數?

順帶一提,這可都是用純C/C++語法寫出來的喲,沒藉助什麼函數編程範式的特有語法哦╮(╯▽╰)╭

PS:上面的聲明我沒有試著用VS測試過,如果有錯的話請輕噴=A=!

PPS:當年碩士項目組的靜態分析器我們就是這麼識別變數類型的,如果這些聲明有錯,那,那我反正是不負責啦(怒摔)!


a(數組名)是指向數組第一個元素的指針,第一個元素什麼類型,他就是什麼類型,這裡的話,第一個元素類型仍然是個數組!

20161212update

如果要聲明一個指針指向a,你就要聲明一個數組指針: int (*p)[10]=a;


二維數組a可以看作a[0]到a[9]十個元素組成的一維數組,a是一維數組的數組名,代表一維數組的首地址。a[0]就是一維數組的數組名,它也是個地址常量。代表一維數組的首地址,即第一個元素a[0][0]的地址。

就這麼簡單,懂了不。


其實a就是一個內存地址

所謂的a[10][10]只不過是向下查10*10個4位元組的內存單元~

所有的數組都是語法糖~


任意一個元素都是int整型,a就是數組類型


舉一個Java的例子,更容易理解

int[] i=new int[5];

i指的是首地址,毋庸置疑,不過類型是什麼?還是數組。

測試一下:

int a[10] 和 int a[10] [10] ,通過a+1去操作,看看取到的具體是哪個值


大牛們都答完了。我只是補充個方便理解的方法作為課外閱讀: C語言的右左法則

C Right-Left Rule (Rick Ord"s CSE 30


a是整型二維數組


a[0][0]是整型 沒錯。

但數組名與指針不同,程序不會為數組名分配內存空間,僅在它出現時將它替換為首元素地址。


首先回答問題:a是一個數組,它的每個元素都是一個數組,而這個數組中的每一個元素都是一個int型。

題主的理解有問題,需要明確的一點是,「指針是什麼」,指針是一種變數類型,如果說a是一個指針(實際上就是在說它是一個指針類型的變數),那麼它就具有變數的共性;而另一個問題就是,「變數有什麼共性」,變數的一個重要共性就是「必須有存儲它的內存空間」即「存儲該變數的內存起始地址」和「該變數所佔用的內存的大小」。繼續分析下去,如果a是個指針的話,那麼在32位機上,內存中就應該有一片4位元組大小的內存用來存儲a變數的值,也就是它所指向的內存地址。這樣的話,你對a取地址(操作),那麼該內存地址中所存儲的值就應該是一個內存地址(它應該在數值上等於你所認為的那個變數所存儲的內存地址),所以再對取來的地址進行內存訪問去取該地址中所存儲的數據,你就應該得到一個內存地址。但你會發現你得到的並非是內存地址,而是a的第一個元素的值。至此,你的想法被證偽。


數組不是指針,你sizeof一下數組 再sizeof一下指針就看出差別了。


首先,看下二維數組在內存中的布局,是一個線性序列,一共包含了三個元素,每個元素又可以看做一個線性序列,這裡面包含了10個int類型的元素。

從這個大的線性序列的第一個元素開始移動,剛開始是在包含10個int類型元素的線性序列內移動,此時變化的是第二維的下標。當計數到第10個int元素時,下一個元素將轉移到這個大的線性序列的第二個元素,此後的過程類似。

也就是二維數組實際上可以看做是一維數組的組合,只不過把二維數組看做一維數組時,數組中的元素又是一個一維數組。

在一維數組,如int a[10]中,絕大多數情況下,數組名a可以看做是指向數組第一個元素的常量指針。類似的,在二維數組int a[10][10]中,數組名a絕大多數情況下,也可以看做指向二維數組第一個元素(即把二維數組看做一個廣義的一維數組時第一個元素)的常量指針,也就是指向含有10個int類型元素的數組的常量指針,即int (*)[10]。而a[0](即 *(a+0) )是對a的解引用,是一個指向int類型的指針。更進一步,a[0][0]是一個int類型的元素。

對於更多維數組,按照比方法,依次深入,可得到每一個元素的類型。


二階指針常量


地址,轉成彙編即可看出


我來個最簡單的,a是整數型,寬度依據編譯器和硬體決定


首先說結論,a或者a[10]用的時候可以等價於常量指針。但數組名一般情況只是編譯時概念,而指針是運行時概念。

實際反彙編時可以發現是沒有a或者a[10]對應的指令的,為什麼?

因為a[10][10]定義的內存是放在棧上由函數自動維護的,無論你是用a還是a[10],在編譯時就能知道偏移,所以a[10][10]告訴編譯器定義了一塊內存,a和a[10]告訴編譯器內存的地址和長度信息。

為什麼通常情況下說棧訪問比堆訪問要快,快就快在訪問棧內存基本上直接可以從cache中讀取到,而通過指針訪問的堆內存需要額外一次定址,往往會cache miss。


a是int** 類型。要解應用數組的元素是,通過對int*移位指向該元素的所在行,然後再通過在該行的移位取得該元素即*(*(a+i)+j)


可以這麼理解:數組有三個要素,首地址,單位長度,個數。

c語言裡面的指針變數就是用來保存地址的, 所以「數組首地址要有類型那就是指針類型」。

為什麼說「要有類型……就是……」,因為c語言裡面類型可以隨便的強制類型轉換,數組可以很直觀的對指針進行偏移,也可以通過指針自增操作來偏移。

一唯數組首地址等同於 一級指針,二維等同於二維


推薦閱讀:

C 字元串常量的空間是不需要回收的?
C++數組名可以看成指針么?
C 語言指針怎麼理解?
同樣的數組參數,用sizeof求數組長度為何會產生不同的結果?
國外有什麼優秀的c語言入門教學視頻?

TAG:C編程語言 |