【问题标题】:Is casting and dereferencing struct pointers of "compatible" structs allowed?是否允许强制转换和取消引用“兼容”结构的结构指针?
【发布时间】:2021-05-04 11:25:16
【问题描述】:

假设我有类似的东西:

在 list.h 中:

//...
#include <stdlib.h>
typedef struct node_s{
    struct node_s *next;
    struct node_s *prev;

    char data[];
}node_t;

void* getDataFromNode(node_t *node){
    return(node->data);
}

node_t* newNode(size_t size){
    node_t *ret = malloc(sizeof(node_t));
    return(ret);
}
//...

在 main.c 中:

#include "list.h"
#include <stddef.h>
typedef struct float_node_s{
    struct foo_node_s *next;
    struct foo_node_s *prev;

    float someFloat;
}float_node_t;

int main(void){
    float *f;
    float_node_t *node;
    //1)
    node = (float_node_t*)newNode(sizeof(float_node_t));
    if(node == NULL){
        return(1);
    }
    //2)
    f = (float*)getDataFromNode((node_t*)node);
    return(0);
}

这是我在很多 C 列表/树/等中看到的。实现。

我可以这样做吗?

具体来说,我可以将node_t 指针转换为float_node_t 指针并将其分配给float_node_t 指针变量,如1)?如果我现在取消引用float_node_t 指针以访问存储在其中的浮点数怎么办?我猜2)已经被禁止了。返回的指针指向char 数组,它被强制转换为float 指针。

C 标准规定,指向不同结构的指针具有相同的表示和对齐要求,不会发生结构元素的重新排序,并且如果结构具有共同的初始序列,则这些结构的初始序列的布局将是相同的。

因此,转换指针和取消引用以访问 prev/next 字段似乎很好,但这不是已经违反了 C 的严格别名规则吗?

关于在“兼容”结构之间转换指针有很多类似的问题,但答案通常不同意甚至相互矛盾。有人说,通过任何一个指针访问公共初始序列的字段都可以,有人说你甚至不能取消引用强制转换的指针。

【问题讨论】:

  • 从法律上讲,您需要使用联合。此外,f = (float*)getDataFromNode((node_t*)node); 假定生成的指针将正确对齐。

标签: c pointers struct language-lawyer


【解决方案1】:

因此,转换指针和取消引用以访问 prev/next 字段似乎没问题,但这不是已经违反了 C 的严格别名规则吗?

是的,即它确实违反了严格的别名规则;但问题是,它可能已经成为一种广泛使用的模式,因为它通常会编译成预期的形式。

【讨论】:

  • because oftentimes it would compile to the expected form. - 这正是 UB 的行为方式。有时会按预期工作。
  • @0___________:C 标准委员会使用什么术语来描述他们期望大多数实现(包括普通平台的所有通用实现)以 100% 的时间以相同的可预测方式处理的构造,但他们认识到一些晦涩的实现可能无法按预期处理?
【解决方案2】:

有一个特殊规则,参见 C17 6.5.2.3:

一个特殊的保证是为了简化联合的使用:如果联合包含 几个结构共享一个共同的初始序列(见下文),如果联合 对象当前包含这些结构之一,允许检查常见的 它们中任何一个的初始部分,任何地方的完整类型的声明 可见。如果对应的成员,两个结构共享一个共同的初始序列 对于一个或多个序列具有兼容的类型(并且对于位域,具有相同的宽度) 初始成员。

因此,如果在同一个翻译单元中可见两个结构的union,则无论类型如何,您都应该能够检查每个结构的公共初始序列。然而,编译器对这个特定规则的支持并不稳固,并且有关于它的 C 语言缺陷报告。

然而,值得注意的是,此特殊规则符合“严格别名规则”,该规则允许通过兼容类型对结构/联合成员进行左值访问,“包括上述(兼容)类型之一的聚合或联合类型在其成员中”。

但是,所有这些都不允许在两个结构之间使用狂野的双关语,您可以在其中重新解释未以不同方式共享的部分 - 这只是严格的别名违规和 UB。

尽管如此,我们不应该编写依赖于这些不稳定规则的语言律师的程序。这里正确的解决方案是:

typedef struct node
{
  struct node* next;
  struct node* prev;
} node_t;

typedef struct
{
  node_t parent;
  float  data;
} float_node_t;

typedef struct
{
  node_t parent;
  char   data[n];
} str_node_t;

这就是多态性在 C 中的工作原理 - 您现在可以使用 float_node_t* 强制转换为 node_t* 并将其传递给任何期望 node_t* 的函数。

如果您希望在运行时更改类型,您也可以在其中显示一个枚举以跟踪类型。你可以用函数指针做多态性。这是一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct node
{
  struct node* next;
  struct node* prev;
  void (*print)(struct node*);
} node_t;

typedef struct
{
  node_t parent;
  float  data;
} float_node_t;
    
typedef struct
{
  node_t parent;
  char   data[100];
} str_node_t;

node_t* float_node_create (float f);
node_t* str_node_create (const char* s);

void float_node_print (struct node* this);
void str_node_print (struct node* this);

#define node_create(data)                   \
  _Generic( (data),                         \
            float: float_node_create,       \
            char*: str_node_create )(data) \

int main (void)
{
  node_t* n1 = node_create(1.0f);
  node_t* n2 = node_create("hello world");
  n1->print(n1);
  n2->print(n2);
  
  free(n1);
  free(n2);
  return 0;   
}

void float_node_print (struct node* this)
{
  printf("%f\n", ((float_node_t*)this)->data );
}

void str_node_print (struct node* this)
{
  puts( ((str_node_t*)this)->data );
}

node_t* float_node_create (float f)
{
  float_node_t* obj = malloc(sizeof *obj);
  obj->data  = f;
  obj->parent.print = float_node_print;
  return (node_t*)obj;
}

node_t* str_node_create (const char* s)
{
  str_node_t* obj = malloc(sizeof *obj);
  strcpy(obj->data,s);
  obj->parent.print = str_node_print;
  return (node_t*)obj;
}

所有这些都是明确定义的行为和可移植标准 C。这依赖于更加成熟和安全的语言规则,可在 6.7.2.1 中找到:

在结构对象中,非位域成员和位域所在的单元的地址按声明顺序递增。一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位域,则指向它所在的单元),反之亦然。结构对象中可能有未命名的填充,但不是在其开头。

【讨论】:

  • 是否有任何特殊原因将 OP 代码中的灵活数组成员 char data[] 更改为固定长度的数组成员?所需长度可以在str_node_create中确定。
  • @IanAbbott 灵活的数组成员只是为示例增加了不必要的复杂性,但没有特别的原因。
  • 我觉得这是一个愚蠢的问题,但为什么我可以用'node_t'指针调用'float_node_print'并将其转换为函数内的'float_node_t'。那是因为第 6.3.2.3 节“指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针并再次返回;结果应与原始指针比较。如果使用转换后的指针调用类型与指向的类型不兼容的函数,行为未定义”?
  • @lulle 这是因为我在答案末尾引用的最后一部分。 “指向结构对象的指针,经过适当转换,指向其初始成员,反之亦然”。我们可以在结构指针和指向初始成员的指针之间随意转换。这是一个可以追溯到早期 C 的强大规则。因此它不依赖于严格的别名和其他此类脆弱的概念。您引用的部分要广泛得多,它只是关于实际的指针转换。
【解决方案3】:

不要在sizeof 中仅使用对象 中的类型,尤其是在这种代码中。使用类型容易出错。

在这种情况下,指针双关语是 IMO 无效的(它违反了严格的别名规则)

我会这样做。

typedef struct node_s{
    struct node_s *next;
    struct node_s *prev;

    char data[];
}node_t;


typedef struct float_node_s{
    struct float_node_s *next;
    struct float_node_s *prev;
    
    float someFloat;
}float_node_t;

typedef union
{
    node_t node_c;
    float_node_t node_f;
}node_ut;

float getFloatDataFromNode(node_ut *node){
    return node -> node_f.someFloat;
}

void* newNode(size_t size){
    node_t *ret = malloc(sizeof(*ret) + size);
    return(ret);
}


int main(void){
    float f;
    node_ut *node;
    //1)
    node = newNode(sizeof(node -> node_f.someFloat));
    if(node == NULL){
        return(1);
    }
    //2)
    f = getFloatDataFromNode(node);
    return(0);
}

https://godbolt.org/z/c3csvoEeY

【讨论】:

  • newNode 中,node_t *ret = malloc(sizeof(*ret) + size); 不考虑任何可能需要的额外填充。
  • @IanAbbott 这是为这种数据结构分配内存的方式。在这种情况下,char 成员之前的填充由 sizeof(*ret) 计算,而 char 成员之后的填充无关紧要。
  • float 之前的填充呢?
  • 我假设 sizeof(float) 也会发生同样的情况?
  • @lulle 在prevsomeFloat 之间可能存在未计入总大小的填充。 float 的对齐大小不太可能比指针类型大,但有可能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-04-17
  • 2012-01-31
  • 2015-08-04
  • 2011-02-04
  • 2012-07-23
  • 2018-09-03
  • 2013-01-04
相关资源
最近更新 更多