Wie kann ein gemischter Datentyp (int, float, char usw.) in einem Array gespeichert werden?

Ich möchte gemischte Datentypen in einem Array speichern. Wie könnte man das tun?

Sie können die Array-Elemente zu einer diskriminierten Vereinigung machen, auch bekannt als Union .

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

Der type member wird verwendet, um die Auswahl zu halten, welches Mitglied der union für jedes Array-Element verwendet werden soll. Wenn Sie also ein int im ersten Element speichern möchten, tun Sie Folgendes:

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

Wenn Sie auf ein Element des Arrays zugreifen möchten, müssen Sie zunächst den Typ überprüfen und dann das entsprechende Mitglied der Union verwenden. Eine switch statement ist nützlich:

 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 } 

Es ist dem Programmierer überlassen, sicherzustellen, dass der type member immer dem letzten in der union gespeicherten Wert entspricht.

Verwenden Sie eine Union:

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

Sie müssen jedoch den Typ jedes Elements im Auge behalten.

Array-Elemente müssen dieselbe Größe haben, deshalb ist dies nicht möglich. Sie könnten dies umgehen, indem Sie einen Variantentyp erstellen:

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

Die Größe des Elements der Union ist die Größe des größten Elements 4.

Es gibt eine andere Art, die Tag-Union (mit welchem ​​Namen auch immer) zu definieren, die es IMO viel angenehmer macht, indem die interne Union entfernt wird. Dies ist der Stil, der im X Window System für Dinge wie Ereignisse verwendet wird.

Das Beispiel in Barmars Antwort gibt der internen Vereinigung den Namen val . Das Beispiel in der Antwort von Sp. Verwendet eine anonyme Vereinigung, um zu vermeiden, dass .val. angegeben werden .val. jedes Mal, wenn Sie auf den Variantensatz zugreifen. Leider sind “anonyme” interne Strukturen und Vereinigungen in C89 oder C99 nicht verfügbar. Es ist eine Compiler-Erweiterung und daher von Natur aus nicht portabel.

Ein besserer Weg IMO ist es, die gesamte Definition umzukehren. Machen Sie jeden Datentyp zu seiner eigenen Struktur und fügen Sie das Tag (Typspezifizierer) in jede Struktur ein.

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

Dann wickeln Sie diese in eine Top-Level-Union ein.

 typedef union { int tag; integer int_; real real_; } record; enum types { INVALID, INT, REAL }; 

Jetzt mag es so aussehen, als würden wir uns wiederholen, und wir sind es auch . Bedenken Sie jedoch, dass diese Definition wahrscheinlich in einer einzelnen Datei isoliert ist. Aber wir haben den Lärm eliminiert, der das Intermediate .val. bevor Sie zu den Daten kommen.

 record i; i.tag = INT; i.int_.val = 12; record r; r.tag = REAL; r.real_.val = 57.0; 

Stattdessen geht es am Ende, wo es weniger anstößig ist. : D

Eine andere Sache, die dies ermöglicht, ist eine Form der inheritance. Edit: Dieser Teil ist nicht Standard C, sondern verwendet eine GNU-Erweiterung.

 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 und Down-Casting.


Edit: Eine Sache, die Sie beachten sollten, ist, wenn Sie eine davon mit C99-Initialisatoren konstruieren. Alle Mitgliedsinitialisierer sollten über dasselbe Gewerkschaftsmitglied verfügen.

 record problem = { .tag = INT, .int_.val = 3 }; problem.tag; // may not be initialized 

Der .tag Initialisierer kann von einem optimierenden Compiler ignoriert werden, da der .int_ initializer .int_ den gleichen Datenbereich verwendet. Obwohl wir das Layout (!) Kennen, und es sollte in Ordnung sein. Nein, ist es nicht. Verwenden Sie stattdessen das “interne” -Tag (es überlagert das äußere Tag, genau wie wir wollen, aber den Compiler nicht verwirren).

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

Sie können ein void * -Array mit einem getrennten Array von size_t. Aber du verlierst den Informationstyp.
Wenn Sie den Informationstyp irgendwie beibehalten müssen, behalten Sie ein drittes Array von int (wobei das int ein Aufzählungswert ist). Dann kodieren Sie die function, die abhängig vom enum .

Union ist der Standardweg. Aber Sie haben auch andere Lösungen. Einer davon ist der markierte pointers , bei dem mehr Informationen in den “freien” Bits eines pointerss gespeichert werden.

