web-dev-qa-db-fra.com

L'utilisation de membres de tableau flexible dans C est-elle une mauvaise pratique?

J'ai récemment lu que l'utilisation de membres de baies flexibles en C était une mauvaise pratique en génie logiciel. Cependant, cette déclaration n'était étayée par aucun argument. Est-ce un fait accepté?

( Membres de tableau flexibles est une fonctionnalité C introduite dans C99 par laquelle on peut déclarer le dernier élément comme étant un tableau de taille non spécifiée. Par exemple:)

struct header {
    size_t len;
    unsigned char data[];
};
69
Lionel

C'est un "fait" accepté que l'utilisation de goto est une mauvaise pratique d'ingénierie logicielle. Cela ne le rend pas vrai. Il y a des moments où goto est utile, en particulier lors de la gestion du nettoyage et lors du portage depuis l'assembleur.

Les membres de la baie flexible me semblent avoir une utilisation principale, du haut de ma tête, qui consiste à mapper les formats de données hérités comme les formats de modèle de fenêtre sur RiscOS. Ils auraient été extrêmement utiles pour cela il y a environ 15 ans, et je suis sûr qu'il y a encore des gens qui s'occupent de ces choses qui les trouveraient utiles.

Si l'utilisation de membres de tableaux flexibles est une mauvaise pratique, je suggère que nous disions tous ceci aux auteurs de la spécification C99. Je soupçonne qu'ils pourraient avoir une réponse différente.

25
Airsource Ltd

VEUILLEZ LIRE ATTENTIVEMENT LES COMMENTAIRES CI-DESSOUS DE CETTE RÉPONSE

À mesure que la normalisation C progresse, il n'y a plus de raison d'utiliser [1].

La raison pour laquelle je donnerais pour ne pas le faire est que cela ne vaut pas la peine de lier votre code à C99 juste pour utiliser cette fonctionnalité.

Le fait est que vous pouvez toujours utiliser l'idiome suivant:

struct header {
  size_t len;
  unsigned char data[1];
};

C'est entièrement portable. Ensuite, vous pouvez prendre en compte le 1 lors de l'allocation de la mémoire pour n éléments dans le tableau data:

ptr = malloc(sizeof(struct header) + (n-1));

Si vous avez déjà C99 comme exigence pour construire votre code pour toute autre raison ou si vous ciblez un compilateur spécifique, je ne vois aucun mal.

19
Remo.D

Vous vouliez...

struct header
{
 size_t len;
 unsigned char data[];
}; 

En C, c'est un idiome commun. Je pense que de nombreux compilateurs acceptent également:

  unsigned char data[0];

Oui, c'est dangereux, mais là encore, ce n'est vraiment pas plus dangereux que les tableaux C normaux - c'est-à-dire TRÈS dangereux ;-). Utilisez-le avec précaution et uniquement dans les cas où vous avez vraiment besoin d'un tableau de taille inconnue. Assurez-vous que vous malloc et libérez la mémoire correctement, en utilisant quelque chose comme: -

  foo = malloc(sizeof(header) + N * sizeof(data[0]));
  foo->len = N;

Une alternative consiste à faire des données simplement un pointeur vers les éléments. Vous pouvez ensuite réallouer () les données à la bonne taille selon les besoins.

  struct header
    {
     size_t len;
     unsigned char *data;
    }; 

Bien sûr, si vous posiez des questions sur C++, l'une ou l'autre serait une mauvaise pratique. Ensuite, vous utiliseriez généralement des vecteurs STL à la place.

10
Roddy

Non, utiliser membres du tableau flexible en C n'est pas une mauvaise pratique.

Cette fonctionnalité de langage a été normalisée pour la première fois dans ISO C99, 6.7.2.1 (16). Pour la norme actuelle, ISO C11, elle est spécifiée à la section 6.7.2.1 (18).

Vous pouvez les utiliser comme ceci:

struct Header {
    size_t d;
    long v[];
};
typedef struct Header Header;
size_t n = 123; // can dynamically change during program execution
// ...
Header *h = malloc(sizeof(Header) + sizeof(long[n]));
h->n = n;

Alternativement, vous pouvez l'allouer comme ceci:

Header *h = malloc(sizeof *h + n * sizeof h->v[0]);

Notez que sizeof(Header) inclut d'éventuels octets de remplissage, ainsi, l'allocation suivante est incorrecte et peut entraîner un débordement de tampon:

Header *h = malloc(sizeof(size_t) + sizeof(long[n])); // invalid!

Une structure avec des membres de tableau flexibles réduit le nombre d'allocations pour elle de 1/2, c'est-à-dire qu'au lieu de 2 allocations pour un objet struct dont vous avez besoin juste de 1. Signifiant moins d'effort et moins de mémoire occupée par la surcharge de comptabilité de l'allocateur de mémoire. En outre, vous enregistrez le stockage pour un pointeur supplémentaire. Ainsi, si vous devez allouer un grand nombre de telles instances de structure, vous améliorez de manière mesurable le temps d'exécution et l'utilisation de la mémoire de votre programme (par un facteur constant).

Contrairement à cela, l'utilisation de constructions non standardisées pour des membres de tableau flexibles qui produisent un comportement non défini (par exemple, comme dans long v[0]; Ou long v[1];) Est évidemment une mauvaise pratique. Ainsi, comme tout comportement indéfini, cela doit être évité.

Depuis la publication de l'ISO C99 en 1999, il y a près de 20 ans, la recherche de la compatibilité avec l'ISO C89 est un argument faible.

6
maxschlepzig

J'ai vu quelque chose comme ça: depuis l'interface C et l'implémentation.

  struct header {
    size_t len;
    unsigned char *data;
};

   struct header *p;
   p = malloc(sizeof(*p) + len + 1 );
   p->data = (unsigned char*) (p + 1 );  // memory after p is mine! 

Remarque: les données n'ont pas besoin d'être le dernier membre.

6
Nyan

Il y a quelques inconvénients liés à la façon dont les structures sont parfois utilisées, et cela peut être dangereux si vous ne réfléchissez pas aux implications.

Pour votre exemple, si vous démarrez une fonction:

void test(void) {
  struct header;
  char *p = &header.data[0];

  ...
}

Ensuite, les résultats ne sont pas définis (car aucun stockage n'a jamais été alloué pour les données). C'est quelque chose que vous connaissez normalement, mais il y a des cas où les programmeurs C sont probablement habitués à pouvoir utiliser la sémantique des valeurs pour les structures, qui se décompose de diverses autres manières.

Par exemple, si je définis:

struct header2 {
  int len;
  char data[MAXLEN]; /* MAXLEN some appropriately large number */
}

Ensuite, je peux copier deux instances simplement par affectation, à savoir:

struct header2 inst1 = inst2;

Ou s'ils sont définis comme des pointeurs:

struct header2 *inst1 = *inst2;

Cela ne fonctionnera cependant pas, car le tableau de variables data n'est pas copié. Ce que vous voulez, c'est malloc dynamiquement la taille de la structure et copier sur le tableau avec memcpy ou équivalent.

De même, l'écriture d'une fonction qui accepte une structure ne fonctionnera pas, car les arguments dans les appels de fonction sont, encore une fois, copiés par valeur, et donc ce que vous obtiendrez ne sera probablement que le premier élément de votre tableau data.

Cela ne fait pas une mauvaise idée à utiliser, mais vous devez garder à l'esprit de toujours allouer dynamiquement ces structures et de les passer uniquement comme des pointeurs.

4
wds

En remarque, pour la compatibilité C89, une telle structure doit être allouée comme:

struct header *my_header
  = malloc(offsetof(struct header, data) + n * sizeof my_header->data);

Ou avec des macros:

#define FLEXIBLE_SIZE SIZE_MAX /* or whatever maximum length for an array */
#define SIZEOF_FLEXIBLE(type, member, length) \
  ( offsetof(type, member) + (length) * sizeof ((type *)0)->member[0] )

struct header {
  size_t len;
  unsigned char data[FLEXIBLE_SIZE];
};

...

size_t n = 123;
struct header *my_header = malloc(SIZEOF_FLEXIBLE(struct header, data, n));

La définition de FLEXIBLE_SIZE sur SIZE_MAX garantit presque que cela échouera:

struct header *my_header = malloc(sizeof *my_header);
4
diapir