Как смешанные типы данных (int, float, char и т. Д.) Могут храниться в массиве?


145

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


8
Это возможно, и есть варианты использования, но это, вероятно, некорректный дизайн. Это не то, для чего массивы.
Джехлин

Ответы:


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, «тегового объединения» в информатике.
Изката

14
И в первой строке страницы объединения с тегами Wikipedia написано: В информатике - объединение с тегами, также называемое вариантом, записью варианта, дискриминационным объединением, непересекающимся объединением или типом суммы ... Это было многократно переизобретено, у него много имена (вроде словари, хэши, ассоциативные массивы и т. д.).
Бармар

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 System для таких вещей, как События.

Пример в ответе Бармара дает название 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

Другая вещь, которую это позволяет, является формой наследования. Редактировать: эта часть не стандартная Си, но использует расширение 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. У вас есть ссылка для дальнейшего обсуждения этой предполагаемой проблемы?
ММ

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 longи uint64_t...), у вас будет еще один бит для тега.

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


В большинстве современных 64-разрядных систем виртуальное адресное пространство все еще намного уже 64-разрядных, поэтому старшие старшие биты также можно использовать в качестве тегов . В зависимости от архитектуры у вас есть разные способы использовать их в качестве тегов. ARM , 68k и многие другие могут быть сконфигурированы так, чтобы игнорировать верхние биты , что позволяет вам свободно использовать их, не беспокоясь о segfault или чем-либо еще. Из приведенной выше статьи в Википедии:

Важным примером использования теговых указателей является среда выполнения Objective C на iOS 7 на ARM64, особенно используемая на iPhone 5S. В iOS 7 виртуальные адреса имеют размер 33 бита (с байтовым выравниванием), поэтому адреса с выравниванием по словам используют только 30 бит (3 младших бита равны 0), оставляя 34 бита для тегов. Указатели класса Objective-C выровнены по словам, а поля тегов используются для многих целей, таких как хранение счетчика ссылок и наличие в объекте деструктора.

Ранние версии MacOS использовали теговые адреса, называемые дескрипторами, для хранения ссылок на объекты данных. Старшие биты адреса указывали, был ли объект данных заблокирован, очищен и / или создан из файла ресурсов, соответственно. Это вызвало проблемы совместимости, когда адресация MacOS увеличилась с 24 бит до 32 бит в System 7.

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

На x86_64 вы все равно можете осторожно использовать старшие биты в качестве тегов . Конечно, вам не нужно использовать все эти 16 битов, и вы можете оставить некоторые биты для будущего

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

Узнайте больше о вышеупомянутых методах: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.