日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
C語言結(jié)構(gòu)體里的成員數(shù)組和指針

單看這文章的標(biāo)題,你可能會覺得好像沒什么意思。你先別下這個結(jié)論,相信這篇文章會對你理解C語言有幫助。這篇文章產(chǎn)生的背景是在微博上,看到@Laruence同學(xué)出了一個關(guān)于C語言的題,微博鏈接。微博截圖如下。我覺得好多人對這段代碼的理解還不夠深入,所以寫下了這篇文章。

站在用戶的角度思考問題,與客戶深入溝通,找到益陽網(wǎng)站設(shè)計與益陽網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗(yàn)好的作品,建站類型包括:網(wǎng)站設(shè)計制作、成都網(wǎng)站設(shè)計、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名注冊、網(wǎng)頁空間、企業(yè)郵箱。業(yè)務(wù)覆蓋益陽地區(qū)。

為了方便你把代碼copy過去編譯和調(diào)試,我把代碼列在下面:

 
 
  1. #include  
  2. struct str{ 
  3.     int len; 
  4.     char s[0]; 
  5. }; 
  6.   
  7. struct foo { 
  8.     struct str *a; 
  9. }; 
  10.   
  11. int main(int argc, char** argv) { 
  12.     struct foo f={0}; 
  13.     if (f.a->s) { 
  14.         printf( f.a->s); 
  15.     } 
  16.     return 0; 
  17. }

你編譯一下上面的代碼,在VC++和GCC下都會在14行的printf處crash掉你的程序。@Laruence 說這個是個經(jīng)典的坑,我覺得這怎么會是經(jīng)典的坑呢?上面這代碼,你一定會問,為什么if語句判斷的不是f.a?而是f.a里面的數(shù)組?寫這樣代碼的人腦子里在想什么?還是用這樣的代碼來玩票?不管怎么樣,我個人覺得這主要還是對C語言理解不深,如果這算坑的話,那么全都是坑。

接下來,你調(diào)試一下,或是你把14行的printf語句改成:

 
 
  1. printf("%x\n", f.a->s);

你會看到程序不crash了。程序輸出:4。 這下你知道了,訪問0×4的內(nèi)存地址,不crash才怪。于是,你一定會有如下的問題:

1)為什么不是 13行if語句出錯?f.a被初始化為空了嘛,用空指針訪問成員變量為什么不crash?

2)為什么會訪問到了0×4的地址?靠,4是怎么出來的?

3)代碼中的第4行,char s[0] 是個什么東西?零長度的數(shù)組?為什么要這樣玩?

讓我們從基礎(chǔ)開始一點(diǎn)一點(diǎn)地來解釋C語言中這些詭異的問題。

結(jié)構(gòu)體中的成員

首先,我們需要知道——所謂變量,其實(shí)是內(nèi)存地址的一個抽像名字罷了。在靜態(tài)編譯的程序中,所有的變量名都會在編譯時被轉(zhuǎn)成內(nèi)存地址。機(jī)器是不知道我們?nèi)〉拿值?,只知道地址?/p>

所以有了——棧內(nèi)存區(qū),堆內(nèi)存區(qū),靜態(tài)內(nèi)存區(qū),常量內(nèi)存區(qū),我們代碼中的所有變量都會被編譯器預(yù)先放到這些內(nèi)存區(qū)中。

有了上面這個基礎(chǔ),我們來看一下結(jié)構(gòu)體中的成員的地址是什么?我們先簡單化一下代碼:

 
 
  1. struct test{ 
  2.     int i; 
  3.     char *p; 
  4. };

上面代碼中,test結(jié)構(gòu)中i和p指針,在C的編譯器中保存的是相對地址——也就是說,他們的地址是相對于struct test的實(shí)例的。如果我們有這樣的代碼:

 
 
  1. struct test t;

我們用gdb跟進(jìn)去,對于實(shí)例t,我們可以看到:

 
 
  1. # t實(shí)例中的p就是一個野指針 
  2. (gdb) p t 
  3. $1 = {i = 0, c = 0 '\000', d = 0 '\000', p = 0x4003e0 "1\355I\211\..."} 
  4.   
  5. # 輸出t的地址 
  6. (gdb) p &t 
  7. $2 = (struct test *) 0x7fffffffe5f0 
  8.   
  9. #輸出(t.i)的地址 
  10. (gdb) p &(t.i) 
  11. $3 = (char **) 0x7fffffffe5f0 
  12.   
  13. #輸出(t.p)的地址 
  14. (gdb) p &(t.p) 
  15. $4 = (char **) 0x7fffffffe5f4

