我在由perl脚本生成的文件中(在编译期间)中有以下声明:
struct _gamedata
{
short res_count;
struct
{
void * resptr;
short id;
short type;
} res_table[3];
}
_gamecoderes =
{
3,
{
{ &char_resource_ID_RES_welcome_object_ID,1002, 1001 },
{ &blah_resource_ID_RES_another_object_ID,1004, 1003 },
{ &char_resource_ID_RES_someting_object_ID,8019, 1001 },
}
};
我的问题是在编译期间生成了struct _gamedata
,res_table
中的项目数会有所不同。所以我不能提前提前声明res_table
的大小。
我需要解析这个结构的一个实例,最初我是通过一个指向char的指针(而不是将struct _gamedata
定义为一个类型。)但我定义的是res_table
。
e.g。
char * pb = (char *)_gamecoderes;
// i.e. pb points to the instance of `struct _gamedata`.
short res_count = (short *)pb;
pb+=2;
res_table * entry = (res_table *)pb;
for( int i = 0; i < res_count; i++ )
{
do_something_with_entry(*entry);
}
我的结果很糟糕。我不知道如何声明类型_struct gamedata
,因为我需要能够在编译时处理res_table
的变量长度。
答案 0 :(得分:7)
由于结构是匿名的,因此无法引用此结构的类型。 (res_table
只是成员名称,而不是类型名称)。您应该为结构提供名称:
struct GameResult {
short type;
short id;
void* resptr;
};
struct _gamedata {
short res_count;
GameResult res_table[3];
};
此外,您不应将data
投射到char*
。可以使用the ->
operator提取res_count
和entry
。这样可以正确计算成员偏移量。
_gamedata* data = ...;
short res_count = data->res_count;
GameResult* entry = data->res_table;
或简单地说:
_gamedata* data;
for (int i = 0; i < data->res_count; ++ i)
do_something_with_entry(data->res_table[i]);
答案 1 :(得分:3)
你的问题是对齐的。 res_count
和res_table
之间至少会有两个字节的填充,因此您不能简单地将两个字节添加到pb
。获取res_table
指针的正确方法是:
res_table *table = &data->res_table;
如果您坚持要转向char*
并返回,则必须使用offsetof
:
#include <stddef.h>
...
res_table *table = (res_table *) (pb + offsetof(_gamedata, res_table));
注意:在C ++中,您可能不会将offsetof
与“非POD”数据类型一起使用(大约“您在普通C中无法声明的类型”)。正确的习语 - 无需投射到char*
并返回 - 无论如何都可以。
答案 2 :(得分:2)
正如@Zack指出的那样,你的结构元素之间存在填充。
我假设您有一个char *因为您已经序列化了结构(在缓存中,在磁盘上或通过网络)。仅仅因为你开始使用char *
并不意味着你必须以艰难的方式访问整个结构。将其转换为类型指针,让编译器为您完成工作:
_gamedata * data = (_gamedata *) my_char_pointer;
for( int i = 0; i < data->res_count; i++ )
{
do_something_with_entry(*data->res_table[i]);
}
答案 3 :(得分:2)
memcpy(3)
,至少使用类型_gamedata
,或定义协议我们可以考虑两个用例。我称之为程序员-API类型,序列化是一种内部便利,记录格式由编译器和库决定。在更正式定义和防弹的实现中,定义了一个协议,并编写了一个专用库来便携地读写一个流。
最佳实践将根据创建版本化协议和开发流I / O操作是否有意义而有所不同。
从编译器 - 对象序列化流中读取时,最好和最完全可移植的实现是声明或动态分配精确或最大大小的_gamedata
,然后使用memcpy(3)
将数据拉出串行流或设备内存或其他任何内容。这使编译器可以分配编译器代码访问的对象,并允许开发人员分配开发人员访问的对象(即char *
)逻辑。
但至少要设置指向_gamedata
的指针,编译器会为你做一切。另请注意,无论res_table[n]
数组的大小如何,res_table[]
始终位于“正确”地址。这不像是让它更大改变第一个元素的位置。
如果_gamedata
对象本身位于缓冲区中并且可能未对齐,例如,如果它不是编译器为_gamedata
类型分配的对象,或者是真实的动态分配器,那么你仍然有潜在的对齐问题,唯一正确的解决方案是memcpy(3)
缓冲区中的每个离散类型。
一个典型的错误是无论如何都要使用未对齐的指针,因为它在x86上工作(缓慢)。但是,在内核模式下或启用高级优化时,它可能无法在移动设备或未来架构上运行,也可能无法在某些架构上运行。最好坚持使用真正的C99。
最后,当以任何方式序列化二进制数据时,您实际上是定义协议。因此,为了获得最大的稳健性,请不要让编译器定义您的协议。由于你在C中,你通常可以离散地处理每个基本对象而不会降低速度。如果作者和读者都这样做,那么只有开发人员必须就协议达成一致,而不是开发人员和编译器以及构建团队,C99作者和Dennis M. Ritchie,可能还有其他人。