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->p和b->p指向同一个地址。
不支持柔性数组(0长度数组)。a->appends和b->appends并不完全相等,只拷贝了前4个字节。这实际上是编译器生成的赋值操作符的副产品,并不是编译器有意为之。
何出此言?我们先看看assign_a函数的反汇编:
(lldb) dis -n assign_a
struct_assign`assign_a:
: pushq %rbp ; 将调用函数的rbp压栈,保存调用者的rbp,函数返回时再弹出恢复
: movq %rsp, %rbp ; 将rbp设置为rsp,rsp的作用见后面的反汇编分析
: movq %rdi, -0x8(%rbp) ; 将第一个参数a保存到栈上(rbp - 8)
: movq %rsi, -0x10(%rbp) ; 将第二个参数b保存在栈上(rbp - 16)
: movq -0x8(%rbp), %rax ; 将第一个参数a赋值给寄存器rax
: movq -0x10(%rbp), %rdx ; 将第二个参数b赋值给寄存器rdx
: movq (%rdx), %rcx ; 第二个参数b,取指针指向的结构体A的开始64位(对应成员变量a和b)到寄存器rcx中
: movq %rcx, (%rax) ; 将rcx赋值给a指向的结构体A的开始64位
: movq 0x8(%rdx), %rcx ; 取b指向的结构体A的第二个64位(对应成员谜题p)到寄存器rcx
: movq %rcx, 0x8(%rax) ; 将rcx赋值给a指向的结构体的第二个64位
: movq 0x10(%rdx), %rdx ; 取b指向的结构体A的第三个64位(对应成员变量append_len和appends的前4个字节)到寄存器rdx
: movq %rdx, 0x10(%rax) ; 将rdx赋值给a指向的结构体的第三个64位
: nop ; 空指令
: popq %rbp ; 弹出rbp,恢复调用者的rbp
: 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:
: pushq %rbp
: movq %rsp, %rbp
: subq $0x10, %rsp ; rsp预留本函数用来保存临时变量的空间,也就是下一级函数的rbp
: movq %rdi, -0x8(%rbp)
: movq %rsi, -0x10(%rbp)
: movq -0x8(%rbp), %rdx
: movq -0x10(%rbp), %rax
: movq %rdx, %rcx
: movl $0x98, %edx ; memcpy第三个参数n(通过寄存器edx传递)
: movq %rax, %rsi ; memcpy第二个参数src(通过寄存器rsi传递)
: movq %rcx, %rdi ; memcpy第一个参数dst(通过寄存器rdi传递)
: callq 0x100000de6 ; symbol stub for: memcpy
: nop
: leave
: retq
总结
结构体赋值的出处:
最早可追溯到K&R经典
gcc实现的C89已经支持
C99规定结构体赋值不包含柔性数组
赋值适用场景:
左值和右值结构体类型相同;
无指针成员变量的结构体;
带指针成员并且指针地址可以共享的结构体。因为赋值操作是浅拷贝,指针成员需要结合使用场景,看是用浅拷贝还是深拷贝。
赋值不适用场景(用memcopy):
数组拷贝;
带柔性数组成员的结构体;
带指针成员并且指针地址不能共享的结构体。
附录