web-dev-qa-db-fra.com

Comment un type de données mixte (int, float, char, etc.) peut-il être stocké dans un tableau?

Je veux stocker des types de données mélangés dans un tableau. Comment peut-on faire ça?

143
chanzerre

Vous pouvez transformer les éléments du tableau en une union discriminée, autrement dit nion étiquetée .

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

Le membre type permet de choisir quel membre du union doit être utilisé pour chaque élément du tableau. Donc si vous voulez stocker un int dans le premier élément, vous feriez:

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

Lorsque vous souhaitez accéder à un élément du tableau, vous devez d'abord vérifier le type, puis utiliser le membre correspondant de l'union. Une instruction switch est utile:

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
}

Il appartient au programmeur de s'assurer que le membre type correspond toujours à la dernière valeur stockée dans le fichier union.

239
Barmar

Utilisez un syndicat:

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

Vous devrez cependant garder une trace du type de chaque élément.

32
user529758

Les éléments de tableau doivent avoir la même taille, c'est pourquoi ce n'est pas possible. Vous pouvez contourner ce problème en créant un type de variante :

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

La taille de l'élément de l'union est la taille du plus grand élément, 4.

20
user1129665

Il existe un style différent de définition de l'union de balises (quel que soit son nom) que l'OMI rend beaucoup plus agréable à tiliser, en supprimant l'union interne. C'est le style utilisé dans le système X Window pour des choses comme les événements.

L'exemple de la réponse de Barmar donne le nom val à l'union interne. L'exemple de la réponse de Sp. Utilise une union anonyme pour éviter d'avoir à spécifier le .val. à chaque fois que vous accédez à l’enregistrement de variante. Malheureusement, les structures et unions internes "anonymes" ne sont pas disponibles dans C89 ou C99. C'est une extension de compilateur, et donc intrinsèquement non portable.

Une meilleure façon, à l’OMI, est d’inverser toute la définition. Attribuez à chaque type de données sa propre structure et placez la balise (spécificateur de type) dans chaque structure.

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

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

Ensuite, vous envelopper dans une union de haut niveau.

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

enum types { INVALID, INT, REAL };

Maintenant, il peut sembler que nous nous répétons, et nous sommes. Mais considérons que cette définition est susceptible d’être isolée dans un seul fichier. Mais nous avons éliminé le bruit de spécifier l’intermédiaire .val. avant d’arriver aux données.

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

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

Au lieu de cela, il va à la fin, où il est moins odieux. :RÉ

Une autre chose que cela permet est une forme d'héritage. Edit: cette partie n’est pas du standard C, mais utilise une extension 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 et down-casting.


Edit: Il faut en être conscient si vous en construisez un avec des initialiseurs C99. Tous les membres doivent être initialisés par le même membre du syndicat.

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

problem.tag; // may not be initialized

Le .tag initializer peut être ignoré par un compilateur optimiseur, car le .int_ initialiseur suivant alias même zone de données. Même si nous connaissons la disposition (!), Et cela devrait soit ok. Non, ce n'est pas. Utilisez plutôt la balise "internal" (elle recouvre la balise externe, comme nous le voulons, mais ne confond pas le compilateur).

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

not_a_problem.tag; // == INT
8
luser droog

Vous pouvez faire un void * tableau, avec un tableau séparé de size_t. Mais vous perdez le type d’information.
Si vous devez conserver le type d'information d'une certaine manière, conservez un troisième tableau d'int (où int est une valeur énumérée). Codez ensuite la fonction qui lance en fonction de la valeur enum.

5
dzada

L'union est la voie standard à suivre. Mais vous avez également d'autres solutions. L'un d'eux est pointeur marqué , ce qui implique de stocker davantage d'informations dans les bits "libres" d'un pointeur.

Selon les architectures, vous pouvez utiliser les bits bas ou élevés, mais la méthode la plus sûre et la plus portable consiste à utiliser les bits faibles inutilisés en tirant parti de la mémoire alignée. . Par exemple, dans les systèmes 32 bits et 64 bits, les pointeurs vers int doivent être des multiples de 4 (en supposant que int est un type 32 bits) et les 2 bits les moins significatifs doivent être 0, vous pouvez donc les utiliser pour stocker le type de vos valeurs. Bien sûr, vous devez effacer les bits de balise avant de déréférencer le pointeur. Par exemple, si votre type de données est limité à 4 types différents, vous pouvez l'utiliser comme ci-dessous

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

