Pourquoi les tableaux C ne peuvent-ils pas avoir une longueur de 0?

13

La norme C11 stipule que les tableaux, à la fois dimensionnés et de longueur variable "doivent avoir une valeur supérieure à zéro". Quelle est la justification pour ne pas autoriser une longueur de 0?

Surtout pour les tableaux de longueur variable, il est parfaitement logique d'avoir une taille de zéro de temps en temps. Il est également utile pour les tableaux statiques lorsque leur taille provient d'une option de configuration de macro ou de build.

Fait intéressant, GCC (et clang) fournissent des extensions qui permettent des tableaux de longueur nulle. Java autorise également les tableaux de longueur zéro.

Kevin Cox
la source
7
stackoverflow.com/q/8625572 ... "Un tableau de longueur nulle serait délicat et déroutant à concilier avec l'exigence que chaque objet ait une adresse unique."
Robert Harvey
3
@RobertHarvey: Étant donné struct { int p[1],q[1]; } foo; int *pp = p+1;, ppserait un pointeur légitime, mais *ppn'aurait pas d'adresse unique. Pourquoi la même logique ne pourrait-elle pas tenir avec un tableau de longueur nulle? Supposons que, donné int q[0]; dans une structure , qse réfère à une adresse dont la validité serait similaire à celle de l' p+1exemple ci-dessus.
supercat
@DocBrown D'après la norme C11 6.7.6.2.5 parlant de l'expression utilisée pour déterminer la taille d'un VLA "... chaque fois qu'il est évalué, il doit avoir une valeur supérieure à zéro." Je ne sais pas pour C99 (et il semble étrange qu'ils le changent), mais il semble que vous ne puissiez pas avoir une longueur de zéro.
Kevin Cox
@KevinCox: existe-t-il une version en ligne gratuite de la norme C11 (ou de la pièce en question) disponible?
Doc Brown
La version finale n'est pas disponible gratuitement (quel dommage) mais vous pouvez télécharger des brouillons. Le dernier projet disponible est open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf .
Kevin Cox

Réponses:

11

Le problème que je parierais est que les tableaux C ne sont que des pointeurs vers le début d'un morceau de mémoire alloué. Avoir une taille 0 signifierait que vous avez un pointeur vers ... rien? Vous ne pouvez rien avoir, alors il aurait fallu choisir quelque chose d’arbitraire. Vous ne pouvez pas utiliser null, car vos tableaux de longueur 0 ressembleraient à des pointeurs nuls. Et à ce stade, chaque implémentation différente va choisir des comportements arbitraires différents, conduisant au chaos.

Telastyn
la source
7
@delnan: Eh bien, si vous voulez être pédant à ce sujet, l'arithmétique des tableaux et des pointeurs est définie de telle sorte qu'un pointeur peut être facilement utilisé pour accéder à un tableau ou pour simuler un tableau. En d'autres termes, c'est l'arithmétique des pointeurs et l'indexation des tableaux qui sont équivalents en C. Mais le résultat est quand même le même ... si la longueur du tableau est nulle, vous ne pointez toujours sur rien.
Robert Harvey
3
@RobertHarvey Tout à fait vrai, mais vos derniers mots (et toute la réponse rétrospectivement) semblent être un moyen confus et déroutant d'expliquer qu'un tel tableau (je pense que c'est ce que cette réponse appelle "un morceau de mémoire alloué"?) Aurait sizeof0, et comment cela causerait des problèmes. Tout cela peut être expliqué en utilisant les concepts et la terminologie appropriés sans perte de brièveté ou de clarté. Mélanger les tableaux et les pointeurs ne risque que de répandre la conception erronée des tableaux = pointeurs (ce qui est plus important dans d'autres contextes) sans aucun avantage.
2
" Vous ne pouvez pas utiliser null, car alors vos tableaux de longueur 0 ressembleraient à des pointeurs null " - en fait, c'est exactement ce que fait Delphi. Les dynarrays vides et les chaînes longues vides sont des pointeurs techniquement nuls.
JensG
3
-1, je suis plein de @delnan ici. Cela n'explique rien, en particulier dans le contexte de ce que l'OP a écrit sur certains compilateurs majeurs prenant en charge le concept de tableaux de longueur nulle. Je suis à peu près sûr que des tableaux de longueur nulle pourraient être fournis en C d'une manière indépendante de l'implémentation, sans "conduire au chaos".
Doc Brown
6

Voyons comment un tableau est généralement disposé en mémoire:

         +----+
arr[0] : |    |
         +----+
arr[1] : |    |
         +----+
arr[2] : |    |
         +----+
          ...
         +----+
arr[n] : |    |
         +----+

Notez qu'il n'y a pas d'objet distinct nommé arrqui stocke l'adresse du premier élément; lorsqu'un tableau apparaît dans une expression, C calcule l'adresse du premier élément selon les besoins.

