C语言结构体赋值分析

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语言结构体赋值分析)