Si vous pouvez vous assurer que les données sont alignées sur 8 octets (comme pour les pointeurs dans les systèmes 64 bits, ou long long et uint64_t...), vous aurez un bit de plus pour la balise.

Cela a pour inconvénient que vous aurez besoin de plus de mémoire si les données n'ont pas été stockées dans une variable ailleurs. Par conséquent, si le type et la plage de vos données sont limités, vous pouvez stocker les valeurs directement dans le pointeur. Cette technique a été utilisée dans la version 32 bits du moteur V8 de Chrome , où elle vérifie le bit le moins significatif de l'adresse pour voir si c'est un = pointeur sur un autre objet (type double, grand entier, chaîne ou un objet) ou valeur signée sur 31 bits (appelé smi - petit entier ). S'il s'agit d'un int, Chrome effectue simplement un décalage arithmétique à droite d'un bit pour obtenir la valeur, sinon le pointeur est déréférencé.


Sur la plupart des systèmes 64 bits actuels, l'espace d'adressage virtuel est toujours beaucoup plus étroit que 64 bits. Par conséquent, les bits de poids fort peuvent également être utilisés comme balises . En fonction de l'architecture, vous avez différentes manières de les utiliser en tant que balises. ARM , 68k et beaucoup d'autres peuvent être configurés pour ignorer les bits supérieurs , vous permettant de les utiliser librement sans vous soucier de segfault ou quoi que ce soit. De l'article Wikipedia ci-dessus lié:

Un exemple significatif d'utilisation de pointeurs balisés est le runtime Objective-C sur iOS 7 sur ARM64, notamment utilisé sur l'iPhone 5S. Dans iOS 7, les adresses virtuelles sont de 33 bits (alignées sur les octets). Par conséquent, les adresses alignées sur Word utilisent uniquement 30 bits (les 3 bits les moins significatifs valant 0), ce qui laisse 34 bits pour les balises. Les pointeurs de classe Objective-C sont alignés sur Word et les champs de balises sont utilisés à diverses fins, telles que le stockage d'un nombre de références et le fait de savoir si l'objet a un destructeur.

Les premières versions de MacOS utilisaient des adresses étiquetées, appelées Handles, pour stocker les références aux objets de données. Les bits de poids fort de l'adresse indiquaient si l'objet de données était respectivement verrouillé, purulable et/ou issu d'un fichier de ressources. Cela posait des problèmes de compatibilité lorsque l’adressage MacOS passait de 24 à 32 bits dans System 7.

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

Sur x86_64 vous pouvez toujours utiliser les bits élevés comme balises avec précaution . Bien sûr, vous n’avez pas besoin d’utiliser tous ces 16 bits et vous pouvez en laisser de côté pour des raisons de sécurité.

Dans les versions précédentes de Mozilla Firefox, ils utilisaient également optimisations pour les petits nombres entiers comme V8, avec 3 bits bas utilisés pour stocker le type (int, chaîne, objet, etc.). Mais depuis JägerMonkey, ils ont emprunté un autre chemin ( Nouvelle représentation de la valeur JavaScript de Mozilla , lien de sauvegarde ). La valeur est maintenant toujours stockée dans une variable double précision de 64 bits. Lorsque le double est un normalisé, il peut être utilisé directement dans les calculs. Toutefois, si ses 16 bits supérieurs sont tous des 1, ce qui correspond à un NaN, les 32 bits les plus bas seront stocker l'adresse (dans un ordinateur 32 bits) sur la valeur ou la valeur directement, les 16 bits restants seront utilisés pour stocker le type. Cette technique s'appelle NaN-boxing ou non-boxing. Il est également utilisé dans JavaScriptCore 64 bits de WebKit et dans SpiderMonkey de Mozilla, le pointeur étant stocké dans les 48 bits les plus bas. Si votre type de données principal est à virgule flottante, il s'agit de la meilleure solution et offre de très bonnes performances.

En savoir plus sur les techniques ci-dessus: https://wingolog.org/archives/2011/05/05/18/value-representation-in-javascript-implementations

3
phuclv