Як змішані типи даних (int, float, char тощо) можуть зберігатися в масиві?


145

Я хочу зберігати змішані типи даних у масиві. Як можна було це зробити?


8
Це можливо і є випадки використання, але це, швидше за все, недосконала конструкція. Це не для чого масиви.
djechlin

Відповіді:


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
І перший рядок сторінки спілки з тегами Вікіпедії говорить про те, що в інформатиці - це позначений союз, який також називають варіантом, варіантом запису, дискримінаційним союзом, роз'єднаним союзом або типом суми, ... Він був заново винайдений стільки разів, що має багато назви (схожі на словники, хеші, асоціативні масиви тощо).
Бармар

1
@Barmar Я переписав це як "позначений союз", але потім прочитав ваш коментар. Я відмовив відмовитись від редагування, я не хотів скасувати вашу відповідь.

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 для таких речей, як Події.

Приклад у відповіді Бармара дає назву valвнутрішньому союзу. У прикладі у відповіді Сп. Використовується анонімне об'єднання, щоб уникнути необхідності вказувати .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

.int_.valне має псевдоніму тієї самої області, хоча тому, що компілятор знає, що .valце більше, ніж зміщення .tag. У вас є посилання для подальшого обговорення цієї передбачуваної проблеми?
ММ

5

Можна зробити void *масив із відокремленим масивом size_t.Але ви втрачаєте тип інформації.
Якщо вам потрібно якось зберегти тип інформації, збережіть третій масив int (де int - перелічене значення), а потім кодуйте функцію, яка відтворює залежно від enumзначення.


Ви також можете зберігати інформацію про тип у самому покажчику
phuclv

3

Союз - це стандартний шлях. Але у вас є й інші рішення. Одним із таких є позначений покажчик , який передбачає збереження додаткової інформації у "вільних" бітах вказівника.

Залежно від архітектури, ви можете використовувати низькі або високі біти, але найбезпечнішим і найбільш портативним способом є використання невикористаних низьких біт , скориставшись перевагою вирівняної пам'яті. Наприклад, у 32-бітних та 64-бітних системах вказівники intповинні бути кратними 4 (припустимо int, що це 32-розрядний тип), а два найменш значущі біти повинні бути 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-бітній версії двигуна V8 Chrome , де вона перевіряє найменш значущий біт адреси, щоб побачити, чи це вказівник на інший об’єкт (наприклад, подвійні, великі цілі числа, рядок чи якийсь об’єкт) або 31 -бітове підписане значення (називається smi- мале ціле число ). Якщо це an 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 використовували мічені адреси під назвою Handles для зберігання посилань на об'єкти даних. Високі біти адреси вказували на те, чи був об'єкт даних заблокованим, очищеним та / або походить відповідно з файлу ресурсів. Це спричинило проблеми з сумісністю, коли адресація 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-боксабо монахині. Він також використовується в 64-розрядних WebKit JavaScriptCore і Mozilla's SpiderMonkey, при цьому вказівник зберігається з низькими 48 бітами. Якщо ваш основний тип даних є плаваючою комою, це найкраще рішення і забезпечує дуже хороші показники.

Детальніше про вищезазначені методи читайте: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.