美文网首页
C语言结构体赋值分析

C语言结构体赋值分析

作者: typesafe | 来源:发表于2019-02-19 21:52 被阅读0次

    C++相比C语言的-大便利是类和结构体可以直接用等号赋值。C++为类和结构体提供了可自定义的赋值操作符opeartor =,甚至编译器会自动生成默认的赋值操作符。如下所示:

    struct A {
        A(int a = 0) : a_(a)
        {
        }
    
        int a_;
    }
    
    void test()
    {
        A a(1);
        A b = a;
        A c;
        c = a;
    }
    

    虽然知道的人不多,C语言其实也支持结构体的赋值,如下所示:

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

    C语言的赋值有一个限制,不支持数组的赋值。C++也有这个限制,所以C++推荐使用STL的vector来代替数组。

    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标准编译-std=c89
    test函数的输出为:

    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
    

    从输出结果来看,有两个地方要注意:

    • 赋值是浅拷贝a->pb->p指向同一个地址。
    • 不支持柔性数组(0长度数组)a->appendsb->appends并不完全相等,只拷贝了前4个字节。这实际上是编译器生成的赋值操作符的副产品,并不是编译器有意为之。

    何出此言?我们先看看assign_a函数的反汇编:

    (lldb) dis -n assign_a
    struct_assign`assign_a:
    <+0>:  pushq  %rbp              ;  将调用函数的rbp压栈,保存调用者的rbp,函数返回时再弹出恢复
    <+1>:  movq   %rsp, %rbp        ; 将rbp设置为rsp,rsp的作用见后面的反汇编分析
    <+4>:  movq   %rdi, -0x8(%rbp)  ; 将第一个参数a保存到栈上(rbp - 8)
    <+8>:  movq   %rsi, -0x10(%rbp) ; 将第二个参数b保存在栈上(rbp - 16)
    <+12>: movq   -0x8(%rbp), %rax  ; 将第一个参数a赋值给寄存器rax
    <+16>: movq   -0x10(%rbp), %rdx ; 将第二个参数b赋值给寄存器rdx
    <+20>: movq   (%rdx), %rcx      ; 第二个参数b,取指针指向的结构体A的开始64位(对应成员变量a和b)到寄存器rcx中
    <+23>: movq   %rcx, (%rax)      ; 将rcx赋值给a指向的结构体A的开始64位
    <+26>: movq   0x8(%rdx), %rcx   ; 取b指向的结构体A的第二个64位(对应成员谜题p)到寄存器rcx
    <+30>: movq   %rcx, 0x8(%rax)   ; 将rcx赋值给a指向的结构体的第二个64位
    <+34>: movq   0x10(%rdx), %rdx  ; 取b指向的结构体A的第三个64位(对应成员变量append_len和appends的前4个字节)到寄存器rdx
    <+38>: movq   %rdx, 0x10(%rax)  ; 将rdx赋值给a指向的结构体的第三个64位
    <+42>: nop                      ; 空指令
    <+43>: popq   %rbp              ; 弹出rbp,恢复调用者的rbp
    <+44>: retq                     ; 函数返回
    

    从上面分析可知,赋值操作一共拷贝了24个字节,也就是sizeof struct A的大小,编译器把最后4个字节看作是paddings,而不是appends的前4个字节。在编译器看来,appends只是不占空间的符号,所以sizeof struct A不包含appends的大小。实际上sizeof a->appends会报编译错误,因为编译时刻并不能知道柔性数组的长度。

    如果将FIXED_LEN变大,编译器生成的赋值操作符也会随之变化。例如,将其改为128,赋值操作符不再用movq指令,而改用memcpy。其原型为:

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

    assign_a函数反汇编变为:

    (lldb) dis -n assign_a
    struct_assign`assign_a:
    <+0>:  pushq  %rbp
     <+1>:  movq   %rsp, %rbp
    <+4>:  subq   $0x10, %rsp               ; rsp预留本函数用来保存临时变量的空间,也就是下一级函数的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第三个参数n(通过寄存器edx传递)
    <+32>: movq   %rax, %rsi                ; memcpy第二个参数src(通过寄存器rsi传递)
    <+35>: movq   %rcx, %rdi                ; memcpy第一个参数dst(通过寄存器rdi传递)
    <+38>: callq  0x100000de6               ; symbol stub for: memcpy
    <+43>: nop
    <+44>: leave
    <+45>: retq
    

    总结

    结构体赋值的出处:

    • 最早可追溯到K&R经典
    • gcc实现的C89已经支持
    • C99规定结构体赋值不包含柔性数组

    赋值适用场景:

    • 左值和右值结构体类型相同;
    • 无指针成员变量的结构体;
    • 带指针成员并且指针地址可以共享的结构体。因为赋值操作是浅拷贝,指针成员需要结合使用场景,看是用浅拷贝还是深拷贝。

    赋值不适用场景(用memcopy):

    • 数组拷贝;
    • 带柔性数组成员的结构体;
    • 带指针成员并且指针地址不能共享的结构体。

    附录

    stackoverflow关于赋值与memcopy的比较
    演示代码

    相关文章

      网友评论

          本文标题:C语言结构体赋值分析

          本文链接:https://www.haomeiwen.com/subject/bskneqtx.html