Abhängig von den Architekturen können Sie die niedrigen oder hohen Bits verwenden, aber die sicherste und portabelste Methode ist die Verwendung der unbenutzten niedrigen Bits, indem der Vorteil des ausgerichteten Speichers ausgenutzt wird. In 32-Bit- und 64-Bit-Systemen müssen pointers auf int beispielsweise ein Vielfaches von 4 sein und die 2 niedrigstwertigen Bits müssen 0 sein. Daher können Sie sie verwenden, um den Typ Ihrer Werte zu speichern. Natürlich müssen Sie die Tag-Bits löschen, bevor Sie den pointers dereferenzieren. Wenn Ihr Datentyp beispielsweise auf 4 verschiedene Typen beschränkt ist, können Sie ihn wie folgt verwenden

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

Wenn Sie sicherstellen können, dass die Daten 8-Byte-ausgerichtet sind (wie für pointers in 64-Bit-Systemen oder long long und uint64_t …), haben Sie ein weiteres Bit für das Tag.

Dies hat den Nachteil, dass Sie mehr Speicher benötigen, wenn die Daten nicht an einer anderen Stelle gespeichert wurden. Falls Art und scope Ihrer Daten begrenzt sind, können Sie die Werte direkt im pointers speichern. Diese Technik wurde in der 32-Bit-Version von Chrome’s V8-Engine verwendet , wo sie das niedrigstwertige Bit der Adresse überprüft, um zu sehen, ob das ein pointers auf ein anderes Objekt (wie Double, Big Integer, String oder irgendein Objekt) oder 31 ist -bit signierter Wert ( smi – small integer ). Wenn es ein int , macht Chrome einfach eine arithmetische Verschiebung um 1 Bit, um den Wert zu erhalten, ansonsten wird der pointers dereferenziert.


Bei den meisten aktuellen 64-Bit-Systemen ist der virtuelle Adressraum immer noch viel kleiner als 64 Bits, daher können die höchstwertigen Bits auch als Tags verwendet werden . Je nach Architektur haben Sie verschiedene Möglichkeiten, diese als Tags zu verwenden. ARM , 68k und viele andere erlauben es Ihnen, die oberen Bits zu ignorieren, so dass Sie sie frei verwenden können, ohne sich um segfault oder irgendetwas zu kümmern. Aus dem verlinkten Wikipedia-Artikel oben:

Ein bedeutendes Beispiel für die Verwendung von getaggten pointersn ist die Objective-C-Laufzeit auf iOS 7 auf ARM64, die insbesondere auf dem iPhone 5S verwendet wird. In iOS 7 sind virtuelle Adressen 33 Bits (Byte-ausgerichtet), so dass wortausgerichtete Adressen nur 30 Bits verwenden (3 niedrigstwertige Bits sind 0), wobei 34 Bits für Tags übrig bleiben. Objective-C-classnzeiger sind wortausgerichtet, und die Tag-Felder werden für viele Zwecke verwendet, z. B. zum Speichern einer Referenzzahl und ob das Objekt einen Destruktor hat. [2] [3]

Frühe Versionen von MacOS verwendeten markierte Adressen mit dem Namen Handles, um Verweise auf Datenobjekte zu speichern. Die hohen Bits der Adresse zeigten an, ob das Datenobjekt jeweils gesperrt, geleert und / oder aus einer Ressourcedatei stammte. Dies führte zu Kompatibilitätsproblemen, wenn die MacOS-Adressierung in System 7 von 24 Bit auf 32 Bit erweitert wurde.

Auf x86_64 können Sie immer noch die hohen Bits als Tags verwenden . Natürlich müssen Sie nicht alle diese 16 Bits verwenden und können einige Bits für zukünftige Beweise lassen

In früheren Versionen von Mozilla Firefox verwenden sie auch kleine Integer-Optimierungen wie V8, wobei die 3 unteren Bits den Typ (int, string, object usw.) speichern . Aber seit JägerMonkey haben sie einen anderen Weg eingeschlagen ( Mozilla’s New JavaScript Value Representation , Backup-Link ). Der Wert wird jetzt immer in einer 64-Bit-Variablen mit doppelter Genauigkeit gespeichert. Wenn das double ein normalisiertes ist , kann es direkt in Berechnungen verwendet werden. Wenn jedoch die hohen 16 Bits alle 1 sind, was ein NaN bezeichnet , werden die niedrigen 32 Bits die Adresse (in einem 32-Bit-Computer) direkt auf den Wert oder den Wert speichern, die verbleibenden 16 Bits werden verwendet um den Typ zu speichern. Diese Technik wird NaN-Boxen oder Nonne-Boxen genannt. Es wird auch im 64-Bit-JavaScript-Core von WebKit und im Mozilla-SpiderMonkey verwendet, wobei der pointers in den niedrigen 48 Bits gespeichert ist. Wenn Ihr Hauptdatentyp ein Gleitkomma-Datentyp ist, ist dies die beste Lösung und liefert eine sehr gute performance.

Lesen Sie mehr über die oben genannten Techniken: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations