Bagaimana tipe data campuran (int, float, char, dll) disimpan dalam array?

145

Saya ingin menyimpan tipe data campuran dalam array. Bagaimana seseorang bisa melakukan itu?

chanzerre
sumber
8
Itu mungkin dan ada kasus penggunaan, tetapi ini mungkin merupakan desain yang cacat. Bukan untuk itu array.
djechlin

Jawaban:

244

Anda dapat membuat elemen-elemen array sebagai kesatuan yang terdiskriminasi, alias yang ditandai .

struct {
    enum { is_int, is_float, is_char } type;
    union {
        int ival;
        float fval;
        char cval;
    } val;
} my_array[10];

The typeanggota digunakan untuk menyimpan pilihan yang anggota unionyang harus digunakan untuk setiap elemen array. Jadi, jika Anda ingin menyimpan intelemen pertama, Anda harus:

my_array[0].type = is_int;
my_array[0].val.ival = 3;

Ketika Anda ingin mengakses elemen array, Anda harus terlebih dahulu memeriksa jenisnya, lalu menggunakan anggota serikat yang sesuai. Sebuah switchpernyataan berguna:

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
}

Terserah kepada programmer untuk memastikan bahwa typeanggota selalu sesuai dengan nilai terakhir yang disimpan di union.

Barmar
sumber
23
+1 Ini adalah maksud dari banyak bahasa penafsiran yang ditulis dalam C
texasbruce
8
@texasbruce juga disebut "tagged union". Saya menggunakan teknik ini juga dalam bahasa saya sendiri. ;)
Wikipedia menggunakan halaman disambiguasi untuk " union terdiskriminasi " - "disjoint union" dalam teori himpunan dan, seperti yang disebutkan @ H2CO3, "tagged union" dalam ilmu komputer.
Izkata
14
Dan baris pertama dari halaman serikat Tagged Wikipedia mengatakan: Dalam ilmu komputer, serikat tagged, juga disebut varian, catatan varian, serikat terdiskriminasi, serikat terputus-putus, atau tipe penjumlahan, ... Ini telah diciptakan kembali berkali-kali karena memiliki banyak nama (jenis kamus seperti, hash, array asosiatif, dll.).
Barmar
1
@Barmar Saya telah menulis ulang sebagai "tagged union" tetapi kemudian membaca komentar Anda. Putar kembali hasil edit, saya tidak bermaksud merusak jawaban Anda.
32

Gunakan serikat pekerja:

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

Anda harus melacak tipe setiap elemen.


sumber
21

Elemen array harus memiliki ukuran yang sama, itu sebabnya itu tidak mungkin. Anda bisa mengatasinya dengan membuat jenis varian :

#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;
}

Ukuran elemen penyatuan adalah ukuran elemen terbesar, 4.


sumber
8

Ada gaya berbeda dalam mendefinisikan tag-union (dengan nama apa pun) yang membuat IMO jauh lebih baik untuk digunakan , dengan menghapus union internal. Ini adalah gaya yang digunakan dalam Sistem X Window untuk hal-hal seperti Acara.

Contoh dalam jawaban Barmar memberi nama valpada persatuan internal. Contoh dalam jawaban Sp. menggunakan serikat anonim untuk menghindari keharusan menentukan .val.setiap kali Anda mengakses catatan varian. Sayangnya, struktur dan serikat internal "anonim" tidak tersedia di C89 atau C99. Ini adalah ekstensi kompiler, dan karenanya bersifat non-portabel.

Cara IMO yang lebih baik adalah membalikkan seluruh definisi. Buat setiap data mengetik struct sendiri, dan menempatkan tag (type specifier) ​​ke dalam masing-masing struct.

typedef struct {
    int tag;
    int val;
} integer;

typedef struct {
    int tag;
    float val;
} real;

Kemudian Anda bungkus ini dalam serikat tingkat atas.

typedef union {
    int tag;
    integer int_;
    real real_;
} record;

enum types { INVALID, INT, REAL };

Sekarang mungkin terlihat bahwa kita mengulangi diri kita sendiri, dan memang begitu . Tetapi pertimbangkan bahwa definisi ini cenderung terisolasi untuk satu file. Tapi kami telah menghilangkan suara dari menentukan perantara .val.sebelum Anda sampai ke data.

record i;
i.tag = INT;
i.int_.val = 12;

record r;
r.tag = REAL;
r.real_.val = 57.0;

Sebaliknya, itu terjadi pada akhirnya, di mana itu kurang menjengkelkan. : D

Hal lain yang memungkinkan adalah bentuk warisan. Sunting: bagian ini bukan standar C, tetapi menggunakan ekstensi 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;

Up-casting dan down-casting.


Sunting: Satu hal yang perlu diperhatikan adalah jika Anda membuat salah satunya dengan inisialisasi C99 yang ditunjuk. Semua inisialisasi anggota harus melalui anggota serikat yang sama.

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

problem.tag; // may not be initialized

The .taginitializer dapat diabaikan oleh compiler mengoptimalkan, karena .int_initializer yang mengikuti alias area data yang sama. Meskipun kita tahu tata letak (!), Dan itu harusnya ok. Tidak, bukan itu. Gunakan tag "internal" sebagai gantinya (itu menutupi tag luar, seperti yang kita inginkan, tetapi tidak membingungkan kompiler).

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

not_a_problem.tag; // == INT
luser droog
sumber
.int_.valtidak alias area yang sama karena kompiler tahu bahwa .valada di offset yang lebih besar daripada .tag. Apakah Anda punya tautan ke diskusi lebih lanjut tentang masalah yang dituduhkan ini?
MM
5

