C語言結(jié)構(gòu)體賦值分析

C++相比C語言的-大便利是類和結(jié)構(gòu)體可以直接用等號賦值。C++為類和結(jié)構(gòu)體提供了可自定義的賦值操作符opeartor =,甚至編譯器會自動(dòng)生成默認(rèn)的賦值操作符。如下所示:

struct A {
    A(int a = 0) : a_(a)
    {
    }

    int a_;
}

void test()
{
    A a(1);
    A b = a;
    A c;
    c = a;
}

雖然知道的人不多,C語言其實(shí)也支持結(jié)構(gòu)體的賦值,如下所示:

struct A {
    int a;
};

void assign_a(struct A *a, struct A *b)
{
    *a = *b;
}

C語言的賦值有一個(gè)限制,不支持?jǐn)?shù)組的賦值。C++也有這個(gè)限制,所以C++推薦使用STL的vector來代替數(shù)組。

C語言的賦值跟C++不同之處在于C語言的賦值操作符不支持用戶自定義,只能由編譯器生成。
先看一段示例代碼:

#define FIXED_LEN 4
struct A {
    int a;
    char b[FIXED_LEN];
    int *p;
    int append_len;
    char appends[];
};

const int ARRAY_SIZE = 10;

void print_sizeof_a(struct A *a)
{
    printf("sizeof A:%lu\n", sizeof(*a));
    printf("sizeof member:a=%lu,b=%lu,p=%lu,append_len=%lu\n", sizeof(a->a), sizeof(a->b), sizeof(a->p),
                    sizeof(a->append_len)/*, sizeof(a->appends)*/);
}

void print_a(struct A *a)
{
    int i;
    int append_print_len = a->append_len > ARRAY_SIZE ? a->append_len : ARRAY_SIZE;
    printf("a=%d,b=[%d,%d,%d,%d],p=%p;append(%d)=", a->a, a->b[0], a->b[1], a->b[2], a->b[3], a->p, a->append_len);
    for (i = 0; i < append_print_len; ++i) {
        printf("%x ", a->appends[i]);
    }
    printf("\n");
}

void assign_a(struct A *a, struct A *b)
{
    *a = *b;
}

int test(void)
{
    const unsigned long size = sizeof(struct A) + ARRAY_SIZE * sizeof(char);
    int x = 100;
    struct A *a = malloc(size);
    a->a = 1;
    a->b[0] = 0;
    a->b[1] = 2;
    a->b[2] = 3;
    a->b[3] = 4;
    a->p    =  &x;
    a->append_len = ARRAY_SIZE;
    memset(a->appends, 0xa, ARRAY_SIZE * sizeof(char));

    struct A *b = malloc(size);
    memset(b->appends, 0xb, ARRAY_SIZE * sizeof(char));

    assign_a(b, a);

    print_sizeof_a(a);

    printf("a:");
    print_a(a);
    printf("b:");
    print_a(b);

    free(a);
    free(b);
    return 0;
}

用gcc(版本是6.2.0,64位macOS 10.14)編譯,并指定以C89標(biāo)準(zhǔn)編譯-std=c89。
test函數(shù)的輸出為:

sizeof A:24
sizeof member:a=4,b=4,p=8,append_len=4
a:a=1,b=[0,2,3,4],p=0x7ffeec85faf4;append(4)=a a a a a a a a a a
b:a=1,b=[0,2,3,4],p=0x7ffeec85faf4;append(4)=a a a a b b b b b b

從輸出結(jié)果來看,有兩個(gè)地方要注意:

  • 賦值是淺拷貝。a->pb->p指向同一個(gè)地址。
  • 不支持柔性數(shù)組(0長度數(shù)組)a->appendsb->appends并不完全相等,只拷貝了前4個(gè)字節(jié)。這實(shí)際上是編譯器生成的賦值操作符的副產(chǎn)品,并不是編譯器有意為之。

何出此言?我們先看看assign_a函數(shù)的反匯編:

