本文代碼編寫編譯啟動並執行環境:[Mac-10.7.1 Lion Intel-based]
Q: 有的時候總是發現一個數組的字串可以修改,但是如果使用字串字面量就不能修改,這是為什嗎?
#include <stdio.h>int main(){ char buf[] = "hello"; char *str = "hello"; buf[0] = 'a'; str[0] = 'a'; return 0;}
代碼運行:
而且是運行到str[0] = 'a'; 的時候掛掉的。
A: 這是因為buf數組的資料存放在棧中,而str指向的字串資料儲存在全域唯讀資料區域。str[0] = 'a'; 修改了不能被修改的資料區塊。
Q: 怎麼才能知道char *str = "hello";這句代碼中的hello字串被儲存在全域唯讀資料區域裡呢?
A: 我們可以使用strings命令來得到上面代碼編譯成的可執行檔裡面的可列印字串。
假設上面的代碼儲存為char_string.c, 編譯: gcc -o char_string char_string.c
先看下strings程式的作用:
接著用strings char_string得到char_string可執行檔內部的可列印字串:
Q: 那麼怎麼證明char buf[] = "hello"; 中buf儲存的資料hello在棧中呢?
A: 使用gcc -S char_string.c得到它的彙編形式:
movbL_.str(%rip), %almovb%al, -14(%rbp)movbL_.str+1(%rip), %almovb%al, -13(%rbp)movbL_.str+2(%rip), %almovb%al, -12(%rbp)movbL_.str+3(%rip), %almovb%al, -11(%rbp)movbL_.str+4(%rip), %almovb%al, -10(%rbp)movbL_.str+5(%rip), %almovb%al, -9(%rbp)leaqL_.str(%rip), %raxmovq%rax, -24(%rbp)movb$97, -14(%rbp)
其中L_.str為:
L_.str:.asciz "hello"
可以看到,上面的彙編代碼中的%rbp即為和堆棧基址相對應的寄存器。
Q: 常常看到,如果arr是個數組,arr[i]和*(arr + i)是等同的, 這裡的i可以為負數嗎?
A: 是的。數組就是個資料區塊,至於是取arr更高地址還是更低地址的資料,這有程式員決定。當然,arr表示數組的初始地址,取比它更低的地址可能不是程式員的本來意圖,小心為之。
#include <stdio.h>#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));int main(){ int arr[] = {1, 2}; int n = 3; PRINT_D(arr[0]) PRINT_D(arr[-1]) return 0;}
編譯運行:
可以看到,arr[0]列印預期的1, arr[-1]輸出數組arr地址更低4個位元組(筆者的平台的int是4個位元組)空間資料的整形值。依據棧的原理,變數n正好儲存在這個位置,所以會輸出3.
Q: 經常看到多維陣列的形式,發現它的形式還有關於多維陣列相關變數的地址,不是很好地分析,如何很好地認識?
A: 如下例子:
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));int main (int argc, const char * argv[]){ int arr[2][3] = {1, 2, 3, 4, 5, 6}; PRINT_P(arr) PRINT_P(&arr) PRINT_P(arr[0]) PRINT_P(&arr[0]) PRINT_P(arr[1]) PRINT_P(&arr[1]) return 0;}
運行結果:
可以看到, arr和&arr是一致的,因為arr就是代表此資料的地址;它的地址依然和它一樣;而且arr的虛擬位址是在編譯階段即可確定。arr[0]也是一個地址,因為arr是二維數組,所以&arr[0]和arr[0]也是一致的;arr[0]即是arr初始一塊資料的地址,所以它們也是一致的;同理可分析,arr[1]和&arr[1].
Q: 對於多維陣列的形式,有的時候也有些不好理解;先拿簡單的一維數組來說,數組作為參數的形式是怎麼樣的?
A: 數組作為參數,只需要將首地址傳入即可,當然一般還可能需要數組元素個數的參數。形如:
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));void print_arr(int *arr, int size){ int i = 0; for(; i < size; ++i) PRINT_D(arr[i])}int main (int argc, const char * argv[]){ int arr[] = {1, 2, 3}; print_arr(arr, sizeof(arr) / sizeof(arr[0])); return 0;}
因為數組元素是整形的,所以數組地址為整形指標,即為上面的int *, 還有個參數size表示數組的大小。
Q: 既然數組的個數可以用sizeof(arr) / sizeof(arr[0])來表示,那麼參數size不就是不必要的嗎,print_arr函數裡面直接用這個運算式不就可以得到數組的大小了嗎?
A: 測試下。
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));void print_arr(int *arr){ int i = 0; for(; i < sizeof(arr) / sizeof(arr[0]); ++i) PRINT_D(arr[i])}int main (int argc, const char * argv[]){ int arr[] = {1, 2, 3}; print_arr(arr); return 0;}
運行:
Q: 為何數組arr的個數不正確了?
A: 我們可以看看print_arr函數的彙編形式:
0x0000000100000dd0 <print_arr+0>:push %rbp0x0000000100000dd1 <print_arr+1>:mov %rsp,%rbp0x0000000100000dd4 <print_arr+4>:sub $0x10,%rsp0x0000000100000dd8 <print_arr+8>:mov %rdi,-0x8(%rbp)0x0000000100000ddc <print_arr+12>:movl $0x0,-0xc(%rbp)0x0000000100000de3 <print_arr+19>:movslq -0xc(%rbp),%rax0x0000000100000de7 <print_arr+23>:cmp $0x2,%rax0x0000000100000deb <print_arr+27>:jae 0x100000e14 <print_arr+68>0x0000000100000ded <print_arr+29>:lea 0xa4(%rip),%rdi # 0x100000e980x0000000100000df4 <print_arr+36>:movslq -0xc(%rbp),%rax0x0000000100000df8 <print_arr+40>:mov -0x8(%rbp),%rcx0x0000000100000dfc <print_arr+44>:mov (%rcx,%rax,4),%esi0x0000000100000dff <print_arr+47>:mov $0x0,%al0x0000000100000e01 <print_arr+49>:callq 0x100000e6e <dyld_stub_printf>0x0000000100000e06 <print_arr+54>:mov %eax,-0x10(%rbp)0x0000000100000e09 <print_arr+57>:mov -0xc(%rbp),%eax0x0000000100000e0c <print_arr+60>:add $0x1,%eax0x0000000100000e0f <print_arr+63>:mov %eax,-0xc(%rbp)0x0000000100000e12 <print_arr+66>:jmp 0x100000de3 <print_arr+19>0x0000000100000e14 <print_arr+68>:add $0x10,%rsp0x0000000100000e18 <print_arr+72>:pop %rbp0x0000000100000e19 <print_arr+73>:retq
可以看到第七行位置的cmp指令和第八行的jae指令,表示如果迴圈變數i大於或者等於2,那麼跳到結束位置。也就是說,這個函數裡面,把arr數組大小看成了2, 這是為什麼呢?這是因為編譯器編譯print_arr函數代碼時,根本不知道傳入int * arr參數的到底是個數組還是指標,所以sizeof(arr) / sizeof(arr[0])得到的是sizeof(int *) / sizeof(int)的值(筆者平台得到的是2)。其實這也是數組名作為參數的一個可能引發錯誤的地方。
Q: 那麼如果把參數形式int *arr改為int arr[]就能將arr當成數組了,代碼就會正確執行?
A: 很可惜,c語言的文法決定了任何代碼都會編譯成確定指令的東西,和上面說的一樣,print_arr依然不知道外部傳入參數arr的數組或者指標到底有多大,sizeof(arr) / sizeof(arr[0])最終又被得到一個詭異的資料。
Q: 那該怎麼辦?
A: 就另外傳入一個參數為傳入數組的元素個數即可。
Q: 關於二維數組,經常看到一些很詭異的樣式,到底怎麼很好地理解?
A: 形如如下代碼:
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));int main (int argc, const char * argv[]){ int arr[] = {1, 2}; int (*p_arr)[2] = &arr; PRINT_D(**p_arr) return 0;}
運行結果:
可以看到, p_arr是一個指標,它指向一個包含2個整形元素的數組;arr數組正好滿足要求,所以p_arr可以指向它。這裡需要注意,p_arr的值是arr的地址,所以使用它的時候需要解引用。*p_arr表示數組arr, *(*p_arr)表示*arr, 也就是arr[0], 所以最後輸出數值1.
Q: 這裡使用p_arr太浪費了,直接用arr比它簡單多了!
A: 是的, 數組指標更多地可以用到二維或者多維陣列更能體現價值。如下代碼:
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));int main (int argc, const char * argv[]){ int arr[][2] = {{1, 2}, {3, 4}}; int (*p_arr)[2] = &arr[0]; PRINT_D(p_arr[1][1]) p_arr = &arr[1]; PRINT_D(p_arr[0][1]) return 0;}
運行結果:
可以看到,arr是二維數組,arr[0]是一個一維數組, &arr[0]是一維數組指標,p_arr是個指標,它需要指向一個包含2個元素的數組, &arr[0]正好符合,所以int (*p_arr)[2] = &arr[0]; 代碼ok; 緊接著,p_arr[1]表示p_arr指向的一維數組為單位的下一個數組,也就是arr[1]所在的數組; p_arr[1][1]也就等同於arr[1][1], 所以結果列印4;我想,你可以分析後兩句代碼的意圖了。
同時,你也可以看到,上面兩段代碼,同是int (*p_arr)[2],可以指向單純的一維數組,同時也可以指向二維數組中的一維數組,這就是指標,只要類型ok,就能指。
Q: 下面使用二維數組以及它的指標,為什麼會掛掉?
#include <stdio.h>#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));int main (int argc, const char * argv[]){ int arr[][2] = {{1, 2}, {3, 4}}; int **p_arr = (int **)arr; PRINT_D(p_arr[0][0]) return 0;}
運行結果:
A: arr是二維數組,arr[0][0]是沒問題的;p_arr是二級指標,它指向arr,也就是p_arr的值是arr.所以p_arr[0]就是以地址p_arr為地址的資料,也就是arr數組的第一個元素,即p_arr[0] 等於1.那麼p_arr[0][0]就是地址為1的一個整形資料,這能保證不掛嗎?
Q: 有的時候,發現下面這樣的聲明實在太難解讀了,有什麼好的方法嗎?
int (*p[3])(int *arg);int (*(*func)(int *p))[3];int (*(*func)[3])(int *p);
A: 要讀懂這些函數,需要掌握優先順序,函數指標的知識。
一一解析:
第一個:(*p[3]),[]優先順序比*高,所以p是一個數組,含有3個元素,*表示數組元素都是指標;接著,看到右邊(int *arg)表明前面的是個函數,參數是int *類型, 最左邊的int表示傳回值為整形;最後得到:p是一個數組,它含有3個元素,每個元素都是函數指標,函數指標的格式是: int (*)(int *arg);
由上,代碼例子:
#include <stdio.h>int (*func1)(int *arg);int (*func2)(int *arg);int (*func3)(int *arg);int main (int argc, const char * argv[]){ int (*p[3])(int *arg) = {func1, func2, func3}; return 0;}
第二個:*func表示func是一個指標,後面的int *p表示,它是一個函數指標,參數為int *p, 左側一個星號,表示傳回值是個指標,右側[3]表示傳回值是個3個元素的數組,每個元素都是指標,最左側的int表示傳回值的數組元素為整形。總結下:func是個函數指標,參數為int *p, 傳回值為包含5個元素的數組,且為指標。
第三個: 類似第一個,不過func多了一個指標類型。總結:func是一個指標,它指向一個數組,數組元素個數為3,每個元素都是一個函數指標,函數指標參數為int *p, 傳回值為int.
xichen
2012-5-14 12:46:50