Как можно хранить смешанный тип данных (int, float, char и т.д.) В массиве?

Я хочу хранить смешанные типы данных в массиве. Как это можно сделать?

Ответ 1

Вы можете сделать элементы массива дискриминационным объединением, aka 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;

Когда вы хотите получить доступ к элементу массива, вы должны сначала проверить тип, а затем использовать соответствующий член объединения. Оператор 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.

Ответ 2

Используйте объединение:

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

Однако вам нужно будет отслеживать тип каждого элемента.

Ответ 3

Элементы массива должны иметь одинаковый размер, поэтому это невозможно. Вы можете обойти это, создав тип варианта:

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

Ответ 4

Существует другой стиль определения объединения тегов (по любому имени), который IMO делает его намного приятнее в использовании, удаляя внутренний союз. Это стиль, используемый в X Window System для таких вещей, как Events.

Пример в ответе Бармара дает имя 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_, который следует за псевдонимами в той же области данных. Хотя мы знаем макет (!), И все должно быть хорошо. Нет, это не так. Вместо этого используйте "внутренний" тег (он накладывает внешний тег так же, как мы хотим, но не путаем компилятор).

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

not_a_problem.tag; // == INT

Ответ 5

Вы можете сделать массив void * с разделенным массивом size_t. Но вы потеряете тип информации.
Если вам нужно сохранить тип информации каким-то образом, сохраните третий массив int (где int - это перечислимое значение). Затем закодируйте функцию, которая выполняется в зависимости от значения enum.

Ответ 6

Союз - это стандартный способ. Но у вас есть и другие решения.

Один элемент отмеченный указатель. Это использует преимущество выровненной памяти, где низкие биты адресов всегда равны нулю. Например, в 32-битных системах указатели на int должны быть кратными 4, а младшие 2 бита должны быть равны 0, поэтому вы можете использовать его для хранения типа ваших значений. Конечно, вам нужно очистить бит до разыменования значений.

void* tp; // tagged pointer
enum { is_int, is_double, is_char_p, is_char } type;
// ...
intptr_t addr = (intptr_t)tp & ~0x03; // clear the 2 low bits in the pointer
switch ((intptr_t)tp & 0x03) // check the 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-битных систем виртуальный адрес еще 48 бит, поэтому высокие 16 бит также могут использоваться как теги.

У этого есть одно disavantage, что вам потребуется больше памяти, если данные не были сохранены нигде. Поэтому, если тип и диапазон ваших данных ограничен, вы можете сохранить значения непосредственно в указателе. Это было использовано в движке Chrome V8, где он проверяет наименее значащий бит адреса, чтобы увидеть, является ли этот указатель двойным или 31-битовым знаковым значением (называемым smi-small integer). Если это int, Chrome просто выполняет арифметический сдвиг вправо 1 бит, чтобы получить значение, иначе указатель будет разыменован.

В предыдущих версиях Mozilla Firefox они также использовали небольшие целые оптимизации, такие как V8, с тремя младшими битами, используемыми для хранения типа (int, string, object... и т.д.). Но поскольку JaegerMonkey они взяли другой путь (Новое представление стоимости JavaScript Mozilla New). Значение теперь всегда сохраняется в 64-битной переменной двойной точности. Когда double является нормированным, его можно использовать непосредственно в вычислениях. Однако, если высокие 16 бит из них - все 1s, которые обозначают NaN, низкие 32 бита будут хранить адрес (в 32-разрядном компьютере) до значения или значения напрямую, остальные 16 бит будут использоваться для хранения типа. Этот метод называется NaN-бокс. Если ваш основной тип данных является плавающей точкой, это лучшее решение и обеспечивает очень хорошую производительность. В 64-битных машинах он также может использоваться, поскольку адрес часто содержит только 48 бит, как указано выше.

Подробнее об этих методах: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations