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

时间:2015-03-10 16:05:18

标签: c struct type-punning

我正在尝试在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 );
}

9 个答案:

答案 0 :(得分:7)

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

传统的解决方案是利用以下事实:指向任何给定结构的指针始终保证与指向该结构的第一个元素的指针相同(即结构没有前导填充: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的前导元素的位置显式< / em>依赖。


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

答案 1 :(得分:1)

如果你真的想要隐藏string_private的定义,我会怎么做。

首先,您应该对包含类定义的结构进行extern,否则它将在声明标头的每个转换单元中重复。将其移至'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源中,声明私有结构类型,在翻译单元外部看不到。使公共类型成为该结构的成员。构造函数将分配私有struct对象,但返回指向其中包含的公共对象的指针。使用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 );
}

用法不变......

main.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;
}
//------------------------------------------------------------------------------

答案 2 :(得分:1)

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

更好的方法是使用private_或类似的东西为私有属性添加前缀。如果你真的想要限制范围,那么在类的.c文件中创建一个静态本地数据数组,并在每次创建一个新对象时为其附加一个“私有”数据结构。基本上,您将私有数据保存在C模块中,并利用c文件范围规则为您提供私有访问保护,尽管这实际上是非常有效的。

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

答案 3 :(得分:1)

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

如果你想要一个C保证可以工作的方法,那么给你的子类一个超类的类型(不是指向这个类的指针)。例如,

typedef struct string_private {
    struct string_public parent;
    char *string;
} string_private;

这需要不同的语法来访问&#34;继承&#34;成员,但你绝对可以肯定...

string_private *my_string;
/* ... initialize my_string ... */
function_with_string_parameter((string) my_string);

...有效(假设您有typedef ed&#34;字符串&#34;为struct string_public *)。而且,你甚至可以避免像这样的演员:

function_with_string_parameter(&my_string->parent);
然而,

这可能是一个完全不同的问题。使用面向对象编程本身并不是一个合适的目标。 OO是一个工具,用于组织具有一些显着优势的代码,但您可以在不模仿任何特定OO语言的特定语法的情况下以OO风格编写。

答案 4 :(得分:1)

在大多数情况下,这可以是任意长度的初始序列,因为所有已知的编译器都会给两个struct的公共成员提供相同的填充。如果他们没有给他们相同的填充,那么他们在遵循C标准的要求时会有一段时间:

  

为了简化联合的使用,我们做了一个特别的保证:如果一个联合包含几个共享一个公共初始序列的结构,并且如果联合对象当前包含这些结构中的一个,则允许检查公共初始其中任何一部分。

如果&#34;初始序列&#34;我真的无法想象编译器如何处理这个问题。将在两个struct中以不同方式填充。

但是有一个严重的&#34;但是&#34;。 严格别名应该关闭以使此设置生效。

严格别名是一条规则,基本上规定两个不兼容类型的指针不能引用相同的内存位置。因此,如果将指向较大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)希望让自己的生活变得更轻松:它认为不兼容类型的两个指针只能&#39; t引用相同的内存位置,因此它不会考虑它们。因此,当您访问p_s_struct->memb1时,它会认为没有任何内容改变p_s_struct->memb1(它知道为1)的价值,因此它赢得了#{1}}。检查&#34; memb1的实际值,只输出1

一种规避这种方法的方法可能是将你的指针声明为指向volatile数据(这意味着告诉编译器这些数据可以在没有它注意的情况下从其他地方更改),但标准并不能保证这个工作。

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

答案 5 :(得分:1)

此代码是否可以在给定的编译器上运行取决于所讨论的编译器的质量,目标平台和预期用途。您可能会在两个地方遇到麻烦:

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

  2. 尽管高质量的编译器应寻求以有用的方式维护“公共初始序列”保证,但该标准将对诸如实现质量问题之类的支持视为对待,并且对于某些编译器来说,解释N1570 6.5p7变得更加流行。他们认为标准会以最低质量的方式提供,除非使用-fno-strict-aliasing进行调用。根据我的观察,icc似乎支持-fstrict-aliasing模式下的CIS保证,但是gcc和clang都处理低质量的方言,即使在所有指针都为指针的情况下,它实际上也会忽略通用初始序列规则永远不会在各自的生命周期内出现别名

使用一个好的编译器,您的代码将正常工作。使用质量低劣的编译器或配置为质量低劣的行为的编译器,您的代码将失败。

答案 6 :(得分:0)

从一个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 */ } 是不可靠的,因为类型不兼容。你可以依赖的是,如果父结构的第一个元素都在子结构的顶部并且顺序相同,那么 重新解释强制转换 将是让你做你想做的。像这样:

{{1}}

这与python在C级实现面向对象编程的方式完全相同。

答案 7 :(得分:0)

如果我没记错的话,这种类型的演员是按照标准的未定义行为。但是,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;
}

您可以来回投射它们并安全地访问这两个第一个成员。当然,如果你有一个小的并将它投射到大的那个,访问important_number成员并获得你的UB。

编辑:

这家伙写了一篇很好的文章:

Type punning isn't funny: Using pointers to recast in C is bad.

答案 8 :(得分:0)

另一种优雅的方法来扩展具有公共部分(例如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中的转义字符\后添加任何空格