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的比较
演示代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • C语言兼有高级语言和低级语言的特点 广泛应用于操作系统和应用软件的编写以及单片机和嵌入式系统的开发 C语言的产生 ...
    果啤阅读 2,858评论 0 43
  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 9,474评论 0 45
  • 人们都说眼睛是心灵的窗口,那么我想双手就是心灵的门了吧。 手,对于我...
    不会说情话的傻瓜阅读 165评论 0 1
  • 曾经无虑亦无忧, 往事觅难求, 提篮捡柴何处, 河边及畈头。 思多载, 望蕲州, 志方酬。 年逾退官, 坦荡襟怀,...
    黄晓红阅读 147评论 1 2
  • 1. 辞职了很久,一直想把自己内心想法写下来,但是一直没心情写,或者没找清思路怎么写。 遥想当年刚毕业那阵,自己是...
    飞天揽月2016阅读 1,103评论 0 4