Я хочу хранить смешанные типы данных в массиве. Как это можно сделать?
Как можно хранить смешанный тип данных (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