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


145

我想在数组中存储混合数据类型。怎么可以这样呢?


8
有可能并且有用例,但这可能是一个有缺陷的设计。那不是数组的目的。
djechlin 2013年

Answers:


244

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

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


23
+1这是许多用C语言编写的解释语言的实现
texasbruce

8
@texasbruce也称为“标记联合”。我也在以自己的语言使用这项技术。;)

维基百科在集合论中使用“歧义联合 ”-“不相交联合” 的歧义页面,而@ H2CO3则在计算机科学中使用“标记联合”。
Izkata

14
维基百科带有标记的联合页面的第一行说:在计算机科学中,带标记的联合,也称为变体,变体记录,有区别的联合,不相交的联合或求和类型,...已经被重塑了很多次,名称(类似字典,散列,关联数组等)。
Barmar

1
@Barmar我将其重写为“ tagged union”,但随后阅读了您的评论。撤消编辑,我不是故意破坏您的答案。

32

使用联合:

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

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


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。


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_后面初始化别名相同的数据区域。即使我们知道布局(!),也应该可以。不,不是。请改用“内部”标记(就像我们想要的那样,它覆盖外部标记,但不会混淆编译器)。

record not_a_problem = { .int_.tag = INT, .int_.val = 3 };

not_a_problem.tag; // == INT

.int_.val不会在同一区域使用别名,因为编译器知道.val偏移量比更大.tag。您是否有链接可以进一步讨论这个所谓的问题?
MM

5

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


您还可以将类型信息存储在指针本身中
phuclv

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 longand uint64_t...),则标签将再有一位。

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


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

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

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

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

在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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.