9

C语言中结构体可以直接赋值,而不用memcpy

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%BB%93%E6%9E%84%E4%BD%93%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E8%B5%8B%E5%80%BC-%E8%80%8C%E4%B8%8D%E7%94%A8memcpy/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在C语言程序开发中,结构体的使用相当频繁。上周我在我的C语言学习圈子里简要介绍了一个小窍门,粗略来说就是使用C语言结构体的赋值语法,代替memcpy()语句,以精简代码,大致如下图所示:

5a80de8cf959d198e8871da3dd416d0d.png

有读者看到后,认为C语言结构体的赋值并不等价于 memcpy,也有朋友评论说 b=a 是“浅拷贝”,还有读者提到结构体赋值效率没有memcpy高,那么 b = a 语句被执行后,究竟发生了什么呢?

编写测试C语言代码

得到答案最简单直接的方法就是实验,因此这里给出一段较为完整的C语言代码,用于测试结构体的赋值语句,如下所示。为了讨论主题,下面C语言代码比较精简:

#include <string.h>

struct s{
    char c;
    long l;
};

int main()
{
    struct s a = {3, 5};
    struct s b, c;

    b = a;
    memcpy(&c, &a, sizeof(struct s));

    return 0;
}
b97961078670535683c6f42b493e1041.png

上面这段C语言代码很简单,main() 函数定义了 3 个结构体变量 a, b, c,其中 a 被初始化为 {3, 5},并通过赋值语句拷贝给 b,memcpy() 拷贝给 c。考察 a,b,c 占用的内存里的值,从最终“拷贝效果”上分析赋值语句和memcpy()的异同。

查看内存值

查看上述C语言程序中的变量 a, b, c 的值方法很多,最直接的方法就是使用 printf() 函数逐字节打印,不过这样就略显繁琐了,使用 GDB 工具调试C语言程序更简单些。

首先,输入 gcc t.c -g 编译上述C语言代码,得到可执行文件 a.out。接着,就可以使用 gdb 调试了:

# gcc t.c -g
# gdb ./a.out
(gdb)

首先在 main() 函数处下断点,然后输入 run 命令让C语言程序运行起来:

(gdb) b main
Breakpoint 1 at 0x4004f1: file t.c, line 10.
(gdb) r
Breakpoint 1, main () at t.c:10
10      struct s a = {3, 5};

可以发现程序停在第 10 行了,此时变量 a,b, c 还没有被赋值或者 memcpy。我们先看一下结构体 s 的 size,可以直接在 gdb 环境查看:

(gdb) p sizeof(struct s)
$1 = 16

发现 sizeof(struct s) 等于 16,这主要是因为C语言编译器为了提升效率,对结构体 s 的两个成员做了内存对齐处理。所以,虽然 char 型的 c 成员实际上只需 1 个字节内存空间,但是因为成员 l 占用 8 字节内存空间,所以编译器在 c 后面预留了 7 个字节。

读者 @Romi1984 认为 C语言结构体赋值拷贝和 memcpy 拷贝不等价,因为“赋值的话,对齐字节不会拷贝”。他的意思应该是 c 后面预留的 7 个字节不会被拷贝,那是不是如此呢?在执行 b =a; 语句之前,我们先来查看 a,b,c 在内存里的值:

6fcb65afe07e720a82a95d300816de5d.png

能够看出,此时变量 a,b,c 的内存值并不完全相同。输入 next 命令,使C语言程序运行到第 16 行,也即 return 0; 语句处,此时赋值语句以及 memcpy 语句都被执行完毕,再查看 a,b,c 的内存值,得到如下输出:
63da743392dbacbb52422d2934031e77.png

发现变量 a, b, c 的值完全相同,包括结构体 s 的 c 成员后内存对齐的 7 个字节,这说明读者 @Romi1984 说的“对齐字节不会被拷贝”是不准确的,至少就本例而言,C语言结构体 s 的赋值拷贝和 memcpy 拷贝效果上是等价的。

虽然通过 gdb 查看内存值,我们发现C语言结构体的赋值拷贝和 memcpy 拷贝效果是等价的,但是,读者 @quser225816904 认为,这两种方式的效率是不一样的。

那究竟是否如此呢?得到答案最直接的办法就是衡量这两条语句的执行时间。不过由于这一“执行时间”很短,难以计量,我们采取其他方法:输入下面的命令,查看C语言程序的汇编代码。

# objdump -dS a.out
ede185764a64e47d3a9767cd2db71065.png

从C语言程序的汇编代码可以看出,b = a; 和 memcpy() 语句都是 4 条 mov 语句,这说明两种拷贝方式的效率相差无几,所以读者 @quser225816904 的说法也是不准确的。另外,从C语言程序的汇编代码也能更直观的看出 b = a; 和 memcpy() 是等价的。

读者也可以通过多次执行 b = a 和 memcpy 语句,对比两种拷贝方式的效率。

“深拷贝”和“浅拷贝”

前面两位读者分别从执行效果和执行效率两个角度质疑了C语言结构体赋值拷贝和memcpy拷贝的等价性,也有读者认为赋值拷贝只是“浅拷贝”,那么究竟是否如此呢?

首先,先要明白“浅拷贝”和“深拷贝”概念,这两个概念 Java,C++,js 等编程语言程序员应该比较熟悉,在C语言中倒是不怎么常提。细究这两个概念的区别并不是本文的重点,所以这里粗略的对“浅拷贝”和“深拷贝”做如下区分,对于把变量 a 拷贝给 b:

如果拷贝后,b 的内容完全等于 a,并且两个变量在内存中是独立的,则称此次拷贝为“深拷贝”。如果靠背后,只是通过 b 能够访问 a 中的内容,a 的内容改变时,b 的“内容”也随之改变,则称此次拷贝为“浅拷贝”。

这样看来,就本例而言,b = a;显然是一次“深拷贝”,因为 a, b 在内存中彼此独立,并且拷贝后,b 的内容和 a 的内容完全相同。那C语言的结构体赋值拷贝一定是“深拷贝”吗?我们将结构体 s 新增一个指针成员 buf:

struct s{
    char c;
    long l;
    char *buf;
};

对 a 的初始化也做相应修改,相关C语言代码如下,请看:

struct s a = {3, 5, };
a.buf = (char *)malloc(128);
strcpy(a.buf, "hello world");

为了讨论主题,上述C语言代码没有做错误处理。现在 b = a; 还是“深拷贝”吗?读者如果做了实验,应该会发现,b 的 buf 成员本身在内存中的确独立于 a 的 buf 成员,但是它指向的内存却与 a 的 buf 成员指向的内存是同一块,所以这时 b = a; 不再是纯粹的“深拷贝”了。

本节主要讨论了C语言结构体的赋值语法可以用于拷贝,并针对之前读者的几个典型问题做了较为详细的实例探讨。不过,C语言是一门非常灵活的编程语言,可能同样的一条语句,在不同的环境下执行结果是不一样的,这一点本文最后的讨论就是一个实例。应该明白,本文举的例子仅是为了抛砖引玉,展示遇到问题该如何分析的方法,学习C语言,应该乐于做实验尝试才对。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK