将一个 C 结构体转换为另一个元素较少的 C 结构体是否安全?

Is it safe to cast a C struct to another with fewer elements?

提问人:Josu Goñi 提问时间:3/11/2015 最后编辑:Josu Goñi 更新时间:9/23/2020 访问量:5640

问:

我正在尝试在 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 );
}
C 结构 体类型双关语

评论

0赞 David Ranieri 3/11/2015
string是 的 typedef 也是 中的成员名,这真的很令人困惑。string_publicstring_private
0赞 Josu Goñi 3/11/2015
你是对的,我应该重命名它,但这只是一个测试。无论如何,作为成员是实际的字符数组,而作为 typedef 是带有其方法的“类”。
1赞 chux - Reinstate Monica 3/11/2015
“将一个 C 结构转换为另一个元素较少的 C 结构体?”具有误导性。这里没有从一个 C 结构到另一个 C 结构的转换。OTOH 有一种指针类型到另一种指针类型的转换。建议“将一个 C 结构体 * 转换为另一个指向元素较少的结构体?
1赞 Sapphire_Brick 3/29/2020
更少的元素,而不是更少

答:

0赞 randomusername 3/11/2015 #1

从一个类型转换到另一个类型是不可靠的,因为类型不兼容。不过,您可以依赖的是,如果父结构的第一个元素都位于子结构的顶部并且顺序相同,那么重新解释强制转换将允许您执行所需的操作。这样: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 级别实现其面向对象编程的方式完全相同。

评论

2赞 RedX 3/11/2015
C没有明确的重新解释演员表。所有铸件都经过重新解释铸件。C
2赞 randomusername 3/11/2015
@RedX C 铸造并不总是重新解释铸造,例如float a = 2.5; float b = (float) (int) a;
1赞 RedX 3/11/2015
@randomusername你是对的。我忘记了使用强制转换进行数字转换。
1赞 randomusername 3/11/2015
@JotaGe 由于您正在对指针进行强制转换,因此您做对了。
1赞 John Bollinger 3/11/2015
根据标准,(所有)C 强制转换表达式将操作数的值转换为指定的类型。但是,将值从一个对象指针类型转换为另一个对象指针类型是一种重新标记的练习;它相当于重新解释指针的引用。
0赞 Vinicius Kamakura 3/11/2015 #2

如果我没记错的话,根据标准,这种类型的铸造是未定义的行为。但是,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 语言中使用指针进行重铸是不好的。

评论

0赞 sp2danny 3/11/2015
不是UB,而是定义明确,并由标准保证
0赞 Vinicius Kamakura 3/11/2015
@sp2danny愿意用标准部分来支持这一点吗?
1赞 sp2danny 3/11/2015
嗯,我发现唯一有效的是结构是同一联合的一部分。(6.5.2.3) 再看一些
0赞 Josu Goñi 3/11/2015
“如果我没记错的话,根据标准,这种类型的铸造是未定义的行为。”这就是我一直在寻找的答案,如果你(或任何人)可以肯定地说出来,我认为这是正确的答案。
0赞 supercat 6/27/2018
6.5p7 中的类型访问规则没有努力定义质量实现应以可预测的方式处理的所有情况。它甚至不允许类似的东西,而是依靠编译器编写者来使用一些常识。不幸的是,标准的编写方式导致编译器编写者认为,不应该期望高质量的编译器处理列出的结构之外的任何结构,除了那些任何程序都不可能没有的结构。struct s {int x;} foo = {0}; foo.x=1;
1赞 Brian McFarland 3/11/2015 #3

如果你真的打算隐藏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;
}
//------------------------------------------------------------------------------

评论

0赞 Josu Goñi 3/11/2015
这就是这个想法,正如你所看到的,它在评论中说“header”:“// PUBLIC PART (header)”。事实上,要正确地做到这一点,你应该使用“static”关键字来确保数据正确隐藏。
1赞 Jon 3/11/2015 #4

好吧,它可能会起作用,但这不是一种非常安全的做事方式。从本质上讲,您只是试图通过缩短结构来“隐藏”对对象私有数据的访问。数据仍然存在,只是无法通过语义访问。这种方法的问题在于,您需要确切地知道编译器如何对结构中的字节进行排序,否则您将从强制转换中获得不同的结果。从记忆中,这在 C 规范中没有定义(其他人可以纠正我)。

更好的方法是在私有属性前面加上 private_ 或类似的东西。如果你真的想限制范围,那么在类的 .c 文件中创建一个静态本地数据数组,并在每次创建新对象时附加一个“私有”数据结构。从本质上讲,您将私有数据保存在 C 模块中,并利用 c 文件范围规则来为您提供私有访问保护,尽管这确实是很多白费的工作。

此外,您的 OO 设计有点令人困惑。字符串类实际上是一个创建字符串对象的字符串工厂对象,如果将这两者分开会更清楚。

评论

0赞 Josu Goñi 3/11/2015
“字符串类实际上是一个字符串工厂对象” 它假装是类的静态部分,具有构造函数、析构函数和其他可以添加的静态函数和变量。
0赞 Jon 3/11/2015
哦,好吧,我明白你在做什么了。
12赞 Alex Celeste 3/11/2015 #5

从理论上讲,这可能是不安全的。两个单独声明的结构允许具有不同的内部安排,因为它们绝对没有明确的要求是兼容的。在实践中,编译器极不可能为两个相同的成员列表实际生成不同的结构(除非某处有特定于实现的注释,此时赌注是关闭的 - 但您会知道这一点)。

传统的解决方案是利用这样一个事实,即指向任何给定结构的指针始终保证与指向该结构的第一个元素的指针相同(即结构没有前导填充: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);

这是完全合规的,并保证有效,因为将共享元素打包到单个前导结构中意味着只有前导元素的位置或被明确依赖。FooBar


在实践中,对齐不太可能是困扰您的问题。一个更紧迫的问题是别名(即允许编译器假设指向不兼容类型的指针没有别名)。指向结构的指针始终与指向其成员类型之一的指针兼容,因此共享基本策略不会给您带来任何问题;在某些情况下,使用编译器未强制标记为兼容的类型可能会导致它发出错误优化的代码,如果您不知道,这可能是一个非常困难的 Heisenbug。

评论

0赞 chux - Reinstate Monica 3/11/2015
此外,指针算术也是有问题的。struct shared *b; struct foo *f = b; --> &b[1] != &f[1];
0赞 user2711115 3/24/2017
因此,使用这种技术(基本结构实例作为派生结构的第一个成员)没有必要关闭严格的别名(与 Mints97 在下面的回答中强调的 op 的解决方案相反)?
0赞 Alex Celeste 3/25/2017
@user2711115 是的,如果您检查 C11 6.5 第 7 段,就会提到这是在所有情况下都允许混叠的情况之一(第五个示例)。这始终是明确定义的,没有扩展。
1赞 John Bollinger 3/11/2015 #6

C 不能保证它会起作用,但通常它会起作用。特别是,C 明确地保留了值表示的大多数方面未指定 (C99 6.2.6.1),包括 your 较小值的表示是否与较大的相应初始成员的布局相同。structstructstruct

如果你想要一个 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” 作为 )。此外,您甚至可以避免这样的投射:typedefstruct string_public *

function_with_string_parameter(&my_string->parent);

然而,这些可能有多大用处是一个完全不同的问题。使用面向对象编程本身并不是一个合适的目标。OO 是一种用于组织代码的工具,它具有一些显着的优点,但您可以以 OO 风格编写,而无需模仿任何特定 OO 语言的特定语法。

1赞 user3079266 3/11/2015 #7

在大多数情况下,这对于任何长度的初始序列都是可以的,因为所有已知的编译器都会为两者的公共成员提供相同的填充。如果他们给他们相同的填充,他们将有一段地狱般的时间遵循 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->memb1p_s_struct->memb11memb11

规避这种情况的一种方法可能是将指针声明为指向数据(这意味着告诉编译器可以从其他地方更改此数据而不会注意到),但标准并不能保证这有效。volatile

请注意,上述所有内容都适用于编译器未以特殊方式打包的 s。struct

评论

1赞 supercat 6/27/2018
CIS 保证有用的主要原因是它允许函数互换处理多种结构。鉴于该标准的标准作者明确认识到实现符合要求的可能性,但质量也很差以至于毫无用处,它没有强制要求编译器以有用的方式遵守 CIS 保证这一事实并不意味着不努力这样做的实现不应被视为质量差。
1赞 supercat 7/14/2018 #8

此代码是否适用于给定的编译器取决于相关编译器的质量、目标平台和预期用途。您可能会在以下两个地方遇到麻烦:

  1. 在某些平台上,写入结构的最后一个成员的最快方法可能会干扰其后面的填充位或字节。如果该对象是与较长结构共享的通用初始序列的一部分,并且在较短的结构中用作填充的位用于在较长的序列中保存有意义的数据,则在以较短类型写入最后一个字段时,此类数据可能会受到干扰。我不认为我见过任何编译器实际上这样做,但这种行为是允许的,这就是为什么 CIS 规则只允许“检查”公共成员的原因。

  2. 虽然质量编译器应该寻求以有用的方式维护通用初始序列保证,但标准将对此类事情的支持视为实现质量问题,并且对于一些编译器来说,以他们认为标准允许的最低质量方式解释 N1570 6.5p7 变得越来越流行,除非使用 .根据我的观察,icc 似乎在模式中支持 CIS 保证,但 gcc 和 clang 都处理一种低质量的方言,出于所有实际目的,即使在指针在其各自的生命周期内从未出现别名的情况下,它也会忽略通用初始序列规则。-fno-strict-aliasing-fstrict-aliasing

使用一个好的编译器,你的代码就会起作用。使用质量较差的编译器,或配置为以质量较差的方式运行的编译器,您的代码将失败。

0赞 fjnet 11/28/2019 #9

使用公共部分(如 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 中的转义字符 \ 后添加任何空格