150

我想将混合数据类型存储在一个数组中。怎么可能做到这一点?

4

6 回答 6

249

您可以使数组元素成为可区分的联合,即标记的联合

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.

于 2013-09-02T16:31:58.443 回答
33

使用联合:

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

不过,您必须跟踪每个元素的类型。

于 2013-09-02T16:28:50.053 回答
21

数组元素需要具有相同的大小,这就是不可能的原因。您可以通过创建变体类型来解决它:

#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。

于 2013-09-02T16:47:49.230 回答
8

通过删除内部联合,IMO 可以更好地使用定义标签联合(无论名称)的不同风格。这是 X 窗口系统中用于事件之类的样式。

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
于 2013-09-03T06:24:07.893 回答
5

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

于 2013-09-02T16:28:20.597 回答
3

联合是标准的方式。但是您也有其他解决方案。其中之一是标记的指针,它涉及将更多信息存储在指针的“空闲”位中。

根据体系结构,您可以使用低位或高位,但最安全和最便携的方法是通过利用对齐内存来使用未使用的低位。例如在 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),您将多一位用于标记。

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


在大多数当前的 64 位系统上,虚拟地址空间仍然比 64 位窄得多,因此最高有效位也可以用作标记。根据体系结构,您有不同的方式将它们用作标签。ARM , 68k和许多其他可以配置为忽略最高位,让您可以自由使用它们而不必担心段错误或任何事情。从上面链接的维基百科文章中:

使用标记指针的一个重要示例是 ARM64 上 iOS 7 上的 Objective-C 运行时,尤其是在 iPhone 5S 上使用。在 iOS 7 中,虚拟地址是 33 位(字节对齐),所以字对齐地址只使用 30 位(3 个最低有效位为 0),剩下 34 位用于标记。Objective-C 类指针是字对齐的,标签字段有多种用途,例如存储引用计数以及对象是否具有析构函数。

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

https://en.wikipedia.org/wiki/Tagged_pointer#Examples

在 x86_64 上,您仍然可以小心地使用高位作为标记。当然,您不需要使用所有这 16 位,并且可以省略一些位以供将来证明

在 Mozilla Firefox 的早期版本中,它们还使用像 V8 这样的小整数优化,其中3 个低位用于存储类型(int、string、object...等)。但自从 JägerMonkey 他们走上了另一条路(Mozilla's New JavaScript Value Representation备份链接)。该值现在始终存储在 64 位双精度变量中。当double标准化的时,它可以直接用于计算。但是,如果它的高 16 位全为 1,表示NaN,则低 32 位将地址(在 32 位计算机中)存储到值或直接值,其余 16 位将被使用存储类型。这种技术称为NaN-boxing或修女拳击。它也用于 64 位 WebKit 的 JavaScriptCore 和 Mozilla 的 SpiderMonkey,指针存储在低 48 位中。如果您的主要数据类型是浮点数,这是最好的解决方案,并且提供了非常好的性能。

阅读有关上述技术的更多信息:https ://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

于 2015-07-19T18:15:09.023 回答