如果读者看了本专栏之前的文章,应该能够发现,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);
编译并执行这段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));
编译并执行修改后的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 个字节,布局大致如下:
对于 query_student 结构体,因为__attribute__((packed))
修饰符,编译器不再做对齐操作,所以它的布局如下:
现在就一切明白了,在这种情况下,memcpy()拷贝结构体后,就相当于使用一个 43 Bytes 的数据结构体解释 44 Bytes 的内存空间,这肯定是要出错的。
小结
本节主要介绍了使用 memcpy() 拷贝“部分”结构体成员的小窍门,这允许程序员写出更加简洁的C语言代码。不过要注意的是,C语言结构体的对齐机制可能会导致拷贝“出错”,值得说明的是,不是只有程序员使用__attribute__((packed))
修饰符时,才会导致结构体部分内存“拷贝出错”。在定义复杂结构体时应对结构体的“自然对齐”了然于胸,如果读者对此概念感到迷惑,可以看看我之前的文章。