我們可以看到,t.i的地址和t的地址是一樣的,t.p的址址相對于t的地址多了個4。說白了,t.i 其實(shí)就是(&t + 0×0),t.p 的其實(shí)就是 (&t + 0×4)。0×0和0×4這個偏移地址就是成員i和p在編譯時就被編譯器給hard code了的地址。于是,你就知道,不管結(jié)構(gòu)體的實(shí)例是什么——訪問其成員其實(shí)就是加成員的偏移量。

下面我們來做個實(shí)驗(yàn):

 
 
  1. struct test{ 
  2.     int i; 
  3.     short c; 
  4.     char *p; 
  5. }; 
  6.   
  7. int main(){ 
  8.     struct test *pt=NULL; 
  9.     return 0; 
  10. }

編譯后,我們用gdb調(diào)試一下,當(dāng)初始化pt后,我們看看如下的調(diào)試:(我們可以看到就算是pt為NULL,訪問其中的成員時,其實(shí)就是在訪問相對于pt的內(nèi)址)

 
 
  1. (gdb) p pt 
  2. $1 = (struct test *) 0x0 
  3. (gdb) p pt->i 
  4. Cannot access memory at address 0x0 
  5. (gdb) p pt->c 
  6. Cannot access memory at address 0x4 
  7. (gdb) p pt->p 
  8. Cannot access memory at address 0x8

注意:上面的pt->p的偏移之所以是0×8而不是0×6,是因?yàn)閮?nèi)存對齊了(我在64位系統(tǒng)上)。關(guān)于內(nèi)存對齊,可參看《深入理解C語言》一文。

好了,現(xiàn)在你知道為什么原題中會訪問到了0×4的地址了吧,因?yàn)槭窍鄬Φ刂贰?/p>

相對地址有很好多處,其可以玩出一些有意思的編程技巧,比如把C搞出面向?qū)ο笫降母杏X來,你可以參看我正好11年前的文章《用C寫面向?qū)ο竦某绦颉罚ㄓ弥羔橆愋蛷?qiáng)轉(zhuǎn)的危險玩法——相對于C++來說,C++編譯器幫你管了繼承和虛函數(shù)表,語義也清楚了很多)

指針和數(shù)組的差別

有了上面的基礎(chǔ)后,你把源代碼中的struct str結(jié)構(gòu)體中的char s[0];改成char *s;試試看,你會發(fā)現(xiàn),在13行if條件的時候,程序因?yàn)镃annot access memory就直接掛掉了。為什么聲明成char s[0],程序會在14行掛掉,而聲明成char *s,程序會在13行掛掉呢?那么char *s 和 char s[0]有什么差別呢?

在說明這個事之前,有必要看一下匯編代碼,用GDB查看后發(fā)現(xiàn):

  • 對于char s[0]來說,匯編代碼用了lea指令,lea   0×04(%rax),   %rdx
  • 對于char*s來說,匯編代碼用了mov指令,mov 0×04(%rax),   %rdx

lea全稱load effective address,是把地址放進(jìn)去,而mov則是把地址里的內(nèi)容放進(jìn)去。所以,就crash了。

從這里,我們可以看到,訪問成員數(shù)組名其實(shí)得到的是數(shù)組的相對地址,而訪問成員指針其實(shí)是相對地址里的內(nèi)容(這和訪問其它非指針或數(shù)組的變量是一樣的)

換句話說,對于數(shù)組 char s[10]來說,數(shù)組名 s 和 &s 都是一樣的(不信你可以自己寫個程序試試)。在我們這個例子中,也就是說,都表示了偏移后的地址。這樣,如果我們訪問 指針的地址(或是成員變量的地址),那么也就不會讓程序掛掉了。

正如下面的代碼,可以運(yùn)行一點(diǎn)也不會crash掉(你匯編一下你會看到用的都是lea指令):

 
 
  1. struct test{ 
  2.     int i; 
  3.     short c; 
  4.     char *p; 
  5.     char s[10]; 
  6. }; 
  7.   
  8. int main(){ 
  9.     struct test *pt=NULL; 
  10.     printf("&s = %x\n", pt->s); //等價于 printf("%x\n", &(pt->s) ); 
  11.     printf("&i = %x\n", &pt->i); //因?yàn)椴僮鞣麅?yōu)先級,我沒有寫成&(pt->i) 
  12.     printf("&c = %x\n", &pt->c); 
  13.     printf("&p = %x\n", &pt->p); 
  14.     return 0; 
  15. }

看到這里,你覺得這能算坑嗎?不要出什么事都去怪語言,想想是不是問題出在自己身上。

#p#

關(guān)于零長度的數(shù)組

首先,我們要知道,0長度的數(shù)組在ISO C和C++的規(guī)格說明書中是不允許的。這也就是為什么在VC++2012下編譯你會得到一個警告:“arning C4200: 使用了非標(biāo)準(zhǔn)擴(kuò)展 : 結(jié)構(gòu)/聯(lián)合中的零大小數(shù)組”。

那么為什么gcc可以通過而連一個警告都沒有?那是因?yàn)間cc 為了預(yù)先支持C99的這種玩法,所以,讓“零長度數(shù)組”這種玩法合法了。關(guān)于GCC對于這個事的文檔在這里:“Arrays of Length Zero”,文檔中給了一個例子(我改了一下,改成可以運(yùn)行的了):

 
 
  1. #include  
  2. #include  
  3.   
  4. struct line { 
  5.    int length; 
  6.    char contents[0]; // C99的玩法是:char contents[]; 沒有指定數(shù)組長度 
  7. }; 
  8.   
  9. int main(){ 
  10.     int this_length=10; 
  11.     struct line *thisline = (struct line *) 
  12.                      malloc (sizeof (struct line) + this_length); 
  13.     thisline->length = this_length; 
  14.     memset(thisline->contents, 'a', this_length); 
  15.     return 0; 
  16. }

看到這里,你覺得這能算坑嗎?不要出什么事都去怪語言,想想是不是問題出在自己身上。

關(guān)于零長度的數(shù)組

首先,我們要知道,0長度的數(shù)組在ISO C和C++的規(guī)格說明書中是不允許的。這也就是為什么在VC++2012下編譯你會得到一個警告:“arning C4200: 使用了非標(biāo)準(zhǔn)擴(kuò)展 : 結(jié)構(gòu)/聯(lián)合中的零大小數(shù)組”。

那么為什么gcc可以通過而連一個警告都沒有?那是因?yàn)間cc 為了預(yù)先支持C99的這種玩法,所以,讓“零長度數(shù)組”這種玩法合法了。關(guān)于GCC對于這個事的文檔在這里:“Arrays of Length Zero”,文檔中給了一個例子(我改了一下,改成可以運(yùn)行的了):

 
 
  1. #include  
  2. #include  
  3.   
  4. struct line { 
  5.    int length; 
  6.    char contents[0]; // C99的玩法是:char contents[]; 沒有指定數(shù)組長度 
  7. }; 
  8.   
  9. int main(){ 
  10.     int this_length=10; 
  11.     struct line *thisline = (struct line *) 
  12.                      malloc (sizeof (struct line) + this_length); 
  13.     thisline->length = this_length; 
  14.     memset(thisline->contents, 'a', this_length); 
  15.     return 0; 
  16. }

上面這段代碼的意思是:我想分配一個不定長的數(shù)組,于是我有一個結(jié)構(gòu)體,其中有兩個成員,一個是length,代表數(shù)組的長度,一個是contents,代碼數(shù)組的內(nèi)容。后面代碼里的 this_length(長度是10)代表是我想分配的數(shù)據(jù)的長度。(這看上去是不是像一個C++的類?)這種玩法英文叫:Flexible Array,中文翻譯叫:柔性數(shù)組。

我們來用gdb看一下:

 
 
  1. (gdb) p thisline 
  2. $1 = (struct line *) 0x601010 
  3.   
  4. (gdb) p *thisline 
  5. $2 = {length = 10, contents = 0x601010 "\n"} 
  6.   
  7. (gdb) p thisline->contents 
  8. $3 = 0x601014 "aaaaaaaaaa"

我們可以看到:在輸出*thisline時,我們發(fā)現(xiàn)其中的成員變量contents的地址居然和thisline是一樣的(偏移量為0×0??!!)。但是當(dāng)我們輸出thisline->contents的時候,你又發(fā)現(xiàn)contents的地址是被offset了0×4了的,內(nèi)容也變成了10個‘a(chǎn)’。(我覺得這是一個GDB的bug,VC++的調(diào)試器就能很好的顯示)

我們繼續(xù),如果你sizeof(char[0])或是 sizeof(int[0]) 之類的零長度數(shù)組,你會發(fā)現(xiàn)sizeof返回了0,這就是說,零長度的數(shù)組是存在于結(jié)構(gòu)體內(nèi)的,但是不占結(jié)構(gòu)體的size。你可以簡單的理解為一個沒有內(nèi)容的占位標(biāo)識,直到我們給結(jié)構(gòu)體分配了內(nèi)存,這個占位標(biāo)識才變成了一個有長度的數(shù)組。

看到這里,你會說,為什么要這樣搞啊,把contents聲明成一個指針,然后為它再分配一下內(nèi)存不行么?就像下面一樣。

 
 
  1. struct line { 
  2.    int length; 
  3.    char *contents; 
  4. }; 
  5.   
  6. int main(){ 
  7.     int this_length=10; 
  8.     struct line *thisline = (struct line *)malloc (sizeof (struct line)); 
  9.     thisline->contents = (char*) malloc( sizeof(char) * this_length ); 
  10.     thisline->length = this_length; 
  11.     memset(thisline->contents, 'a', this_length); 
  12.     return 0; 
  13. }

這不一樣清楚嗎?而且也沒什么怪異難懂的東西。是的,這也是普遍的編程方式,代碼是很清晰,也讓人很容易理解。即然這樣,那為什么要搞一個零長度的數(shù)組?有毛意義?!

這個事情出來的原因是——我們想給一個結(jié)構(gòu)體內(nèi)的數(shù)據(jù)分配一個連續(xù)的內(nèi)存!這樣做的意義有兩個好處:

第一個意義是,方便內(nèi)存釋放。如果我們的代碼是在一個給別人用的函數(shù)中,你在里面做了二次內(nèi)存分配,并把整個結(jié)構(gòu)體返回給用戶。用戶調(diào)用free可以釋放結(jié)構(gòu)體,但是用戶并不知道這個結(jié)構(gòu)體內(nèi)的成員也需要free,所以你不能指望用戶來發(fā)現(xiàn)這個事。所以,如果我們把結(jié)構(gòu)體的內(nèi)存以及其成員要的內(nèi)存一次性分配好了,并返回給用戶一個結(jié)構(gòu)體指針,用戶做一次free就可以把所有的內(nèi)存也給釋放掉。(讀到這里,你一定會覺得C++的封閉中的析構(gòu)函數(shù)會讓這事容易和干凈很多)

第二個原因是,這樣有利于訪問速度。連續(xù)的內(nèi)存有益于提高訪問速度,也有益于減少內(nèi)存碎片。(其實(shí),我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來尋址)

我們來看看是怎么個連續(xù)的,用gdb的x命令來查看:(我們知道,用struct line {}中的那個char contents[]不占用結(jié)構(gòu)體的內(nèi)存,所以,struct line就只有一個int成員,4個字節(jié),而我們還要為contents[]分配10個字節(jié)長度,所以,一共是14個字節(jié))

 
 
  1. (gdb) x /14b thisline 
  2. 0x601010:       10      0       0       0       97      97      97      97 
  3. 0x601018:       97      97      97      97      97      97

從上面的內(nèi)存布局我們可以看到,前4個字節(jié)是 int length,后10個字節(jié)就是char contents[]。

如果用指針的話,會變成這個樣子:

 
 
  1. (gdb) x /16b thisline 
  2. 0x601010:       1       0       0       0       0       0       0       0 
  3. 0x601018:       32      16      96      0       0       0       0       0 
  4. (gdb) x /10b this->contents 
  5. 0x601020:       97      97      97      97      97      97      97      97 
  6. 0x601028:       97      97

上面一共輸出了四行內(nèi)存,其中,

  • 第一行前四個字節(jié)是 int length,第一行的后四個字節(jié)是對齊。
  • 第二行是char* contents,64位系統(tǒng)指針8個長度,他的值是0×20 0×10 0×60 也就是0×601020。
  • 第三行和第四行是char* contents指向的內(nèi)容。

從這里,我們看到,其中的差別——數(shù)組的原地就是內(nèi)容,而指針的那里保存的是內(nèi)容的地址。

后記

好了,我的文章到這里就結(jié)束了。但是,請允許我再嘮叨兩句。

1)看過這篇文章,你覺得C復(fù)雜嗎?我覺得并不簡單。某些地方的復(fù)雜程度不亞于C++。

2)那些學(xué)不好C++的人一定是連C都學(xué)不好的人。連C都沒學(xué)好,你們根本沒有資格鄙視C++。

3)當(dāng)你們在說有坑的時候,你得問一下自己,是真有坑還是自己的學(xué)習(xí)能力上出了問題。

如果你覺得你的C語言還不錯,歡迎你看看《C語言的謎題》還有《誰說C語言很簡單?》還有《語言的歧義》以及《深入理解C語言》一文。


文章名稱:C語言結(jié)構(gòu)體里的成員數(shù)組和指針
當(dāng)前URL:http://m.5511xx.com/article/dhjpcdj.html