如何将混合数据类型(int、 float、 char 等)存储在数组中?

我想在一个数组中存储混合数据类型。如何做到这一点?

58304 次浏览

可以使用 size_t.分隔的数组执行 void *数组,但是会丢失信息类型。
如果需要以某种方式保留信息类型,那么保留第三个 int 数组(其中 int 是枚举值) ,然后编写根据 enum值进行强制转换的函数。

使用工会:

union {
int ival;
float fval;
void *pval;
} array[10];

但是,您必须跟踪每个元素的类型。

您可以使数组元素成为一个有区别的联合,也就是 带标签的工会

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中的最后一个值。

数组元素需要具有相同的大小,这就是为什么它不可能的原因。你可以通过创建一个 变异型来解决这个问题:

#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 通过删除内部联合使它更适合于 使用。这是 XWindowSystem 中用于事件之类事件的样式。

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

这允许的另一件事是继承的一种形式

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

工会是标准的解决办法。但是你也有其他的解决方案。其中之一是 标记的指针 ,它涉及到在指针的 “免费”位中存储更多信息。

根据不同的架构,您可以使用低位或高位,但是最安全和最便携的方式是利用对齐内存来使用 未使用的低位。例如,在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 longuint64_t...) ,那么标记就会多一个位。

这有一个缺点,如果数据没有存储在其他地方的变量中,则需要更多的内存。因此,在数据的类型和范围受到限制的情况下,可以将值直接存储在指针中。这种技术已经在32位版本的 Chrome 的 V8引擎中使用,它检查地址的最低有效位,看看是 指向另一个对象的指针(像双精度、大整数、字符串或某个对象)还是 31位有符号值(称为 小整数)。如果是 int,Chrome 只需要做一个右移1位的算术运算就可以得到值,否则指针就会被取消引用。


在大多数当前的64位系统中,虚拟地址空间仍然比64位窄得多,因此使用了 最高最高有效位也可以用作标记。根据不同的体系结构,您可以使用不同的方式来将这些标记用作标记。手臂六万八和许多其他可以配置到 忽略上面的部分,允许您自由使用它们而不用担心 Segfault 或任何东西。从上面链接的维基百科文章:

使用标记指针的一个重要例子是 ARM64上的 iOS7上的 Objective-C 运行时,特别是在 iPhone5S 上。在 iOS7中,虚拟地址是33位(字节对齐) ,所以字对齐的地址只使用30位(3个最低有效位为0) ,留下34位给标签。Objective-C 类指针是字对齐的,标记字段用于许多目的,比如存储引用计数以及对象是否具有析构函数。

早期版本的 MacOS 使用称为 Handles 的标记地址来存储对数据对象的引用。地址的高位分别表示数据对象是否被锁定、可清除和/或来自资源文件。当 MacOS 地址在 System7中从24位升级到32位时,这导致了兼容性问题。

Https://en.wikipedia.org/wiki/tagged_pointer#examples

在 x86 _ 64 您仍然可以小心地使用高位作为标记。当然,你不需要使用所有这些16位,可以留下一些位以备将来证明

在 Mozilla Firefox 的早期版本中,他们也使用了 小整数优化算法,比如 V8,以及 3低位用于存储类型(int、 string、 object... 等等)。但是自从 JägerMonkey 之后,他们走了另一条路(Mozilla 的新 JavaScript 值表示备份连接)。该值现在总是存储在64位双精度变量中。当 double正常化时,它可以直接用于计算。然而,如果它的高16位都是1,表示一个 NaN ,低32位将存储地址(在一个32位计算机)的值或值直接,其余16位将用于存储类型。这种技术被称为 NaN-box或双节棍。它也用于64位 WebKit 的 JavaScriptCore 和 Mozilla 的 SpiderMonkey,指针以低于48位的形式存储。如果您的主数据类型是浮点数,那么这是最好的解决方案,并提供非常好的性能。

阅读更多关于以上技巧的内容: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations