<kbd id="daqct"></kbd>

  • <nav id="daqct"></nav>
    <wbr id="daqct"><pre id="daqct"></pre></wbr>
    <wbr id="daqct"></wbr>
    <form id="daqct"><th id="daqct"></th></form>
    更多課程 選擇中心

    C/C++培訓
    達內IT學院

    400-111-8989

    C語言結構體里的成員數組和指針

    • 發布:C++培訓
    • 來源:資料庫
    • 時間:2017-06-05 20:01

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    structstr{
        intlen;
        chars[0];
    };
     
    structfoo {
        structstr *a;
    };
     
    intmain(intargc,char** argv) {
        structfoo f={0};
        if(f.a->s) {
            printf( f.a->s);
        }
        return0;
    }

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

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

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

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

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

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

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

    讓我們從基礎開始一點一點地來解釋C語言中這些詭異的問題。

    結構體中的成員

    首先,我們需要知道——所謂變量,其實是內存地址的一個抽像名字罷了。在靜態編譯的程序中,所有的變量名都會在編譯時被轉成內存地址。機器是不知道我們取的名字的,只知道地址。

    所以有了——棧內存區,堆內存區,靜態內存區,常量內存區,我們代碼中的所有變量都會被編譯器預先放到這些內存區中。

    有了上面這個基礎,我們來看一下結構體中的成員的地址是什么?我們先簡單化一下代碼:

    1
    2
    3
    4
    structtest{
        inti;
        char*p;
    };

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

    1
    structtest t;

    我們用gdb跟進去,對于實例t,我們可以看到:

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

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

    下面我們來做個實驗:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    structtest{
        inti;
        shortc;
        char*p;
    };
     
    intmain(){
        structtest *pt=NULL;
        return0;
    }

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

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

    注意:上面的pt->p的偏移之所以是0x8而不是0x6,是因為內存對齊了(我在64位系統上)。關于內存對齊,可參看《深入理解C語言》一文。

    好了,現在你知道為什么原題中會訪問到了0x4的地址了吧,因為是相對地址。

    相對地址有很好多處,其可以玩出一些有意思的編程技巧,比如把C搞出面向對象式的感覺來,你可以參看我正好11年前的文章《用C寫面向對像的程序》(用指針類型強轉的危險玩法——相對于C++來說,C++編譯器幫你管了繼承和虛函數表,語義也清楚了很多)

    指針和數組的差別

    有了上面的基礎后,你把源代碼中的struct str結構體中的char s[0];改成char *s;試試看,你會發現,在13行if條件的時候,程序因為Cannot access memory就直接掛掉了。為什么聲明成char s[0],程序會在14行掛掉,而聲明成char *s,程序會在13行掛掉呢?那么char *s 和 char s[0]有什么差別呢?

    在說明這個事之前,有必要看一下匯編代碼,用GDB查看后發現:

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

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

    lea全稱load effective address,是把地址放進去,而mov則是把地址里的內容放進去。所以,就crash了。

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    structtest{
        inti;
        shortc;
        char*p;
        chars[10];
    };
     
    intmain(){
        structtest *pt=NULL;
        printf("&s = %x\n", pt->s);//等價于 printf("%x\n", &(pt->s) );
        printf("&i = %x\n", &pt->i);//因為操作符優先級,我沒有寫成&(pt->i)
        printf("&c = %x\n", &pt->c);
        printf("&p = %x\n", &pt->p);
        return0;
    }

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

    關于零長度的數組

    首先,我們要知道,0長度的數組在ISO C和C++的規格說明書中是不允許的。這也就是為什么在VC++2012下編譯你會得到一個警告:“arning C4200: 使用了非標準擴展 : 結構/聯合中的零大小數組”。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdlib.h>
    #include <string.h>
     
    structline {
       intlength;
       charcontents[0];// C99的玩法是:char contents[]; 沒有指定數組長度
    };
     
    intmain(){
        intthis_length=10;
        structline *thisline = (structline *)
                         malloc(sizeof(structline) + this_length);
        thisline->length = this_length;
        memset(thisline->contents,'a', this_length);
        return0;
    }

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

    我們來用gdb看一下:

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    structline {
       intlength;
       char*contents;
    };
     
    intmain(){
        intthis_length=10;
        structline *thisline = (structline *)malloc(sizeof(structline));
        thisline->contents = (char*)malloc(sizeof(char) * this_length );
        thisline->length = this_length;
        memset(thisline->contents,'a', this_length);
        return0;
    }

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

    這個事情出來的原因是——我們想給一個結構體內的數據分配一個連續的內存!這樣做的意義有兩個好處:

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

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

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

    1
    2
    3
    (gdb) x/14bthisline
    0x601010:       10      0       0       0       97      97      97      97
    0x601018:       97      97      97      97      97      97

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

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

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

    上面一共輸出了四行內存,其中,

    • 第一行前四個字節是 int length,第一行的后四個字節是對齊。

    • 第二行是char* contents,64位系統指針8個長度,他的值是0x20 0x10 0x60 也就是0x601020。

    • 第三行和第四行是char* contents指向的內容。

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

    后記

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

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

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

    3)當你們在說有坑的時候,你得問一下自己,是真有坑還是自己的學習能力上出了問題。

    預約申請免費試聽課

    填寫下面表單即可預約申請免費試聽!怕錢不夠?可就業掙錢后再付學費! 怕學不會?助教全程陪讀,隨時解惑!擔心就業?一地學習,可全國推薦就業!

    上一篇:用C語言寫貪吃蛇游戲圖文教程
    下一篇:C語言從入門到進階的書籍推薦 ,讓你少走彎路的C語言書籍

    超全的C語言標識符知識

    C指針——指針類型轉換

    C指針——指針和結構類型的關系

    C指針——數組和指針的關系

    • 掃碼領取資料

      回復關鍵字:視頻資料

      免費領取 達內課程視頻學習資料

    • 視頻學習QQ群

      添加QQ群:1143617948

      免費領取達內課程視頻學習資料

    Copyright ? 2021 Tedu.cn All Rights Reserved 京ICP備08000853號-56 京公網安備 11010802029508號 達內時代科技集團有限公司 版權所有

    選擇城市和中心
    黑龍江省

    吉林省

    河北省

    湖南省

    貴州省

    云南省

    廣西省

    海南省

    欧美三级片,白洁外传,第四色播日韩AV第一页,啪啪免费观看大全av 百度 好搜 搜狗
    <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>