Alors, réfléchissons à ceci: un tableau à 0 élément n'aurait pas de stockage mis de côté, ce qui signifie qu'il n'y a rien pour calculer l'adresse du tableau (en d'autres termes, il n'y a pas de mappage d'objet pour l'identifiant). C'est comme dire: «Je veux créer une intvariable qui ne prend pas de mémoire». C'est une opération absurde.

Éditer

Les tableaux Java sont des animaux complètement différents des tableaux C et C ++; ce n'est pas un type primitif, mais un type de référence dérivé de Object.

Modifier 2

Un point soulevé dans les commentaires ci-dessous - la contrainte "supérieur à 0" ne s'applique qu'aux tableaux dont la taille est spécifiée via une expression constante ; un VLA est autorisé à avoir une longueur 0 La déclaration d'un VLA avec une expression non constante de valeur 0 n'est pas une violation de contrainte, mais elle appelle un comportement non défini.

Il est clair que les VLA sont des animaux différents des tableaux réguliers , et leur implémentation peut permettre une taille 0 . Ils ne peuvent pas être déclarés staticou à portée de fichier, car la taille de ces objets doit être connue avant le démarrage du programme.

Cela ne vaut également rien qu'à compter de C11, les implémentations ne sont pas nécessaires pour prendre en charge les VLA.

John Bode
la source
3
Désolé, mais à mon humble avis, vous manquez le point, tout comme Telastyn. Les tableaux de longueur nulle peuvent avoir beaucoup de sens, et les implémentations existantes comme celles dont l'OP nous a parlé montrent que cela peut être fait.
Doc Brown
@DocBrown: Tout d'abord, j'expliquais pourquoi la norme linguistique les interdisait très probablement. Deuxièmement, je voudrais un exemple où un tableau de longueur 0 est logique, car je ne peux honnêtement pas en penser un. L'implémentation la plus probable est de traiter T a[0]comme T *a, mais alors pourquoi ne pas simplement l'utiliser T *a?
John Bode
Désolé, mais je n'achète pas le «raisonnement théorique» pour expliquer pourquoi la norme l'interdit. Lisez ma réponse comment l'adresse pourrait être réellement calculée facilement. Et je vous suggère de suivre le lien dans le premier commentaire de Robert Harveys sous la question et de lire la deuxième réponse, il y a un exemple utile.
Doc Brown
@DocBrown: Ah. Le structhack. Je ne l'ai jamais utilisé personnellement; n'a jamais travaillé sur un problème nécessitant un structtype de taille variable .
John Bode
2
Et sans oublier AFAIK depuis C99, C permet des tableaux de longueur variable. Et lorsque la taille du tableau est un paramètre, ne pas avoir à traiter une valeur de 0 comme un cas spécial peut simplifier beaucoup de programmes.
Doc Brown
2

Vous voudriez généralement que votre tableau de taille zéro (en fait variable) connaisse sa taille au moment de l'exécution. Emballez ensuite cela dans un structet utilisez des membres de tableau flexibles , comme par exemple:

struct my_st {
   unsigned len;
   double flexarray[]; // of size len
};

De toute évidence, le membre du tableau flexible doit être le dernier dans son structet vous devez avoir quelque chose avant. Souvent, cela serait lié à la longueur réelle occupée par l'exécution de ce membre de groupe flexible.

Bien sûr, vous alloueriez:

 unsigned len = some_length_computation();
 struct my_st*p = malloc(sizeof(struct my_st)+len*sizeof(double));
 if (!p) { perror("malloc my_st"); exit(EXIT_FAILURE); };
 p->len = len;
 for (unsigned ix=0; ix<len; ix++)
    p->flexarray[ix] = log(3.0+(double)ix);

AFAIK, c'était déjà possible en C99, et c'est très utile.

BTW, les membres de tableau flexibles n'existent pas en C ++ (car il serait difficile de définir quand et comment ils doivent être construits et détruits). Voir cependant le futur std :: dynarray

Basile Starynkevitch
la source
Vous savez, ils pourraient simplement être contraints à des types triviaux, et il n'y aurait aucune difficulté.
Déduplicateur
2

Si l'expression type name[count]est écrite dans une fonction, vous dites au compilateur C d'allouer sur les sizeof(type)*countoctets de trame de pile et de calculer l'adresse du premier élément du tableau.

Si l'expression type name[count]est écrite en dehors de toutes les définitions de fonctions et de structures, vous dites au compilateur C d'allouer sur les sizeof(type)*countoctets de segment de données et de calculer l'adresse du premier élément du tableau.