(lldb) dis -n assign_a
struct_assign`assign_a:
<+0>:  pushq  %rbp              ;  將調(diào)用函數(shù)的rbp壓棧,保存調(diào)用者的rbp,函數(shù)返回時(shí)再彈出恢復(fù)
<+1>:  movq   %rsp, %rbp        ; 將rbp設(shè)置為rsp,rsp的作用見后面的反匯編分析
<+4>:  movq   %rdi, -0x8(%rbp)  ; 將第一個(gè)參數(shù)a保存到棧上(rbp - 8)
<+8>:  movq   %rsi, -0x10(%rbp) ; 將第二個(gè)參數(shù)b保存在棧上(rbp - 16)
<+12>: movq   -0x8(%rbp), %rax  ; 將第一個(gè)參數(shù)a賦值給寄存器rax
<+16>: movq   -0x10(%rbp), %rdx ; 將第二個(gè)參數(shù)b賦值給寄存器rdx
<+20>: movq   (%rdx), %rcx      ; 第二個(gè)參數(shù)b,取指針指向的結(jié)構(gòu)體A的開始64位(對應(yīng)成員變量a和b)到寄存器rcx中
<+23>: movq   %rcx, (%rax)      ; 將rcx賦值給a指向的結(jié)構(gòu)體A的開始64位
<+26>: movq   0x8(%rdx), %rcx   ; 取b指向的結(jié)構(gòu)體A的第二個(gè)64位(對應(yīng)成員謎題p)到寄存器rcx
<+30>: movq   %rcx, 0x8(%rax)   ; 將rcx賦值給a指向的結(jié)構(gòu)體的第二個(gè)64位
<+34>: movq   0x10(%rdx), %rdx  ; 取b指向的結(jié)構(gòu)體A的第三個(gè)64位(對應(yīng)成員變量append_len和appends的前4個(gè)字節(jié))到寄存器rdx
<+38>: movq   %rdx, 0x10(%rax)  ; 將rdx賦值給a指向的結(jié)構(gòu)體的第三個(gè)64位
<+42>: nop                      ; 空指令
<+43>: popq   %rbp              ; 彈出rbp,恢復(fù)調(diào)用者的rbp
<+44>: retq                     ; 函數(shù)返回

從上面分析可知,賦值操作一共拷貝了24個(gè)字節(jié),也就是sizeof struct A的大小,編譯器把最后4個(gè)字節(jié)看作是paddings,而不是appends的前4個(gè)字節(jié)。在編譯器看來,appends只是不占空間的符號,所以sizeof struct A不包含appends的大小。實(shí)際上sizeof a->appends會報(bào)編譯錯(cuò)誤,因?yàn)榫幾g時(shí)刻并不能知道柔性數(shù)組的長度。

如果將FIXED_LEN變大,編譯器生成的賦值操作符也會隨之變化。例如,將其改為128,賦值操作符不再用movq指令,而改用memcpy。其原型為:

void *memcpy(void *restrict dst, const void *restrict src, size_t n);

assign_a函數(shù)反匯編變?yōu)椋?/p>

(lldb) dis -n assign_a
struct_assign`assign_a:
<+0>:  pushq  %rbp
 <+1>:  movq   %rsp, %rbp
<+4>:  subq   $0x10, %rsp               ; rsp預(yù)留本函數(shù)用來保存臨時(shí)變量的空間,也就是下一級函數(shù)的rbp
<+8>:  movq   %rdi, -0x8(%rbp)
<+12>: movq   %rsi, -0x10(%rbp)
<+16>: movq   -0x8(%rbp), %rdx
<+20>: movq   -0x10(%rbp), %rax
<+24>: movq   %rdx, %rcx
<+27>: movl   $0x98, %edx               ; memcpy第三個(gè)參數(shù)n(通過寄存器edx傳遞)
<+32>: movq   %rax, %rsi                ; memcpy第二個(gè)參數(shù)src(通過寄存器rsi傳遞)
<+35>: movq   %rcx, %rdi                ; memcpy第一個(gè)參數(shù)dst(通過寄存器rdi傳遞)
<+38>: callq  0x100000de6               ; symbol stub for: memcpy
<+43>: nop
<+44>: leave
<+45>: retq

總結(jié)

結(jié)構(gòu)體賦值的出處:

  • 最早可追溯到K&R經(jīng)典
  • gcc實(shí)現(xiàn)的C89已經(jīng)支持
  • C99規(guī)定結(jié)構(gòu)體賦值不包含柔性數(shù)組

賦值適用場景:

  • 左值和右值結(jié)構(gòu)體類型相同;
  • 無指針成員變量的結(jié)構(gòu)體;
  • 帶指針成員并且指針地址可以共享的結(jié)構(gòu)體。因?yàn)橘x值操作是淺拷貝,指針成員需要結(jié)合使用場景,看是用淺拷貝還是深拷貝。

賦值不適用場景(用memcopy):

  • 數(shù)組拷貝;
  • 帶柔性數(shù)組成員的結(jié)構(gòu)體;
  • 帶指針成員并且指針地址不能共享的結(jié)構(gòu)體?

附錄

stackoverflow關(guān)于賦值與memcopy的比較
演示代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • C語言兼有高級語言和低級語言的特點(diǎn) 廣泛應(yīng)用于操作系統(tǒng)和應(yīng)用軟件的編寫以及單片機(jī)和嵌入式系統(tǒng)的開發(fā) C語言的產(chǎn)生 ...
    果啤閱讀 3,053評論 0 43
  • 1.C和C++的區(qū)別?C++的特性?面向?qū)ο缶幊痰暮锰帲?答:c++在c的基礎(chǔ)上增添類,C是一個(gè)結(jié)構(gòu)化語言,它的重...
    杰倫哎呦哎呦閱讀 10,024評論 0 45
  • 人們都說眼睛是心靈的窗口,那么我想雙手就是心靈的門了吧。 手,對于我...
    不會說情話的傻瓜閱讀 206評論 0 1
  • 曾經(jīng)無慮亦無憂, 往事覓難求, 提籃撿柴何處, 河邊及畈頭。 思多載, 望蘄州, 志方酬。 年逾退官, 坦蕩襟懷,...
    黃曉紅閱讀 167評論 1 2
  • 1. 辭職了很久,一直想把自己內(nèi)心想法寫下來,但是一直沒心情寫,或者沒找清思路怎么寫。 遙想當(dāng)年剛畢業(yè)那陣,自己是...
    飛天攬?jiān)?016閱讀 1,369評論 0 4

友情鏈接更多精彩內(nèi)容