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->p和b->p指向同一個(gè)地址。 -
不支持柔性數(shù)組(0長度數(shù)組)。
a->appends和b->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)體?