4

C语言陷阱与技巧第38节,拷贝部分结构体,内容不正确时怎么回事?

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC38%E8%8A%82-%E6%8B%B7%E8%B4%9D%E9%83%A8%E5%88%86%E7%BB%93%E6%9E%84%E4%BD%93-%E5%86%85%E5%AE%B9%E4%B8%8D/
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.

如果读者看了本专栏之前的文章,应该能够发现,C语言的灵活性很大程度上依赖指针语法和结构体语法,不过归根结底,这种灵活性都要归功于C语言可以直接操作内存。

拷贝结构体的部分成员

《》一节介绍了获取结构体成员在内存中偏移地址的小技巧,借助该技巧,我们可以轻易的得到结构体的一段内存,例如:

struct save_student{
    int   index;
    int   offset;

    int   number;
    char  name[32];
    int   gender;
    int   age;
};

这个结构体描述了一个C语言模块存储学生的信息,包括:学号,姓名,性别,年龄。假设为了便于C语言模块管理,该结构体还建立了 index 和 offset 两个不对外公开,仅内部使用的私有成员。

既然 index 和 offset 只是C语言模块内部使用的私有成员,那查询结果其实只需要学号,姓名,性别,年龄 4 条信息就可以了。为了C语言代码的可读性和模块的独立性,下面建立了用于描述查询结果的结构体:

struct query_student{
    int   number;
    char  name[32];
    int   gender;
    int   age;
};

这样一来,查询学生信息,并返回结果的核心C语言代码似乎可以按照下面这样写:

void query(struct query_student *result)
{
    struct save_student stuInfo;
    query_from_disk(&stuInfo);  /** 从磁盘查找信息 */
    ...
    result->number = stuInfo.number;
    result->gender = stuInfo.gender;
    result->age    = stuInfo.age;
    strcpy(result->name, stuInfo.name);
    ...
}

上面这样的C语言代码自然可以将从磁盘查找到的学生信息通过 result 传出,不过似乎“蹩脚”了点,因为结构体 query_student 的各个成员实际上与结构体 save_student 的部分成员是相同的,甚至连顺序都是相同的。

头脑灵活的读者应该想到了,这种情况直接使用 memcpy() 似乎更加简洁,相关C语言代码如下,请看:

void query(struct query_student *result)
{
    struct save_student stuInfo;
    query_from_disk(&stuInfo);  /** 从磁盘查找信息 */
    ...
    memcpy(result, &stuInfo.number, sizeof(struct query_student));
    ...
}

这段C语言代码很简单,结构体 save_student 是从成员 number 开始完全与结构体 query_student 相同的,因此从 stuInfo.number 成员偏移处开始,将内存拷贝给 result,拷贝长度正好是结构体 query_student 的长度。

这样的代码简洁多了,原本结构体 query_student 有多少个成员,就得写多少行赋值语句,现在只需一行 memcpy() 就搞定了,那它是否可以正常工作呢?

为了便于测试,我们编写如下C语言代码测试之:

struct save_student stuInfo = {0, 0, 1, "Jim", 1, 18};
struct query_student result = {};

memcpy(&result, &stuInfo.number, sizeof(struct query_student));
printf("result number: %d, name: %s, gender: %d, age: %d\n",
            result.number, result.name, result.gender, result.age);
a828bab6f21d31d5e3b00fc8e1cefa53.png

编译并执行这段C语言代码,得到如下输出:
# gcc t.c 
# ./a.out 
result number: 1, name: Jim, gender: 1, age: 18

可见,memcpy() 语句的确可以将查询信息拷贝给 result 的各个成员。不过,memcpy()这种拷贝一定是安全的吗?

拷贝结构体的“陷阱”

现在我们对 save_students 和 query_student 结构体稍作修改,修改后的C语言代码如下,请看:

struct save_student{
    int   index;
    int   offset;

    int   number;
    char  name[31];
    int   gender;
    int   age;
};

struct query_student{
    int   number;
    char  name[31];
    int   gender;
    int   age;
}__attribute__((packed));
4314f862bec8581befd10379fb5d508b.png

编译并执行修改后的C语言代码,发现输出有些奇怪,gender 不再等于 1,age 也不是预期的 18:
# gcc t.c
# ./a.out 
result number: 1, name: Jim, gender: 309, age: 4608

这是怎么回事呢?其实原因本专栏之前的文章里已经介绍过:这与C语言结构体成员的对齐机制有关。对于 save_student 结构体中的成员,为了提升效率,C语言编译器对其做了“对齐”操作,在 name 成员后填补了 1 个字节,布局大致如下:

724fc74542456aea44c1acf83d406ebc.png

对于 query_student 结构体,因为__attribute__((packed))修饰符,编译器不再做对齐操作,所以它的布局如下:
e6264d185f7fad8bd106c2c3eee2f883.png

现在就一切明白了,在这种情况下,memcpy()拷贝结构体后,就相当于使用一个 43 Bytes 的数据结构体解释 44 Bytes 的内存空间,这肯定是要出错的。
71fe211b3dc11cbaaace071fe65cd586.png

本节主要介绍了使用 memcpy() 拷贝“部分”结构体成员的小窍门,这允许程序员写出更加简洁的C语言代码。不过要注意的是,C语言结构体的对齐机制可能会导致拷贝“出错”,值得说明的是,不是只有程序员使用__attribute__((packed))修饰符时,才会导致结构体部分内存“拷贝出错”。在定义复杂结构体时应对结构体的“自然对齐”了然于胸,如果读者对此概念感到迷惑,可以看看我之前的文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK