提问人:Josu Goñi 提问时间:3/11/2015 最后编辑:Josu Goñi 更新时间:9/23/2020 访问量:5640
将一个 C 结构体转换为另一个元素较少的 C 结构体是否安全?
Is it safe to cast a C struct to another with fewer elements?
问:
我正在尝试在 C 上做 OOP(只是为了好玩),我想出了一种方法来进行数据抽象,方法是先使用公共部分的结构体和公共部分的更大结构,然后是私有部分。这样,我在构造函数中创建整个结构,并将其强制转换为小结构。这是正确的还是会失败的?
下面是一个示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// PUBLIC PART (header)
typedef struct string_public {
void (*print)( struct string_public * );
} *string;
string string_class_constructor( const char *s );
void string_class_destructor( string s );
struct {
string (*new)( const char * );
void (*delete)( string );
} string_class = { string_class_constructor, string_class_destructor };
// TEST PROGRAM ----------------------------------------------------------------
int main() {
string s = string_class.new( "Hello" );
s->print( s );
string_class.delete( s ); s = NULL;
return 0;
}
//------------------------------------------------------------------------------
// PRIVATE PART
typedef struct string_private {
// Public part
void (*print)( string );
// Private part
char *stringData;
} string_private;
void print( string s ) {
string_private *sp = (string_private *)( s );
puts( sp->stringData );
}
string string_class_constructor( const char *s ) {
string_private *obj = malloc( sizeof( string_private ) );
obj->stringData = malloc( strlen( s ) + 1 );
strcpy( obj->stringData, s );
obj->print = print;
return (string)( obj );
}
void string_class_destructor( string s ) {
string_private *sp = (string_private *)( s );
free( sp->stringData );
free( sp );
}
答:
从一个类型转换到另一个类型是不可靠的,因为类型不兼容。不过,您可以依赖的是,如果父结构的第一个元素都位于子结构的顶部并且顺序相同,那么重新解释强制转换将允许您执行所需的操作。这样:struct
struct parent {
int data;
char *more_data;
};
struct child {
int data;
char *more_data;
double even_more_data;
};
int main() {
struct child c = {0};
struct parent p1 = (struct parent) c; /* bad */
struct parent p2 = *(struct parent *) &c; /* good */
}
这与 python 在 C 级别实现其面向对象编程的方式完全相同。
评论
C
没有明确的重新解释演员表。所有铸件都经过重新解释铸件。C
float a = 2.5; float b = (float) (int) a;
如果我没记错的话,根据标准,这种类型的铸造是未定义的行为。但是,GCC 和 MS C 都保证这将按照您的想法工作。
因此,例如:
struct small_header {
char[5] ident;
uint32_t header_size;
}
struct bigger_header {
char[5] ident;
uint32_t header_size;
uint32_t important_number;
}
您可以来回施放它们并安全地访问前两个成员。当然,如果你有一个小的,把它扔到大的,访问这个成员,给你一个UB。important_number
编辑:
这家伙写了一篇关于这个的好文章:
类型双关语并不好笑:在 C 语言中使用指针进行重铸是不好的。
评论
struct s {int x;} foo = {0}; foo.x=1;
如果你真的打算隐藏string_private的定义,我会这样做。
首先,您应该将包含类定义的结构外部化,否则它将在声明标头的每个翻译单元中重复。将其移动到“c”文件。否则,公共接口中几乎没有变化。
string_class.h:
#ifndef STRING_CLASS_H
#define STRING_CLASS_H
// PUBLIC PART (header)
typedef struct string_public {
void (*print)( struct string_public * );
} *string;
string string_class_constructor( const char *s );
void string_class_destructor( string s );
typedef struct {
string (*new)( const char * );
void (*delete)( string );
} string_class_def;
extern string_class_def string_class;
#endif
在string_class源中,声明一个私有结构类型,在翻译单元之外看不到。使公共类型成为该结构的成员。构造函数将分配私有结构对象,但返回指向其中包含的公共对象的指针。使用魔法从公共施放回私人。offsetof
string_class.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include "string_class.h"
typedef struct string_private {
void (*print)( string );
char *string;
struct string_public public;
} string_private;
string_class_def string_class = { string_class_constructor, string_class_destructor };
void print( string s ) {
/* this ugly cast is where the "Magic" happens. Basically,
it converts the string into a char pointer so subtraction will
work on byte boundaries. Then subtracts the offset of public
from the start of string_private to back up to a pointer to
the private object. "offsetof" should be in <stddef.h>*/
string_private *sp = (string_private *)( (char*) s - offsetof(struct string_private, public));
// Private part
puts( sp->string );
}
string string_class_constructor( const char *s ) {
string_private *obj = malloc( sizeof( string_private ) );
obj->string = malloc( strlen( s ) + 1 );
strcpy( obj->string, s );
obj->public.print = print;
return (string)( &obj->public );
}
void string_class_destructor( string s ) {
string_private *sp = (string_private *)( (char*) s - offsetof(struct string_private, public));
free( sp->string );
free( sp );
}
用法保持不变...
主.c:
#include <stdlib.h> // just for NULL
#include "string_class.h"
// TEST PROGRAM ----------------------------------------------------------------
int main() {
string s = string_class.new( "Hello" );
s->print( s );
string_class.delete( s ); s = NULL;
return 0;
}
//------------------------------------------------------------------------------
评论
好吧,它可能会起作用,但这不是一种非常安全的做事方式。从本质上讲,您只是试图通过缩短结构来“隐藏”对对象私有数据的访问。数据仍然存在,只是无法通过语义访问。这种方法的问题在于,您需要确切地知道编译器如何对结构中的字节进行排序,否则您将从强制转换中获得不同的结果。从记忆中,这在 C 规范中没有定义(其他人可以纠正我)。
更好的方法是在私有属性前面加上 private_ 或类似的东西。如果你真的想限制范围,那么在类的 .c 文件中创建一个静态本地数据数组,并在每次创建新对象时附加一个“私有”数据结构。从本质上讲,您将私有数据保存在 C 模块中,并利用 c 文件范围规则来为您提供私有访问保护,尽管这确实是很多白费的工作。
此外,您的 OO 设计有点令人困惑。字符串类实际上是一个创建字符串对象的字符串工厂对象,如果将这两者分开会更清楚。
评论
从理论上讲,这可能是不安全的。两个单独声明的结构允许具有不同的内部安排,因为它们绝对没有明确的要求是兼容的。在实践中,编译器极不可能为两个相同的成员列表实际生成不同的结构(除非某处有特定于实现的注释,此时赌注是关闭的 - 但您会知道这一点)。
传统的解决方案是利用这样一个事实,即指向任何给定结构的指针始终保证与指向该结构的第一个元素的指针相同(即结构没有前导填充:C11,6.7.2.1.15)。这意味着,通过在两个结构的领先位置使用共享类型的值结构,可以强制两个结构的前导元素不仅相同,而且严格兼容:
struct shared {
int a, b, c;
};
struct Foo {
struct shared base;
int d, e, f;
};
struct Bar {
struct shared base;
int x, y, z;
};
void work_on_shared(struct shared * s) { /**/ }
//...
struct Foo * f = //...
struct Bar * b = //...
work_on_shared((struct shared *)f);
work_on_shared((struct shared *)b);
这是完全合规的,并保证有效,因为将共享元素打包到单个前导结构中意味着只有前导元素的位置或被明确依赖。Foo
Bar
在实践中,对齐不太可能是困扰您的问题。一个更紧迫的问题是别名(即允许编译器假设指向不兼容类型的指针没有别名)。指向结构的指针始终与指向其成员类型之一的指针兼容,因此共享基本策略不会给您带来任何问题;在某些情况下,使用编译器未强制标记为兼容的类型可能会导致它发出错误优化的代码,如果您不知道,这可能是一个非常困难的 Heisenbug。
评论
struct shared *b; struct foo *f = b; --> &b[1] != &f[1];
C 不能保证它会起作用,但通常它会起作用。特别是,C 明确地保留了值表示的大多数方面未指定 (C99 6.2.6.1),包括 your 较小值的表示是否与较大的相应初始成员的布局相同。struct
struct
struct
如果你想要一个 C 保证会起作用的方法,那么给你的子类一个其超类类型的成员(而不是指向此类类型的指针)。例如
typedef struct string_private {
struct string_public parent;
char *string;
} string_private;
这需要不同的语法来访问“继承”成员,但您可以绝对确定......
string_private *my_string;
/* ... initialize my_string ... */
function_with_string_parameter((string) my_string);
...有效(假设您已将 ed “string” 作为 )。此外,您甚至可以避免这样的投射:typedef
struct string_public *
function_with_string_parameter(&my_string->parent);
然而,这些可能有多大用处是一个完全不同的问题。使用面向对象编程本身并不是一个合适的目标。OO 是一种用于组织代码的工具,它具有一些显着的优点,但您可以以 OO 风格编写,而无需模仿任何特定 OO 语言的特定语法。
在大多数情况下,这对于任何长度的初始序列都是可以的,因为所有已知的编译器都会为两者的公共成员提供相同的填充。如果他们不给他们相同的填充,他们将有一段地狱般的时间遵循 C 标准的这个要求:struct
为了简化联合的使用,做出了一个特殊的保证:如果一个联合包含多个共享公共初始序列的结构,并且如果联合对象当前包含这些结构之一,则允许检查其中任何一个的共同初始部分。
我真的无法想象如果“初始序列”在两个 s 中的填充方式不同,编译器将如何处理这个问题。struct
但是有一个严重的“但是”。应关闭严格锯齿,此设置才能正常工作。
严格别名是一条规则,它基本上规定两个不兼容类型的指针不能引用相同的内存位置。因此,如果将指向较大指针的指针强制转换为指向较小指针的指针(反之亦然),则通过取消引用其中一个成员的初始序列来获取其初始序列中的成员的值,然后通过另一个值更改该值,然后从第一个指针再次检查它,则它不会更改。即:struct
struct smaller_struct {
int memb1;
int memb2;
}
struct larger_struct {
int memb1;
int memb2;
int additional_memb;
}
/* ... */
struct larger_struct l_struct, *p_l_struct;
struct smaller_struct *p_s_struct;
p_l_struct = &l_struct;
p_s_struct = (struct smaller_struct *)p_l_struct;
p_l_struct->memb1 = 1;
printf("%d", p_l_struct->memb1); /* Outputs 1 */
p_s_struct->memb1 = 2;
printf("%d", p_l_struct->memb1); /* Should output 1 with strict-aliasing enabled and 2 without strict-aliasing enabled */
你看,使用严格别名优化的编译器(如 -O3 模式下的 GCC)希望让自己的生活更轻松:它认为两个不兼容类型的指针不能引用相同的内存位置,因此它不认为它们引用。因此,当您访问时,它会认为没有任何东西更改了 (它知道的) 的值,因此它不会“检查”的实际值而只是输出。p_s_struct->memb1
p_s_struct->memb1
1
memb1
1
规避这种情况的一种方法可能是将指针声明为指向数据(这意味着告诉编译器可以从其他地方更改此数据而不会注意到),但标准并不能保证这有效。volatile
请注意,上述所有内容都适用于编译器未以特殊方式打包的 s。struct
评论
此代码是否适用于给定的编译器取决于相关编译器的质量、目标平台和预期用途。您可能会在以下两个地方遇到麻烦:
在某些平台上,写入结构的最后一个成员的最快方法可能会干扰其后面的填充位或字节。如果该对象是与较长结构共享的通用初始序列的一部分,并且在较短的结构中用作填充的位用于在较长的序列中保存有意义的数据,则在以较短类型写入最后一个字段时,此类数据可能会受到干扰。我不认为我见过任何编译器实际上这样做,但这种行为是允许的,这就是为什么 CIS 规则只允许“检查”公共成员的原因。
虽然质量编译器应该寻求以有用的方式维护通用初始序列保证,但标准将对此类事情的支持视为实现质量问题,并且对于一些编译器来说,以他们认为标准允许的最低质量方式解释 N1570 6.5p7 变得越来越流行,除非使用 .根据我的观察,icc 似乎在模式中支持 CIS 保证,但 gcc 和 clang 都处理一种低质量的方言,出于所有实际目的,即使在指针在其各自的生命周期内从未出现别名的情况下,它也会忽略通用初始序列规则。
-fno-strict-aliasing
-fstrict-aliasing
使用一个好的编译器,你的代码就会起作用。使用质量较差的编译器,或配置为以质量较差的方式运行的编译器,您的代码将失败。
使用公共部分(如 OOP)扩展结构的另一种优雅方式
#define BASE_T \
int a; \
int b; \
int c;
struct Base_t {
BASE_T
};
struct Foo_t {
BASE_T
int d, e, f;
};
struct Bar_t {
BASE_T
int x, y, z;
};
void doBaseStuff(struct Base_t * pBase) {
pBase->a = 1;
pBase->b = 2;
pBase->c = 3;
}
int main() {
struct Foo_t foo;
struct Bar_t bar;
doBaseStuff((struct Base_t*) &foo);
doBaseStuff((struct Base_t*) &bar);
bar.a = 0; // I can directly access on properties of BASE_T, without doing any cast
foo.e = 6;
return 0;
}
此代码兼容 C98 和 C99,但不要在 BASE_T 中的转义字符 \ 后添加任何空格
评论
string
是 的 typedef 也是 中的成员名,这真的很令人困惑。string_public
string_private