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

154
我想在一个数组中存储不同的数据类型。有什么方法可以做到这一点?

11
可以实现并且有使用情况,但这可能是一个有缺陷的设计。这不是数组的本意所在。 - djechlin
6个回答

253
你可以将数组元素设为区分联合体,也称为标记联合
struct {
    enum { is_int, is_float, is_char } type;
    union {
        int ival;
        float fval;
        char cval;
    } val;
} my_array[10];
< p > 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中存储的最后一个值。


29
这是用C语言编写的许多解释型语言的实现。+1 - SwiftMango
8
@texasbruce也称为“标记联合”。我在自己的语言中也使用了这种技术。 ;) - user529758
维基百科使用消歧页面来解释“discriminated union” - 集合论中的“disjoint union”,以及如@H2CO3所提到的计算机科学中的“tagged union”。 - Izkata
15
维基百科标记联合页面的第一行写道:在计算机科学中,标记联合(tagged union),也称为变体(variant)、变体记录(variant record)、判别式联合(discriminated union)、不相交联合(disjoint union)或和类型(sum type)...。它已经被反复发明了许多次,因此有很多名称(就像字典、哈希表、关联数组等)。 - Barmar
1
@Barmar 我已将其重写为“标记联合”,但随后阅读了您的评论。回滚了编辑,我并不是想破坏您的答案。 - user529758
显示剩余2条评论

33

使用联合:

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

有一种不同风格的定义标签联合(无论叫什么名字)的方法,我认为它通过去除内部联合使得使用更加美好。这是X Window系统中用于事件等事情的样式。

Barmar回答中的示例将名称val赋予内部联合。Sp.的回答中使用匿名联合来避免每次访问变体记录时都需要指定.val.。不幸的是,“匿名”内部结构和联合在C89或C99中不可用。它是一个编译器扩展,因此固有的不可移植性。

我认为更好的方法是颠倒整个定义。使每个数据类型都成为自己的结构,并将标签(类型说明符)放入每个结构中。

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

.int_.val并不与相同的区域别名,因为编译器知道.val的偏移量大于.tag。你有关于这个所谓问题的进一步讨论链接吗? - M.M

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

你也可以将类型信息存储在指针本身中。 - phuclv

4
联合是标准方法。但您也有其他解决方案。其中之一是标记指针,它涉及在指针的"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 longuint64_t...),那么您将有一个标记的额外位。这样做的一个缺点是,如果数据没有在其他地方存储在变量中,您需要更多的内存。因此,如果您的数据类型和范围有限,则可以直接将值存储在指针中。这种技术已经在32位版本的Chrome V8引擎中使用,其中它检查地址的最低有效位,以查看是否为指向另一个对象(如double、大整数、字符串或某个对象)的指针,还是31位有符号值(称为smi - small integer)。如果它是一个int,Chrome只需进行算术右移1位即可获得该值,否则将解除引用指针。
在大多数当前的64位系统上,虚拟地址空间仍然比64位窄得多,因此高位最重要的位也可以用作标记。根据体系结构,您有不同的方法来使用它们作为标记。 ARM68k和许多其他体系结构可以配置为忽略顶部位,允许您自由使用它们而不必担心segfault或任何其他问题。从上面链接的维基百科文章中:
iOS 7上ARM64架构中Objective-C runtime的显著例子是使用了标记指针,特别是在iPhone 5S上。在iOS 7中,虚拟地址为33位(按字节对齐),因此只有30位用于按字对齐的地址(3个最低有效位为0),剩下的34位用于标记。Objective-C类指针按字对齐,并且标记字段用于许多目的,例如存储引用计数以及对象是否具有析构函数。
早期版本的MacOS使用称为“句柄”的带标记地址来存储数据对象的引用。地址的高位指示数据对象是否已锁定,可清除,和/或源自资源文件。当MacOS从System 7的24位进阶到32位时,这导致兼容性问题。
在x86_64上,您仍然可以谨慎地使用高位作为标记。当然,您不需要使用所有16位,并且可以留出一些位用于未来的保护。 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 Representationbackup 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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接