nameest en fait un objet constant qui stocke l'adresse du premier élément du tableau et chaque objet qui stocke une adresse de mémoire est appelé pointeur, c'est donc la raison pour laquelle vous traitez namecomme un pointeur plutôt qu'un tableau. Notez que les tableaux en C ne sont accessibles que via des pointeurs.

Si countest une expression constante qui évalue à zéro, vous dites au compilateur C d'allouer zéro octet sur la trame de pile ou le segment de données et de renvoyer l'adresse du premier élément du tableau, mais le problème en faisant cela est que le premier élément de tableau de longueur nulle n'existe pas et vous ne pouvez pas calculer l'adresse de quelque chose qui n'existe pas.

C'est rationnel que l'élément non. count+1n'existe pas dans un counttableau de longueur, c'est pourquoi le compilateur C interdit de définir un tableau de longueur nulle comme variable dans et en dehors d'une fonction, car quel est le contenu de namealors? Quelle adresse namestocke exactement?

Si pest un pointeur, l'expression p[n]est équivalente à*(p + n)

Lorsque l'astérisque * dans l'expression de droite est une opération de déréférencement du pointeur, ce qui signifie accéder à la mémoire pointée par p + nou accéder à la mémoire dont l'adresse est stockée p + n, où p + nest l'expression du pointeur, il prend l'adresse de pet ajoute à cette adresse le nombre nmultiplier le taille du type du pointeur p.

Est-il possible d'ajouter une adresse et un numéro?

Oui, c'est possible, car l'adresse est un entier non signé généralement représenté en notation hexadécimale.

user307542
la source
De nombreux compilateurs autorisaient les déclarations de tableaux de taille nulle avant que la norme ne l'interdise, et beaucoup continuent à autoriser ces déclarations comme extension. De telles déclarations ne poseront aucun problème si l'on reconnaît qu'un objet de taille Na N+1des adresses associées, dont la première Nidentifie des octets uniques et la dernière Ndont chaque point vient de dépasser un de ces octets. Une telle définition fonctionnerait très bien même dans le cas dégénéré où Nest 0.
supercat
1

Si vous voulez un pointeur vers une adresse mémoire, déclarez-en un. Un tableau pointe en fait sur un morceau de mémoire que vous avez réservé. Les tableaux se désintègrent aux pointeurs lorsqu'ils sont passés aux fonctions, mais si la mémoire vers laquelle ils pointent est sur le tas, pas de problème. Il n'y a aucune raison de déclarer un tableau de taille zéro.

ncmathsadist
la source
2
Généralement, vous ne le feriez pas directement, mais à la suite d'une macro ou lors de la déclaration d'un tableau de longueur variable avec des données dynamiques.
Kevin Cox
Un tableau ne pointe jamais. Il peut contenir des pointeurs, et dans la plupart des contextes, vous utilisez en fait un pointeur vers le premier élément, mais c'est une autre histoire.
Déduplicateur
1
Le nom du tableau EST un pointeur constant vers la mémoire contenue dans le tableau.
ncmathsadist
1
Non, le nom du tableau se désintègre en un pointeur vers le premier élément, dans la plupart des contextes. La différence est souvent cruciale.
Déduplicateur
1

Depuis l'époque de la C89 d'origine, lorsqu'une norme C spécifiait que quelque chose avait un comportement indéfini, cela signifiait «Faire tout ce qui rendrait une implémentation sur une plate-forme cible particulière plus adaptée à son objectif». Les auteurs de la norme ne voulaient pas essayer de deviner quels comportements pourraient être les plus adaptés à un usage particulier. Les implémentations C89 existantes avec des extensions VLA peuvent avoir eu des comportements différents, mais logiques, quand on leur a donné une taille de zéro (par exemple, certains pourraient avoir traité le tableau comme une expression d'adresse donnant NULL, tandis que d'autres le traitant comme une expression d'adresse qui pourrait être égale à l'adresse de une autre variable arbitraire, mais pourrait sans risque y avoir ajouté zéro sans piéger). Si un code pouvait se fonder sur un comportement aussi différent, les auteurs de la norme ne

Plutôt que d'essayer de deviner ce que les implémentations pourraient faire ou de suggérer que tout comportement devrait être considéré comme supérieur à tout autre, les auteurs de la norme ont simplement permis aux implémenteurs de faire preuve de jugement dans le traitement de ce cas comme ils l'entendaient. Les implémentations qui utilisent malloc () en arrière-plan peuvent traiter l'adresse du tableau comme NULL (si malloc à taille zéro renvoie null), celles qui utilisent des calculs d'adresse de pile peuvent produire un pointeur qui correspond à l'adresse d'une autre variable, et d'autres implémentations peuvent faire autres choses. Je ne pense pas qu'ils s'attendaient à ce que les auteurs de compilateurs fassent tout leur possible pour que le boîtier de coin de taille zéro se comporte de manière délibérément inutile.

supercat
la source