Wie können gemischte Datentypen (int, float, char usw.) in einem Array gespeichert werden?


145

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


8
Es ist möglich und es gibt Anwendungsfälle, aber dies ist wahrscheinlich ein fehlerhaftes Design. Dafür sind Arrays nicht gedacht.
Djechlin

Antworten:


244

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

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

Das Element typewird verwendet, um die Auswahl zu halten, welches Mitglied des unionis für jedes Array-Element verwendet werden soll. Wenn Sie also ein intElement im ersten Element speichern möchten , gehen Sie wie folgt vor:

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 zuerst den Typ überprüfen und dann das entsprechende Mitglied der Union verwenden. Eine switchAussage 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 bleibt dem Programmierer überlassen, sicherzustellen, dass das typeMitglied immer dem zuletzt im union.


23
+1 Dies ist die Implementierung vieler Dolmetschersprachen, die in C
texasbruce

8
@texasbruce wird auch als "getaggte Union" bezeichnet. Ich benutze diese Technik auch in meiner eigenen Sprache. ;)

Wikipedia verwendet eine Disambiguierungsseite für " diskriminierte Vereinigung " - "disjunkte Vereinigung" in der Mengenlehre und, wie @ H2CO3 erwähnt, "markierte Vereinigung" in der Informatik.
Izkata

14
In der ersten Zeile der Wikipedia- Seite " Tagged Union " heißt es: In der Informatik wird eine Tagged Union, auch als Variante, Variantendatensatz, diskriminierte Union, disjunkte Union oder Summentyp bezeichnet, ... so oft neu erfunden, dass es viele gibt Namen (ähnlich wie Wörterbücher, Hashes, assoziative Arrays usw.).
Barmar

1
@Barmar Ich habe es als "tagged union" umgeschrieben, aber dann deinen Kommentar gelesen. Ich habe die Bearbeitung rückgängig gemacht und wollte Ihre Antwort nicht zerstören.

32

Verwenden Sie eine Gewerkschaft:

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

Sie müssen jedoch den Typ jedes Elements verfolgen.


21

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

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

Die Größe des Elements der Vereinigung entspricht der Größe des größten Elements, 4.


8

Es gibt einen anderen Stil die Tag-Vereinigung definieren (gleich welchen Namen) , die IMO es viel schöner machen Gebrauch von der internen Vereinigung zu entfernen. Dies ist der Stil, der im X Window System für Dinge wie Ereignisse verwendet wird.

Das Beispiel in Barmars Antwort gibt valder internen Vereinigung den Namen . Das Beispiel in der Antwort von Sp. Verwendet eine anonyme Vereinigung, um zu vermeiden, dass bei .val.jedem Zugriff auf den Variantendatensatz angegeben werden muss. Leider sind "anonyme" interne Strukturen und Gewerkschaften in C89 oder C99 nicht verfügbar. Es ist eine Compiler-Erweiterung und daher von Natur aus nicht portierbar.

Eine bessere Möglichkeit für IMO besteht darin, die gesamte Definition umzukehren. Machen Sie jeden Datentyp zu einer 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 Union der obersten Ebene ein.

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

enum types { INVALID, INT, REAL };

Jetzt scheint es, dass wir uns wiederholen, und wir sind es . Bedenken Sie jedoch, dass diese Definition wahrscheinlich auf eine einzelne Datei beschränkt ist. Wir haben jedoch das Rauschen bei der Angabe des Zwischenprodukts beseitigt.val. bevor Sie zu den Daten gelangen.

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 widerlich ist. : D.

Eine andere Sache, die dies erlaubt, ist eine Form der Vererbung. Bearbeiten: 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- und Downcasting.


Bearbeiten: Ein Problem, das Sie beachten sollten, ist, wenn Sie eines davon mit C99-Initialisierern erstellen. Alle Mitgliederinitialisierer sollten über dasselbe Gewerkschaftsmitglied erfolgen.

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

problem.tag; // may not be initialized

Die .tagInitialisierung kann durch einen optimierenden Compiler, weil die ignoriert werden .int_initializer die folgt Aliase der gleichen Datenbereich. Auch wenn wir das Layout (!) Kennen, und es sollte in Ordnung sein. Nein, das ist es nicht. Verwenden Sie stattdessen das "interne" Tag (es überlagert das äußere Tag, genau wie wir es wollen, verwirrt aber den Compiler nicht).

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

not_a_problem.tag; // == INT

.int_.valAlias ​​nicht derselbe Bereich, da der Compiler weiß, dass der .valVersatz größer ist als .tag. Haben Sie einen Link zur weiteren Diskussion über dieses angebliche Problem?
MM

5

Sie können ein void *Array mit einem getrennten Array von size_t.erstellen. Sie verlieren jedoch den Informationstyp.
Wenn Sie den Informationstyp auf irgendeine Weise beibehalten müssen, behalten Sie ein drittes Array von int bei (wobei int ein Aufzählungswert ist). Codieren Sie dann die Funktion, die abhängig vom enumWert umgewandelt wird.


Sie können die
Typinformationen auch

3

