假设我有类似的东西:
列表.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 的严格别名规则吗?
关于在“兼容”结构之间转换指针有很多类似的问题,但答案通常不一致,甚至相互矛盾。有人说通过任一指针访问公共初始序列的字段都可以,而有人说你甚至不能取消引用强制转换的指针。
有一个特殊规则,参见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:
在结构体对象中,非位域成员和位域所在的单元的地址按照声明的顺序递增。指向结构对象的指针经过适当转换后,指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。结构对象内可能有未命名的填充,但不是在其开头。
因此,转换指针并取消引用来访问上一个/下一个字段似乎没问题,但这不是已经违反了 C 的严格别名规则吗?
确实如此,即它确实违反了严格别名规则;但问题是,它可能已成为一种广泛使用的模式,因为它通常会编译为预期的形式。
不要仅在
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);
}