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

问题描述 投票:7回答:8

我正在尝试在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 struct type-punning
8个回答
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的前导元素的位置才被明确地依赖。


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


1
投票

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

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

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


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

1
投票

C并不保证它会起作用,但通常它会起作用。特别是,C明确地保留了struct值的表示的大部分方面未指定(C99 6.2.6.1),包括较小的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);

......有效(假设你有typedefed“string”作为struct string_public *)。而且,你甚至可以避免像这样的演员:

function_with_string_parameter(&my_string->parent);

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


1
投票

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

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

如果“初始序列”在两个structs中填充不同,我真的无法想象编译器将如何处理这个问题。

但有一个严重的“但是”。应关闭严格别名以使此设置生效。

严格别名是一条规则,基本上规定两个不兼容类型的指针不能引用相同的内存位置。因此,如果您将指向较大的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数据(这意味着告诉编译器这些数据可以在没有它注意的情况下从其他地方更改),但标准并不能保证这一点。

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


1
投票

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

  1. 在某些平台上,写入结构的最后一个成员的最快方法可能会干扰填充位或其后的字节。如果该对象是与较长结构共享的公共初始序列的一部分,并且在较短的一个中用作填充的位用于在较长的一个中保存有意义的数据,则在写入最后一个字段时,这些数据可能会受到干扰。较短的类型。我认为我没有看到任何编译器实际上这样做,但行为是允许的,这就是为什么CIS规则只允许对普通成员进行“检查”的原因。
  2. 虽然质量编译器应该寻求以有用的方式维护通用初始序列保证,但标准对待诸如实施质量问题之类的东西,并且对于一些编译器来说,以最低质量的方式解释N1570 6.5p7变得更加时髦他们认为标准允许,除非用-fno-strict-aliasing调用。根据我的观察,icc似乎支持-fstrict-aliasing模式下的CIS保证,但是gcc和clang都处理一种低质量的方言,即使在指针在各自的生命周期内从不混淆的情况下,出于所有实际目的忽略了公共初始序列规则。

使用一个好的编译器,您的代码将起作用。使用质量较差的编译器,或配置为质量较差的编译器,您的代码将失败。


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

struct small_header {
    char[5]  ident;
    uint32_t header_size;
}

struct bigger_header {
    char[5]  ident;
    uint32_t header_size;
    uint32_t important_number;
}

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


0
投票

如果我没记错的话,这种类型的转换是根据标准的未定义行为。但是,GCC和MS C都保证这会像你想象的那样工作。

所以,例如:

important_number

您可以来回投射它们并安全地访问这两个第一个成员。当然,如果你有一个小的并把它投到大的那个,访问Type punning isn't funny: Using pointers to recast in C is bad.成员,让你获得一个UB。

编辑:

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

qazxswpoi

© www.soinside.com 2019 - 2024. All rights reserved.