Anda dapat melakukan void *larik, dengan larik terpisah size_t.Tapi Anda kehilangan tipe informasinya.
Jika Anda perlu menyimpan tipe informasi dengan cara tertentu, simpan array int ketiga (di mana int adalah nilai yang disebutkan) Kemudian kode fungsi yang digunakan tergantung pada enumnilainya.

dzada
sumber
Anda juga dapat menyimpan informasi jenis dalam pointer itu sendiri
phuclv
3

Serikat pekerja adalah cara standar untuk maju. Tetapi Anda memiliki solusi lain juga. Salah satunya adalah penunjuk yang ditandai , yang melibatkan penyimpanan lebih banyak informasi dalam bit "bebas" dari penunjuk.

Bergantung pada arsitektur, Anda dapat menggunakan bit rendah atau tinggi, tetapi cara teraman dan paling portabel adalah menggunakan bit rendah yang tidak digunakan dengan mengambil keuntungan dari memori selaras. Misalnya dalam sistem 32-bit dan 64-bit, pointer ke intharus kelipatan 4 (dengan asumsi intadalah tipe 32-bit) dan 2 bit paling tidak signifikan harus 0, maka Anda dapat menggunakannya untuk menyimpan jenis nilai Anda . Tentu saja Anda perlu menghapus bit tag sebelum menentukan pointer. Misalnya jika tipe data Anda terbatas pada 4 jenis yang berbeda maka Anda dapat menggunakannya seperti di bawah ini

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;
}

Jika Anda dapat memastikan bahwa data selaras 8-byte (seperti untuk pointer dalam sistem 64-bit, atau long longdan uint64_t...), Anda akan memiliki satu bit lagi untuk tag.

Ini memiliki satu kelemahan yaitu Anda akan membutuhkan lebih banyak memori jika data belum disimpan dalam variabel di tempat lain. Oleh karena itu jika jenis dan jangkauan data Anda terbatas, Anda dapat menyimpan nilai-nilai secara langsung di pointer. Teknik ini telah digunakan dalam versi 32-bit dari mesin V8 Chrome , di mana ia memeriksa bit paling tidak signifikan dari alamat untuk melihat apakah itu adalah penunjuk ke objek lain (seperti bilangan bulat, bilangan bulat besar, string atau beberapa objek) atau 31 -ditandatangani nilai (disebut smi- integer kecil ). Jika itu adalah int, Chrome hanya melakukan aritmatika dengan menggeser 1 bit untuk mendapatkan nilai, jika tidak, penunjuk akan ditinjau ulang.


Pada kebanyakan sistem 64-bit saat ini, ruang alamat virtual masih jauh lebih sempit daripada 64 bit, oleh karena itu bit yang paling signifikan juga dapat digunakan sebagai tag . Bergantung pada arsitekturnya, Anda memiliki berbagai cara untuk menggunakannya sebagai tag. ARM , 68k , dan banyak lainnya dapat dikonfigurasi untuk mengabaikan bit teratas , memungkinkan Anda untuk menggunakannya secara bebas tanpa khawatir tentang segfault atau apa pun. Dari artikel Wikipedia yang ditautkan di atas:

Contoh signifikan dari penggunaan penunjuk yang ditandai adalah runtime Objective-C pada iOS 7 pada ARM64, terutama digunakan pada iPhone 5S. Di iOS 7, alamat virtual adalah 33 bit (byte-aligned), jadi alamat word-aligned hanya menggunakan 30 bit (3 bit paling signifikan adalah 0), meninggalkan 34 bit untuk tag. Pointer kelas Objective-C selaras kata, dan bidang tag digunakan untuk banyak tujuan, seperti menyimpan jumlah referensi dan apakah objek memiliki destruktor.

Versi awal MacOS menggunakan alamat yang ditandai yang disebut Menangani untuk menyimpan referensi ke objek data. Bit alamat yang tinggi mengindikasikan apakah objek data dikunci, dapat purgeable, dan / atau berasal dari file sumber daya, masing-masing. Ini menyebabkan masalah kompatibilitas ketika pengalamatan MacOS maju dari 24 bit ke 32 bit di Sistem 7.

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

Pada x86_64 Anda masih dapat menggunakan bit tinggi sebagai tag dengan hati-hati . Tentu saja Anda tidak perlu menggunakan semua 16 bit itu dan dapat meninggalkan beberapa bit untuk bukti di masa depan

Dalam versi sebelumnya Mozilla Firefox mereka juga menggunakan optimisasi integer kecil seperti V8, dengan 3 bit rendah yang digunakan untuk menyimpan tipe (int, string, objek ... dll). Tetapi sejak JägerMonkey mereka mengambil jalur lain ( Representasi Nilai JavaScript Baru Mozilla , tautan cadangan ). Nilai sekarang selalu disimpan dalam variabel presisi ganda 64-bit. Ketika doubleadalah dinormalisasi satu, dapat digunakan secara langsung dalam perhitungan. Namun jika 16 bit tinggi semuanya 1s, yang menunjukkan NaN , 32 bit rendah akan menyimpan alamat (di komputer 32-bit) ke nilai atau nilai secara langsung, 16-bit sisanya akan digunakan untuk menyimpan tipenya. Teknik ini disebut NaN-tinjuatau nun-tinju. Ini juga digunakan dalam JavaScriptCore WebKit 64-bit dan SpiderMonkey Mozilla dengan pointer disimpan dalam bit 48 yang rendah. Jika tipe data utama Anda adalah floating-point, ini adalah solusi terbaik dan memberikan kinerja yang sangat baik.

Baca lebih lanjut tentang teknik di atas: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

phuclv
sumber