Answers:
您可以使数组元素成为可区分的联合,也称为标记联合。
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
该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。
定义标签联合(使用任何名称)有另一种方式,IMO 通过删除内部联合使它更易于使用。这是X Window系统中用于事件之类的样式。
Barmar答案中的示例将名称val
指定为内部联合。Sp。的答案中的示例使用匿名联合,以避免必须指定.val.
每次访问变体记录时都。不幸的是,C89或C99中没有“匿名”内部结构和联合。这是一个编译器扩展,因此固有地不可移植。
IMO更好的方法是颠倒整个定义。将每个数据类型设为自己的结构,然后将标记(类型说明符)放入每个结构中。
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_
后面初始化别名相同的数据区域。即使我们知道布局(!),也应该可以。不,不是。请改用“内部”标记(就像我们想要的那样,它覆盖外部标记,但不会混淆编译器)。
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
不会在同一区域使用别名,因为编译器知道.val
偏移量比更大.tag
。您是否有链接可以进一步讨论这个所谓的问题?
工会是标准的做法。但是您还有其他解决方案。其中之一是标记指针,它涉及在指针的“空闲”位中存储更多信息。
根据架构的不同,您可以使用低位或高位,但是最安全,最便携的方式是利用对齐内存的优势来使用未使用的低位。例如,在32位和64位系统中,指向的指针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;
}
如果您可以确保数据是8字节对齐的(例如64位系统中的指针,或long long
and uint64_t
...),则标签将再有一位。
这样做的一个缺点是,如果没有将数据存储在其他位置的变量中,则需要更多的内存。因此,如果数据的类型和范围受到限制,则可以将值直接存储在指针中。此技术已在32位版本的Chrome V8引擎中使用,它会检查地址的最低有效位以查看它是否是指向另一个对象(例如,双精度,大整数,字符串或某个对象)或31的指针。位符号值(称为smi
-小整数)。如果是int
,Chrome只会将算术右移1位以获取值,否则将取消引用指针。
在当前的大多数64位系统上,虚拟地址空间仍比64位窄得多,因此最高有效位也可以用作标记。根据体系结构,您有不同的方式将它们用作标签。可以将ARM,68k和许多其他配置为忽略高位,从而使您可以自由使用它们,而无需担心segfault或其他问题。从上面链接的维基百科文章中:
使用标记指针的一个重要示例是ARM64上的iOS 7上的Objective-C运行时,尤其是在iPhone 5S上使用。在iOS 7中,虚拟地址为33位(字节对齐),因此字对齐地址仅使用30位(3个最低有效位为0),为标记保留34位。Objective-C类指针是按字对齐的,并且标记字段用于许多目的,例如存储引用计数以及对象是否具有析构函数。
MacOS的早期版本使用标记为Handles的地址来存储对数据对象的引用。地址的高位分别指示数据对象是已锁定,可清除和/或源自资源文件。当MacOS寻址在System 7中从24位提高到32位时,这会引起兼容性问题。
在x86_64上,您仍然可以小心使用高位作为标签。当然,您不需要全部使用这16位,并且可以省略一些位以供将来使用
在Mozilla Firefox的早期版本中,它们还使用小型整数优化(例如V8),其中3个低位用于存储类型(整数,字符串,对象等)。但是自从JägerMonkey以来,他们采取了另一条道路(Mozilla的New JavaScript Value Representation,备份链接)。现在,该值始终存储在64位双精度变量中。当double
是一个归一化的一个,它可以直接用于计算。但是,如果高16位全为1(表示NaN),则低32位会将地址(在32位计算机中)存储为该值或直接将该值存储,其余16位将被使用存储类型。此技术称为NaN装箱或修女拳。它也用在64位WebKit的JavaScriptCore和Mozilla的SpiderMonkey中,指针存储在低48位中。如果您的主要数据类型为浮点数,则这是最佳解决方案,并且可以提供非常好的性能。
阅读有关上述技术的更多信息:https : //wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations