是否允许转换和取消引用“兼容”结构的结构指针?

问题描述 投票:0回答:3

假设我有类似的东西:

列表.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);
}

这是我在很多数据结构(例如列表、树)实现中看到的。

我可以这样做吗?

具体来说,我可以将

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

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

因此,通过转换指针并取消引用来访问上一个/下一个字段看起来很好,但这不是已经违反了 C 的严格别名规则吗?

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

c pointers struct language-lawyer
3个回答
4
投票

有一个特殊规则,参见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:

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


3
投票

因此,转换指针并取消引用来访问上一个/下一个字段似乎没问题,但这不是已经违反了 C 的严格别名规则吗?

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


-1
投票

不要仅在

sizeof
objects 中使用类型,尤其是在此类代码中。使用类型很容易出错。

在这种情况下,指针双关在 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

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