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

如果读者看了本专栏之前的文章,应该能够发现,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))修饰符时,才会导致结构体部分内存“拷贝出错”。在定义复杂结构体时应对结构体的“自然对齐”了然于胸,如果读者对此概念感到迷惑,可以看看我之前的文章。

阅读更多:   杂谈 , C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif