struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
< p > type
成员用于保存union
的哪个成员应该用于每个数组元素的选择。因此,如果您想在第一个元素中存储一个int
,则应执行以下操作:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
当你想访问数组的元素时,你必须首先检查类型,然后使用联合的相应成员。使用switch
语句很有用:
switch (my_array[n].type) {
case is_int:
// Do stuff for integer, using my_array[n].ival
break;
case is_float:
// Do stuff for float, using my_array[n].fval
break;
case is_char:
// Do stuff for char, using my_array[n].cvar
break;
default:
// Report an error, this shouldn't happen
}
程序员需要确保type
成员始终对应于union
中存储的最后一个值。
使用联合:
union {
int ival;
float fval;
void *pval;
} array[10];
不过,你需要跟踪每个元素的类型。
数组元素需要具有相同的大小,这就是为什么不可能的原因。您可以通过创建变体类型来解决此问题:
#include <stdio.h>
#define SIZE 3
typedef enum __VarType {
V_INT,
V_CHAR,
V_FLOAT,
} VarType;
typedef struct __Var {
VarType type;
union {
int i;
char c;
float f;
};
} Var;
void var_init_int(Var *v, int i) {
v->type = V_INT;
v->i = i;
}
void var_init_char(Var *v, char c) {
v->type = V_CHAR;
v->c = c;
}
void var_init_float(Var *v, float f) {
v->type = V_FLOAT;
v->f = f;
}
int main(int argc, char **argv) {
Var v[SIZE];
int i;
var_init_int(&v[0], 10);
var_init_char(&v[1], 'C');
var_init_float(&v[2], 3.14);
for( i = 0 ; i < SIZE ; i++ ) {
switch( v[i].type ) {
case V_INT : printf("INT %d\n", v[i].i); break;
case V_CHAR : printf("CHAR %c\n", v[i].c); break;
case V_FLOAT: printf("FLOAT %f\n", v[i].f); break;
}
}
return 0;
}
联合体元素的大小是最大元素的大小,即4。
有一种不同风格的定义标签联合(无论叫什么名字)的方法,我认为它通过去除内部联合使得使用更加美好。这是X Window系统中用于事件等事情的样式。
Barmar回答中的示例将名称val
赋予内部联合。Sp.的回答中使用匿名联合来避免每次访问变体记录时都需要指定.val.
。不幸的是,“匿名”内部结构和联合在C89或C99中不可用。它是一个编译器扩展,因此固有的不可移植性。
我认为更好的方法是颠倒整个定义。使每个数据类型都成为自己的结构,并将标签(类型说明符)放入每个结构中。
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
然后您将这些内容包装在顶级联合中。
typedef union {
int tag;
integer int_;
real real_;
} record;
enum types { INVALID, INT, REAL };
现在看起来我们好像在重复自己,而我们确实是在重复。但请考虑这个定义很可能只会被限定在一个单独的文件中。但是我们已经消除了在获取数据之前指定中间.val.
的干扰。
record i;
i.tag = INT;
i.int_.val = 12;
record r;
r.tag = REAL;
r.real_.val = 57.0;
相反,它会被放在结尾处,这样就不那么讨厌了。:D
另一个好处是这种方式可以实现一种继承的形式。编辑:这部分不是标准的C语法,但使用了GNU扩展。
if (r.tag == INT) {
integer x = r;
x.val = 36;
} else if (r.tag == REAL) {
real x = r;
x.val = 25.0;
}
integer g = { INT, 100 };
record rg = g;
向上转型和向下转型。
编辑:需要注意的一个问题是,如果您使用C99指定的初始化程序来构造其中之一,则所有成员初始化程序都应通过同一联合成员完成。
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
.tag
的初始化器可能会被优化编译器忽略,因为后面的.int_
初始化器将 别名化了相同的数据区域。即便是我们知道这个布局(!),并且它应该没问题,但实际上却不是这样。请改用 "internal" 标签(它覆盖外部标签,就像我们希望的那样,但不会混淆编译器)。
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
并不与相同的区域别名,因为编译器知道.val
的偏移量大于.tag
。你有关于这个所谓问题的进一步讨论链接吗? - M.Msize_t
数组来创建一个void *
数组,但是您会丢失信息类型。enum
值进行转换的函数。int
指针必须是4的倍数(假设int
是32位类型),而且2个最低有效位必须为0,因此您可以使用它们来存储值的类型。当然,在解引用指针之前,您需要清除标记位。例如,如果您的数据类型仅限于4种不同类型,则可以像下面这样使用它。void* tp; // tagged pointer
enum { is_int, is_double, is_char_p, is_char } type;
// ...
uintptr_t addr = (uintptr_t)tp & ~0x03; // clear the 2 low bits in the pointer
switch ((uintptr_t)tp & 0x03) // check the tag (2 low bits) for the type
{
case is_int: // data is int
printf("%d\n", *((int*)addr));
break;
case is_double: // data is double
printf("%f\n", *((double*)addr));
break;
case is_char_p: // data is char*
printf("%s\n", (char*)addr);
break;
case is_char: // data is char
printf("%c\n", *((char*)addr));
break;
}
long long
和uint64_t
...),那么您将有一个标记的额外位。这样做的一个缺点是,如果数据没有在其他地方存储在变量中,您需要更多的内存。因此,如果您的数据类型和范围有限,则可以直接将值存储在指针中。这种技术已经在32位版本的Chrome V8引擎中使用,其中它检查地址的最低有效位,以查看是否为指向另一个对象(如double、大整数、字符串或某个对象)的指针,还是31位有符号值(称为smi
- small integer)。如果它是一个int
,Chrome只需进行算术右移1位即可获得该值,否则将解除引用指针。
double
是规范化的时,它可以直接用于计算。然而,如果它的高16位都是1,表示NaN,则低32位将存储值或直接存储值的地址(在32位计算机上),剩余的16位将用于存储类型。这种技术称为NaN-boxing或nun-boxing。它还在64位WebKit的JavaScriptCore和Mozilla的SpiderMonkey中使用,指针存储在低48位中。如果您的主要数据类型是浮点数,则这是最好的解决方案,并提供非常好的性能。
阅读有关上述技术的更多信息:https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations