1. 程式人生 > >char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on

char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on

本文測試環境 :  X86-64 bit 架構的伺服器 CentOS x64 5.x gcc version 4.1.2 20080704
指標和陣列是C的比較難搞懂的知識點, 需要結合記憶體來學習, 非常感謝各位兄弟為我指點迷津. 下面總結一下 :  首先說明一下C程式在執行時, 不同的內容或變數分別儲存在什麼地方? 分了幾塊區域分別是, code, constants, global, heap, stack; (記憶體地址從低到高) 其中constants儲存常量(常量值不允許修改), global儲存在所有函式以外定義的全域性變數(全域性變數允許修改), heap是一塊動態記憶體區域(可存放持久化內容, 不會自動釋放記憶體), stack存放函式內的本地變數(函式執行完後本地變數佔用的記憶體將自動釋放);  
如圖 :  char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on in memory - 德哥@Digoal - The Heart,The World.
接下來要介紹兩個符號 * 和 &. 1. * 用在定義變數型別, 或者 強制型別轉換時, 表示要定義一個指標 或者 把這個變數型別轉換成指標(並告知這個指標指向的是什麼型別的資料) . 2. * 用在一個指標變數的前面, 表示這個指標指向地址儲存的內容 (內容是什麼型別的就取決於這個指標定義時:  告知這個指標指向的是什麼型別的資料,指標的加減運算得到的記憶體地址也和指標 指向的是什麼型別的資料相關,int * a,a+1 得到的地址則是在a指向的地址基礎上加4位元組.). 3. & 用在變數名的前面, 表示這個變數所在的地址 . 
例1 : 

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char * a = "hello";   fprintf(stdout, "&a:%p, a:%p, a:%s\n", &a, a, a);   return 0; } 結果
[[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b &a:0x7fff71b22230, a:0x400618, a:hello


&a 表示a的地址 .  a 表示a儲存的內容, 因為a是指標, 所以它的內容讀取出來就是一個地址 .  *a 表示a指標儲存的這個地址 儲存的內容 .  在記憶體中的圖示 :  char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on in memory - 德哥@Digoal - The Heart,The World.   這個圖包含了幾塊內容 :  1. 在64位的系統中 , 指標佔用了8個位元組. 因為指標中儲存的是地址, 而且地址是64位的. 所以需要8個位元組. 2. 在x86架構的機器中, 記憶體填充是從低位到高位的. 所以hello在記憶體中是這樣儲存的 :  地址:  內容

   

0x400618 : 0x68  (ascii : h) 0x400619 : 0x65  (ascii : e) 0x40061a : 0x6c  (ascii : l) 0x40061b : 0x6c  (ascii : l) 0x40061c : 0x6f   (ascii : o) 0x40061d : 0x68  (ascii : NULL)


3. 那怎麼來證明以上是正確的呢? 很簡單, 按照每個位元組來列印就知道了. 這裡需要注意的是指標的加減法得到的地址和指標指向的地址儲存的內容的型別是有關的, 反過來說, 要讓指標加1剛好得到的是下一個位元組那就告訴編譯器, 這個指標指向的地址的內容是char型別就好了, 因為sizeof(char) = 1位元組. 先來列印一下hello是不是按照上面說的這樣儲存的?

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char * a = "hello";   fprintf(stdout, "&a:%p, a:%p, a:%s\n", &a, a, a);   fprintf(stdout, "sizeof(char *):%lu, sizeof(char):%lu\n", sizeof(char *), sizeof(char));   fprintf(stdout, "a+0:%p, *(a+0):%x, *(a+0):%c\n", a+0, *(a+0), *(a+0));   fprintf(stdout, "a+1:%p, *(a+1):%x, *(a+1):%c\n", a+1, *(a+1), *(a+1));   fprintf(stdout, "a+2:%p, *(a+2):%x, *(a+2):%c\n", a+2, *(a+2), *(a+2));   fprintf(stdout, "a+3:%p, *(a+3):%x, *(a+3):%c\n", a+3, *(a+3), *(a+3));   fprintf(stdout, "a+4:%p, *(a+4):%x, *(a+4):%c\n", a+4, *(a+4), *(a+4));   fprintf(stdout, "a+5:%p, *(a+5):%x, *(a+5):%c\n", a+5, *(a+5), *(a+5));   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b &a:0x7ffffe249680, a:0x4006f8, a:hello sizeof(char *):8, sizeof(char):1 a+0:0x4006f8, *(a+0):68, *(a+0):h a+1:0x4006f9, *(a+1):65, *(a+1):e a+2:0x4006fa, *(a+2):6c, *(a+2):l a+3:0x4006fb, *(a+3):6c, *(a+3):l a+4:0x4006fc, *(a+4):6f, *(a+4):o a+5:0x4006fd, *(a+5):0, *(a+5):

解說 :    a 是一個指標, 這個指標指向的內容是char型別的, 所以這個指標的加減運算a+1 表示地址加1位元組.   *a 把你帶到它指向的 0x4006f8, 而fprintf 以16進位制和char輸出的正是 0x4006f8這個地址的 這個位元組上的內容(1位元組剛好用兩個16進位制數表示, 剛好可以轉成char)   a+0這裡只是為了說明指標的加減運算, 可以去掉+0.    sizeof(char *):8 表示指標佔用了8位元組, sizeof(char):1 表示char佔用了1位元組.
4. 接下來列印 &a 這個地址是不是也是按照從低到高儲存的? 為了一個位元組一個位元組打出,  我們需要再定義一個指標b , 用指標b來做加減運算, 但是這樣能如願嗎?

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char * a = "hello";   char ** b = &a;   fprintf(stdout, "sizeof(a):%lu, b:%p, &a:%p, b+1:%p\n", sizeof(a), b, &a, b+1);   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b sizeof(a):8, b:0x7fff3692b520, &a:0x7fff3692b520, b+1:0x7fff3692b528

解說 :  sizeof(a)=8 結果告訴我們, a這個變數佔用了8位元組, 因為它儲存的是個記憶體地址, 在64位的作業系統中這個是可以理解的. b這個指標指向a, 所以b和&a 打印出來的值轉成記憶體地址當然是一樣的. b+1 運算得到的地址當然應該是加8個位元組 . 因為地址做加減運算得到的當然還是地址, 至於得到的結果和什麼有關, 當然和這個做加減運算的指標定義時所告知的它指向什麼型別的資料有關. char ** b, ( *b 表示 b是一個指標, 然後char *則是告訴你b指向的是一個指標, 甭管是什麼型別的指標,反正b指向的是一個指標, 一個指標佔用8位元組, 所以b+1就是加8個位元組)
那怎麼樣能讓b+1結果是加1個位元組呢? 這裡要用到強制型別轉換. (char *) b 就把b強制轉成char *了.  所以 ((char *) b)+1 就是加1位元組. *( ((char *) b)+1) 就是取這個地址的內容. %x表示取一個位元組並按照16進位制打印出來.

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char * a = "hello";   char ** b = &a;   unsigned short i;   unsigned short x = (unsigned short) sizeof(a);   fprintf(stdout, "&a:%p, a:%p\n", &a, a);   for(i=0; i<x; i++) {     fprintf(stdout, "i:%u, ((char *) b)+i:%p, *(((char *) b)+i):%x\n", i, ((char *) b)+i, *(((char *) b)+i));   }   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b &a:0x7fff1ffff608, a:0x400728 i:0, ((char *) b)+i:0x7fff1ffff608, *(((char *) b)+i):28 i:1, ((char *) b)+i:0x7fff1ffff609, *(((char *) b)+i):7 i:2, ((char *) b)+i:0x7fff1ffff60a, *(((char *) b)+i):40 i:3, ((char *) b)+i:0x7fff1ffff60b, *(((char *) b)+i):0 i:4, ((char *) b)+i:0x7fff1ffff60c, *(((char *) b)+i):0 i:5, ((char *) b)+i:0x7fff1ffff60d, *(((char *) b)+i):0 i:6, ((char *) b)+i:0x7fff1ffff60e, *(((char *) b)+i):0 i:7, ((char *) b)+i:0x7fff1ffff60f, *(((char *) b)+i):0

解說 :  a變數存放在記憶體地址 0x7fff1ffff608這裡的連續8個位元組中. a變數的8個位元組中儲存了什麼內容呢?  0x400728 它是從低位到高位儲存的, 如上面的結果, 28存在 0x7fff1ffff608, 07存在 0x7fff1ffff609, 40存在 0x7fff1ffff60a, 另外5個位元組存的都是0x00.

5. 如果是多位元組的資料又是怎麼儲存的呢? 比如int 佔據了4個位元組, 它是怎麼儲存的 ? 答案也是從低位到高位.

   

[[email protected]-172-16-3-33 zzz]# cat c.c #include <stdio.h> int main() {   int a = -987654;   int * b = &a;   unsigned short i;   unsigned short x = (unsigned short) sizeof(a);   fprintf(stdout, "&a:%p, a:%i, a:%x\n", &a, a, a);   for(i=0; i<x; i++) {     fprintf(stdout, "i:%u, ((char *) b)+i:%p, (unsigned char) (*(((char *) b)+i)):%x\n", i, ((char *) b)+i, (unsigned char) (*(((char *) b)+i)));   }   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c &a:0x7fffe57051cc, a:-987654, a:fff0edfa  //負數是正數的反碼加1, 所以得到的是這個結果. i:0, ((char *) b)+i:0x7fffe57051cc, (unsigned char) (*(((char *) b)+i)):fa i:1, ((char *) b)+i:0x7fffe57051cd, (unsigned char) (*(((char *) b)+i)):ed i:2, ((char *) b)+i:0x7fffe57051ce, (unsigned char) (*(((char *) b)+i)):f0 i:3, ((char *) b)+i:0x7fffe57051cf, (unsigned char) (*(((char *) b)+i)):ff

注意, 這裡一定要把內容再強制轉換成unsigned char再輸出, 就是這個 (unsigned char) (*(((char *) b)+i)):%x .  否則編譯器會輸出4位元組的16進位制數, 不太好看. 如下 : 

   

#include <stdio.h> int main() {   int a = -987654;   int * b = &a;   unsigned short i;   unsigned short x = (unsigned short) sizeof(a);   fprintf(stdout, "&a:%p, a:%i, a:%x\n", &a, a, a);   for(i=0; i<x; i++) {     fprintf(stdout, "i:%u, ((char *) b)+i:%p, (*(((char *) b)+i)):%x\n", i, ((char *) b)+i, (*(((char *) b)+i)));   }   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c &a:0x7ffff6fada3c, a:-987654, a:fff0edfa i:0, ((char *) b)+i:0x7ffff6fada3c, (*(((char *) b)+i)):fffffffa i:1, ((char *) b)+i:0x7ffff6fada3d, (*(((char *) b)+i)):ffffffed i:2, ((char *) b)+i:0x7ffff6fada3e, (*(((char *) b)+i)):fffffff0 i:3, ((char *) b)+i:0x7ffff6fada3f, (*(((char *) b)+i)):ffffffff


6. 那麼bit-field又是怎麼儲存的呢? 答案當然也是從低位到高位儲存的, 這裡要用到struct來驗證.

   

[[email protected]-172-16-3-33 zzz]# cat c.c #include <stdio.h> int main() {   typedef struct test{     unsigned char f1:1;     unsigned char f2:2;     unsigned char f3:3;     unsigned char f4:2;   } test;   test t1 = {0,1,4,3};   test * pt1 = &t1;   fprintf(stdout, "(unsigned char) (*((unsigned char *) pt1)):%x\n", (unsigned char) (*((unsigned char *) pt1)));   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c (unsigned char) (*((unsigned char *) pt1)):e2

解說 : 

   

test t1 = {0,1,4,3}; 轉成二進位制分別如下 f1(0) : 0 f2(1) : 01 f3(4) : 100 f4(3) : 11

如果是按低位到高位儲存的, 那麼它儲存的應該是 : 11100010 . 這個與0xe2 剛好相符. 這裡同樣用到了強制型別轉換,  (unsigned char *) pt1是把指向struct test的指標轉成了指向unsigned char的指標. (unsigned char) (*((unsigned char *) pt1)) 是把  *((unsigned char *) pt1) 轉成了unsigned char型別.  如果不使用指標, 直接把struct變數轉成unsigned是不能編譯通過的. 錯誤如下 : 

   

[[email protected]-172-16-3-33 zzz]# cat c.c #include <stdio.h> int main() {   typedef struct test{     unsigned char f1:1;     unsigned char f2:2;     unsigned char f3:3;     unsigned char f4:2;   } test;   test t1 = {0,1,4,3};   unsigned char a = (unsigned char) t1;   fprintf(stdout, "a:%x\n", a);   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c ./c.c: In function ‘main’: ./c.c:11: error: aggregate value used where an integer was expected

所以指標在C裡面運用真的太靈活了.
進入正題, 講講 char **a , char *a[] , char a[][], char * a[][] , char ** a[][] , char * a [][][] 看起來很複雜, 其實理解了就不復雜了. 1.  char **a :  表示a是一個指標, 這個指標指向的地址儲存的是 char * 型別的資料.  指標的加減運算在這裡的體現 :  a + 1 表示地址加8位元組 .   char * 也是一個指標,  用(*a)表示 , 指向的地址儲存的是 char 型別的資料。 指標的加減運算在這裡的體現 : (* a) + 1 表示地址加1位元組 .  

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char * a = "hello";   char ** b = &a;   fprintf(stdout, "&b:%p, b:%p, &a:%p, a:%p, *a:%c, a:%s\n", &b, b, &a, a, *a, a);   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b &b:0x7fff5319c1d8, b:0x7fff5319c1e0, &a:0x7fff5319c1e0, a:0x400628, *a:h, a:hello


圖示 :  char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on in memory - 德哥@Digoal - The Heart,The World.    char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on in memory - 德哥@Digoal - The Heart,The World.  
2.  char *a[] 表示 a是陣列, 陣列中的元素是指標, 指向char型別. (數組裡面所有的元素是連續的記憶體存放的). 需要特別注意 :  陣列名在C裡面做了特殊處理 , 陣列名用陣列所佔用記憶體區域的第一個位元組的記憶體地址替代了。並且陣列名a也表示指標. 如陣列佔用的記憶體區域是0x7fff5da3f550到0x7fff5da3f5a0, 那麼a就被替換成 0x7fff5da3f550. 所以a 並不表示a地址儲存的內容, 而是a地址本身(這個從 a = &a 就能夠體現出來). 這個一定要理解, 否則會無法進行下去.  a+1 表示a的第二個元素的記憶體地址, 所以是加8位元組.( 因為a的元素是char 指標, 所需要的空間為8位元組(64位記憶體地址). ) *(a+1) 則表示a這個陣列的第二個元素的內容 (是個char 型別的指標. 本例表示為world字串的地址). *(*(a+1)) 則表示 a這個陣列的第二個元素的內容(char指標)所指向的內容( w字元). char * a[10] 表示限定這個陣列最多可存放10個元素(char指標), 也就是說這個陣列佔用10*8 = 80位元組. 如果儲存超出陣列的限額編譯警告如下 : 

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char *a[1] = {"abc","def"};   fprintf(stdout, "a[1]:%s\n", a[1]);   return 0; } 結果 [[email protected]-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b cc1: warnings being treated as errors ./b.c: In function ‘main’: ./b.c:4: warning: excess elements in array initializer  // 超出陣列長度. 因為賦值時給了2個元素, 而限定只有1個元素. ./b.c:4: warning: (near initialization for ‘a’)


例子 :  char * a, char ** a, char * a[], char a[][], char * a[][], char ** a[][], char * a [][][], and so on in memory - 德哥@Digoal - The Heart,The World.  

   

[[email protected]-172-16-3-33 zzz]# cat b.c #include <stdio.h> int main() {   char *a[10] = {"hello",