Union ist der Standardweg. Sie haben aber auch andere Lösungen. Einer davon ist der markierte Zeiger , bei dem mehr Informationen im "freien" Speicher gespeichert werden. Bits eines Zeigers .

Abhängig von den Architekturen können Sie die niedrigen oder hohen Bits verwenden. Am sichersten und portabelsten ist es jedoch, die nicht verwendeten niedrigen Bits zu verwenden, indem Sie den Vorteil des ausgerichteten Speichers nutzen. In 32-Bit- und 64-Bit-Systemen müssen die Zeiger auf intein Vielfaches von 4 sein (vorausgesetzt, es inthandelt sich um einen 32-Bit-Typ), und die 2 niedrigstwertigen Bits müssen 0 sein. Daher können Sie sie zum Speichern des Typs Ihrer Werte verwenden . Natürlich müssen Sie die Tag-Bits löschen, bevor Sie den Zeiger 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;
// ...
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;
}

Wenn Sie sicherstellen können, dass die Daten 8-Byte-ausgerichtet sind (wie bei Zeigern in 64-Bit-Systemen oder long longund 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 anderer Stelle in einer Variablen gespeichert wurden. Wenn daher der Typ und der Bereich Ihrer Daten begrenzt sind, können Sie die Werte direkt im Zeiger speichern. Diese Technik wurde in der 32-Bit-Version der V8-Engine von Chrome verwendet , bei der das niedrigstwertige Bit der Adresse überprüft wird, um festzustellen, ob es sich um einen Zeiger auf ein anderes Objekt (z. B. doppelte, große Ganzzahlen, Zeichenfolge oder ein Objekt) oder ein 31 handelt -bit vorzeichenbehafteter Wert (aufgerufen smi- kleine Ganzzahl ). Wenn dies der intFall ist, führt Chrome einfach eine arithmetische Rechtsverschiebung um 1 Bit durch, um den Wert zu erhalten. Andernfalls wird der Zeiger dereferenziert.


Auf den meisten aktuellen 64-Bit-Systemen ist der virtuelle Adressraum immer noch viel schmaler als 64 Bit, daher können die höchstwertigen Bits auch als Tags verwendet werden . Abhängig von der Architektur haben Sie verschiedene Möglichkeiten, diese als Tags zu verwenden. ARM , 68k und viele andere können so konfiguriert werden, dass die oberen Bits ignoriert werden , sodass Sie sie frei verwenden können, ohne sich um Segfault oder ähnliches kümmern zu müssen. Aus dem oben verlinkten Wikipedia-Artikel:

Ein wichtiges Beispiel für die Verwendung von markierten Zeigern ist die Objective-C-Laufzeit unter iOS 7 unter ARM64, die insbesondere auf dem iPhone 5S verwendet wird. In iOS 7 sind virtuelle Adressen 33 Bit (byte-ausgerichtet), sodass wortausgerichtete Adressen nur 30 Bit verwenden (3 niedrigstwertige Bits sind 0), sodass 34 Bit für Tags übrig bleiben. Objective-C-Klassenzeiger sind wortausgerichtet, und die Tag-Felder werden für viele Zwecke verwendet, z. B. zum Speichern eines Referenzzählers und zur Frage, ob das Objekt einen Destruktor hat.

Frühere Versionen von MacOS verwendeten getaggte Adressen, die als Handles bezeichnet wurden, um Verweise auf Datenobjekte zu speichern. Die hohen Bits der Adresse zeigten an, ob das Datenobjekt gesperrt, löschbar und / oder aus einer Ressourcendatei stammend war. Dies verursachte Kompatibilitätsprobleme, als die MacOS-Adressierung in System 7 von 24 Bit auf 32 Bit erweitert wurde.

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

Auf x86_64 können Sie die hohen Bits weiterhin vorsichtig als Tags verwenden . Natürlich müssen Sie nicht alle diese 16 Bits verwenden und können einige Bits für die Zukunftssicherheit weglassen

In früheren Versionen von Mozilla Firefox wurden auch kleine Ganzzahloptimierungen wie V8 verwendet, wobei die 3 niedrigen Bits zum Speichern des Typs (int, string, object ... usw.) verwendet wurden. Aber seit JägerMonkey haben sie einen anderen Weg eingeschlagen ( Mozillas neue JavaScript-Wertrepräsentation , Backup-Link ). Der Wert wird jetzt immer in einer 64-Bit-Variablen mit doppelter Genauigkeit gespeichert. Wenn die doublea normalisiert ein, kann es direkt in Berechnungen verwendet werden. Wenn jedoch die hohen 16 Bits alle 1s sind, die ein NaN bezeichnen , speichern die niedrigen 32 Bits die Adresse (in einem 32-Bit-Computer) auf den Wert oder den Wert direkt, die verbleibenden 16 Bits werden verwendet um den Typ zu speichern. Diese Technik heißt NaN-Boxenoder Nonnenboxen. Es wird auch in JavaScriptCore von 64-Bit-WebKit und SpiderMonkey von Mozilla verwendet, wobei der Zeiger in den niedrigen 48 Bit gespeichert wird. Wenn Ihr Hauptdatentyp Gleitkomma ist, ist dies die beste Lösung und liefert eine sehr gute Leistung.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.