web-dev-qa-db-fra.com

Tableau de taille 0 à la fin de la structure

Mon professeur d'un cours de programmation de systèmes que je prends nous a dit aujourd'hui de définir une structure avec un tableau de longueur nulle à la fin:

struct array{
    size_t size;
    int data[0];
};

typedef struct array array;

Il s'agit d'une structure utile pour définir ou initialiser un tableau avec une variable, c'est-à-dire quelque chose comme suit:

array *array_new(size_t size){
    array* a = malloc(sizeof(array) + size * sizeof(int));

    if(a){
        a->size = size;
    }

    return a;
}

Autrement dit, en utilisant malloc(), nous allouons également de la mémoire pour le tableau de taille zéro. C'est complètement nouveau pour moi, et cela semble étrange, car, à ma connaissance, les structures n'ont pas nécessairement leurs éléments dans des emplacements continus.

Pourquoi le code dans array_new Alloue de la mémoire à data[0]? Pourquoi serait-il légal d'accéder alors, disons

array * a = array_new(3);
a->data[1] = 12;

?

D'après ce qu'il nous a dit, il semble qu'un tableau défini comme longueur zéro à la fin d'une structure soit assuré de venir immédiatement après le dernier élément de la structure, mais cela semble étrange, car, encore une fois, d'après ma compréhension, les structures pourraient avoir rembourrage.

J'ai également vu que ce n'est qu'une fonctionnalité de gcc et n'est défini par aucune norme. Est-ce vrai?

34
nbro

Actuellement, il existe une fonctionnalité standard, comme mentionné dans C11, chapitre §6.7.2.1, appelée membre du tableau flexible.

Citant la norme,

Dans un cas particulier, le dernier élément d'une structure avec plus d'un membre nommé peut avoir un type de tableau incomplet; c'est ce qu'on appelle un membre de tableau flexible. Dans la plupart des situations, le membre du tableau flexible est ignoré. En particulier, la taille de la structure est comme si le membre de tableau flexible était omis, sauf qu'il peut avoir plus de remplissage de fin que l'omission impliquerait. [...]

La syntaxe doit être

struct s { int n; double d[]; };

où le dernier élément est de type incomplet, ( pas de dimensions de tableau, pas même 0).

Donc, votre code devrait mieux ressembler

struct array{
    size_t size;
    int data[ ];
};

être conforme aux normes.

Maintenant, pour en venir à votre exemple, d'un tableau de taille 0, c'était une manière héritée ( "struct hack") de réaliser la même chose. Avant C99, GCC a pris cela en charge en tant qu'extension pour émuler la fonctionnalité de membre de groupe flexible.

37
Sourav Ghosh

Votre professeur est confus. Ils devraient aller lire ce qui se passe si je définis un tableau de taille nulle . Il s'agit d'une extension GCC non standard; ce n'est pas un C valide et ce n'est pas quelque chose qu'ils devraient apprendre aux élèves à utiliser (*).

À la place, utilisez un membre de tableau flexible C standard . Contrairement à votre tableau de taille nulle, cela fonctionnera réellement, de manière portable:

struct array{
    size_t size;
    int data[];
};

Les membres de tableau flexibles sont garantis pour compter comme zéro lorsque vous utilisez sizeof sur la structure, vous permettant de faire des choses comme:

malloc(sizeof(array) + sizeof(int[size]));

(*) Dans les années 90, les gens utilisaient un exploit dangereux pour ajouter des données après les structures, connu sous le nom de "struct hack". Pour fournir un moyen sûr d'étendre une structure, GCC a implémenté la fonctionnalité de tableau de taille nulle en tant qu'extension non standard. Il est devenu obsolète en 1999 lorsque la norme C a finalement fourni un meilleur moyen de le faire.

24
Lundin

D'autres réponses expliquent que les tableaux de longueur nulle sont une extension GCC et que C permet un tableau de longueur variable mais personne n'a répondu à vos autres questions.

d'après ma compréhension, les structures n'ont pas nécessairement leurs éléments dans des emplacements continus.

Oui. struct le type de données n'a pas nécessairement ses éléments dans des emplacements continus.

Pourquoi le code dans array_new alloue de la mémoire à data[0]? Pourquoi serait-il légal d'accéder alors, disons

array * a = array_new(3);
a->data[1] = 12;

?

Vous devez noter que l'une des restrictions sur le tableau de longueur nulle est qu'il doit être le dernier membre d'une structure. Par cela, le compilateur sait que la structure peut avoir un objet de longueur variable et un peu plus de mémoire sera nécessaire au moment de l'exécution.
Mais, vous ne devriez pas être confondu avec; "puisque le tableau de longueur nulle est le dernier membre de la structure, la mémoire allouée au tableau de longueur nulle doit être ajoutée à la fin de la structure et puisque les structures n'ont pas nécessairement leurs éléments dans des emplacements continus, alors comment que la mémoire allouée soit accessible? "

Non, ce n'est pas le cas. L'allocation de mémoire pour les membres de la structure n'est pas nécessairement contiguë, il peut y avoir un remplissage entre eux, mais cette mémoire allouée doit être accessible avec la variable data. Et oui, le rembourrage n'aura aucun effet ici. La règle est: §6.7.2.1/15

Dans un objet de structure, les membres non-champ de bits et les unités dans lesquelles résident les champs de bits ont des adresses qui augmentent dans l'ordre dans lequel ils sont déclarés.


J'ai également vu que ce n'est qu'une fonctionnalité de gcc et n'est défini par aucune norme. Est-ce vrai?

Oui. Comme d'autres réponses ont déjà mentionné que les tableaux de longueur nulle ne sont pas pris en charge par le standard C, mais une extension des compilateurs GCC. C99 introduit membre du tableau flexible. Un exemple de la norme C (6.7.2.1):

Après la déclaration:

struct s { int n; double d[]; };

la structure struct s a un membre de tableau flexible d. Une manière typique d'utiliser ceci est:

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

et en supposant que l'appel à malloc réussisse, l'objet pointé par p se comporte, dans la plupart des cas, comme si p avait été déclaré comme:

struct { int n; double d[m]; } *p;

(il existe des circonstances dans lesquelles cette équivalence est rompue; en particulier, les décalages du membre d peuvent ne pas être les mêmes).

7
haccks

Une manière plus standard serait de définir votre tableau avec une taille de données de 1, comme dans:

struct array{
    size_t size;
    int data[1]; // <--- will work across compilers
};

Ensuite, utilisez le offset du membre de données (pas la taille du tableau) dans le calcul:

array *array_new(size_t size){
    array* a = malloc(offsetof(array, data) + size * sizeof(int));

    if(a){
        a->size = size;
    }

    return a;
}

Cela utilise efficacement array.data comme marqueur pour savoir où les données supplémentaires peuvent aller (selon la taille).

1
Neil

La façon dont je le faisais est sans membre factice à la fin de la structure: la taille de la structure elle-même vous indique l'adresse juste après. Ajouter 1 au pointeur tapé va là:

header * p = malloc (sizeof (header) + buffersize);
char * buffer = (char*)(p+1);

En ce qui concerne les structures en général, vous pouvez savez que les champs sont disposés dans l'ordre. La possibilité de faire correspondre une structure imposée requise par une image binaire au format de fichier, un appel de système d'exploitation ou du matériel est un avantage de l'utilisation de C. Vous devez savoir comment fonctionne le remplissage pour l'alignement, mais ils sont en ordre et dans un bloc contigu.

1
JDługosz