Jawaban:
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 type
anggota digunakan untuk menyimpan pilihan yang anggota union
yang harus digunakan untuk setiap elemen array. Jadi, jika Anda ingin menyimpan int
elemen 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 switch
pernyataan 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 type
anggota selalu sesuai dengan nilai terakhir yang disimpan di union
.
Gunakan serikat pekerja:
union {
int ival;
float fval;
void *pval;
} array[10];
Anda harus melacak tipe setiap elemen.
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.
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 val
pada 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 .tag
initializer 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
.int_.val
tidak alias area yang sama karena kompiler tahu bahwa .val
ada di offset yang lebih besar daripada .tag
. Apakah Anda punya tautan ke diskusi lebih lanjut tentang masalah yang dituduhkan ini?
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 enum
nilainya.
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 int
harus kelipatan 4 (dengan asumsi int
adalah 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 long
dan 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.
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 double
adalah 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