我想在数组中存储混合数据类型。怎么可能这样做?
答案 0 :(得分:239)
您可以使数组元素成为有区别的联合,即tagged union。
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;
如果要访问数组的元素,必须首先检查类型,然后使用union的相应成员。 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
中存储的最后一个值。
答案 1 :(得分:32)
使用联盟:
union {
int ival;
float fval;
void *pval;
} array[10];
但是,您必须跟踪每个元素的类型。
答案 2 :(得分:20)
数组元素需要具有相同的大小,这就是为什么它不可能。您可以通过创建variant type:
来解决这个问题#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;
}
union的元素大小是最大元素的大小,4。
答案 3 :(得分:8)
通过删除内部联合,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_
初始化程序别名相同的数据区域。即使我们知道布局(!), 也应该。不,不是。改为使用“internal”标签(它覆盖外部标签,就像我们想要的那样,但不会混淆编译器。)
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
答案 4 :(得分:5)
您可以使用void *
的独立数组执行size_t.
数组但是您丢失了信息类型。
如果需要以某种方式保留信息类型,请保留第三个int数组(其中int是枚举值)然后根据enum
值对转换的函数进行编码。
答案 5 :(得分:3)
联盟是标准的出路。但是你也有其他解决方案。其中一个是tagged pointer,它涉及在指针的“free”位中存储更多信息。
根据体系结构的不同,您可以使用低位或高位,但最安全,最便携的方法是利用对齐内存来使用未使用的低位。例如,在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
和uint64_t
...),你还会再多一点对于标签。
这有一个缺点,如果数据尚未存储在其他地方的变量中,则需要更多内存。因此,如果数据的类型和范围有限,您可以将值直接存储在指针中。此技术已在32位版本的 Chrome的V8引擎中使用,它会检查地址的最低有效位,以查看它是否是指向另一个对象的指针(如double,big integer,string或some objects)或 31位有符号值(称为smi
- small integer)。如果它是int
,Chrome只会将算术右移1位来获取值,否则指针将被解除引用。
在大多数当前的64位系统上,虚拟地址空间仍然比64位窄得多,因此高最高有效位也可以用作标记。根据架构,您可以使用不同的方式将其用作标签。 ARM,68k和许多其他人可以配置为忽略顶部位,允许您自由使用它们而不必担心段错或其他任何问题。来自上面链接的维基百科文章:
使用标记指针的一个重要示例是ARM64上iOS 7上的Objective-C运行时,特别是在iPhone 5S上使用。在iOS 7中,虚拟地址是33位(字节对齐),因此字对齐地址仅使用30位(3个最低有效位为0),标记为34位。 Objective-C类指针是字对齐的,标记字段用于多种用途,例如存储引用计数以及对象是否具有析构函数。
早期版本的MacOS使用名为Handles的标记地址来存储对数据对象的引用。地址的高位分别指示数据对象是否被锁定,可清除和/或源自资源文件。当系统7中的MacOS寻址从24位提升到32位时,这会导致兼容性问题。
在x86_64 you can still use the high bits as tags with care上。当然,您不需要使用所有这16位,并且可以省略一些位以供将来证明
在早期版本的Mozilla Firefox中,他们还使用小整数优化,如V8, 3低位用于存储类型(int,string,object ..等)。但是,自从JägerMonkey他们采取了另一条道路(Mozilla’s New JavaScript Value Representation,backup link)。该值现在始终存储在64位双精度变量中。当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