Placement de la déclaration de variable en C

129

J'ai longtemps pensé qu'en C, toutes les variables devaient être déclarées au début de la fonction. Je sais que dans C99, les règles sont les mêmes que dans C ++, mais quelles sont les règles de placement de déclaration de variable pour C89 / ANSI C?

Le code suivant se compile avec succès avec gcc -std=c89et gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

Les déclarations de cet sprovoquer une erreur en mode C89 / ANSI?

mcjabberz
la source
54
Juste une note: les variables dans ansi C ne doivent pas être déclarées au début d'une fonction mais plutôt au début d'un bloc. Donc, char c = ... en haut de votre boucle for est complètement légal dans ansi C. Les char * s, cependant, ne le seraient pas.
Jason Coco

Réponses:

149

Il se compile avec succès parce que GCC autorise la déclaration de en stant qu'extension GNU, même si elle ne fait pas partie du standard C89 ou ANSI. Si vous souhaitez adhérer strictement à ces normes, vous devez passer le -pedanticdrapeau.

La déclaration de cau début d'un { }bloc fait partie de la norme C89; le bloc n'a pas besoin d'être une fonction.

mipadi
la source
41
Il est probablement intéressant de noter que seule la déclaration de sest une extension (du point de vue C89). La déclaration de cest parfaitement légale en C89, aucune extension n'est nécessaire.
Du
7
@AndreyT: Oui, en C, les déclarations de variables devraient être @ le début d'un bloc et non une fonction en soi; mais les gens confondent bloc et fonction car c'est le premier exemple de bloc.
legends2k
1
J'ai déplacé le commentaire avec +39 votes dans la réponse.
MarcH
78

Pour C89, vous devez déclarer toutes vos variables au début d'un bloc de portée .

Ainsi, votre char cdéclaration est valide car elle se trouve en haut du bloc de portée de la boucle for. Mais, la char *sdéclaration devrait être une erreur.

Kiley Hykawy
la source
2
Tout à fait correct. Vous pouvez déclarer des variables au début de n'importe quel {...}.
Artelius
5
@Artelius Pas tout à fait correct. Seulement si les curlies font partie d'un bloc (pas s'ils font partie d'une déclaration de struct ou d'union ou d'un initialiseur contreventé.)
Jens
Juste pour être pédant, la déclaration erronée doit être au moins notifiée selon la norme C. Cela devrait donc être une erreur ou un avertissement gcc. Autrement dit, ne croyez pas qu'un programme peut être compilé pour signifier qu'il est conforme.
jinawee
35

Le regroupement des déclarations de variables en haut du bloc est un héritage probablement dû aux limitations des anciens compilateurs C primitifs. Tous les langages modernes recommandent et parfois même appliquent la déclaration des variables locales au dernier point: là où elles sont initialisées pour la première fois. Parce que cela élimine le risque d'utiliser une valeur aléatoire par erreur. La séparation de la déclaration et de l'initialisation vous empêche également d'utiliser "const" (ou "final") quand vous le pouvez.

C ++ continue malheureusement d'accepter l'ancienne méthode de déclaration la plus élevée pour la compatibilité descendante avec C (un glisser de compatibilité C hors de beaucoup d'autres ...) Mais C ++ essaie de s'en éloigner:

  • La conception des références C ++ ne permet même pas un tel haut du regroupement de blocs.
  • Si vous séparez la déclaration et l'initialisation d'un objet local C ++, vous payez le coût d'un constructeur supplémentaire pour rien. Si le constructeur no-arg n'existe pas, vous n'êtes même pas autorisé à séparer les deux!

C99 commence à déplacer C dans cette même direction.

Si vous craignez de ne pas trouver où les variables locales sont déclarées, cela signifie que vous avez un problème beaucoup plus important: le bloc englobant est trop long et doit être divisé.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions

Mars
la source
Voir aussi comment forcer les déclarations de variables en haut du bloc peut créer des failles de sécurité: lwn.net/Articles/443037
MarcH
"C ++ continue malheureusement d'accepter l'ancien moyen de déclaration pour la compatibilité descendante avec C": à mon humble avis, c'est juste la manière propre de le faire. Un autre langage "résout" ce problème en initialisant toujours avec 0. Bzzt, cela ne masque les erreurs de logique que si vous me le demandez. Et il y a de nombreux cas où vous avez BESOIN d'une déclaration sans initialisation car il existe plusieurs emplacements possibles pour l'initialisation. Et c'est pourquoi le RAII de C ++ est vraiment très pénible - Vous devez maintenant inclure un état non initialisé «valide» dans chaque objet pour tenir compte de ces cas.
Jo So
1
@JoSo: Je ne comprends pas pourquoi vous pensez qu'avoir des lectures de variables non initialisées produira des effets arbitraires rendra les erreurs de programmation plus faciles à détecter que de leur donner une valeur cohérente ou une erreur déterministe? Notez qu'il n'y a aucune garantie qu'une lecture d'un stockage non intégré se comportera d'une manière compatible avec tout modèle de bits que la variable aurait pu contenir, ni même qu'un tel programme se comportera d'une manière compatible avec les lois habituelles du temps et de la causalité. Compte tenu de quelque chose comme int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat
1
@JoSo: Pour les pointeurs, en particulier sur les implémentations qui interceptent les opérations null, all-bits-zero est souvent une valeur d' interruption utile. De plus, dans les langages qui spécifient explicitement que les variables par défaut à tous les bits-zéro, se fier à cette valeur n'est pas une erreur . Les compilateurs n'ont pas encore tendance à devenir trop farfelus avec leurs "optimisations", mais les rédacteurs de compilateurs continuent d'essayer d'être de plus en plus intelligents. Une option du compilateur pour initialiser des variables avec des variables pseudo-aléatoires délibérées peut être utile pour identifier les défauts, mais le simple fait de laisser le stockage contenant sa dernière valeur peut parfois masquer des défauts.
supercat
22

D'un point de vue maintenabilité, plutôt que syntaxique, il y a au moins trois courants de pensée:

  1. Déclarez toutes les variables au début de la fonction afin qu'elles soient au même endroit et que vous puissiez voir la liste complète en un coup d'œil.

  2. Déclarez toutes les variables aussi près que possible de l'endroit où elles ont été utilisées pour la première fois, afin que vous sachiez pourquoi chacune est nécessaire.

  3. Déclarez toutes les variables au début du bloc de portée le plus interne, afin qu'elles soient hors de portée dès que possible et permettent au compilateur d'optimiser la mémoire et de vous dire si vous les utilisez accidentellement là où vous ne l'aviez pas prévu.

Je préfère généralement la première option, car je trouve que les autres me forcent souvent à parcourir le code pour les déclarations. La définition initiale de toutes les variables facilite également leur initialisation et leur visualisation à partir d'un débogueur.

Je déclare parfois des variables dans un bloc de portée plus petit, mais uniquement pour une bonne raison, dont j'en ai très peu. Un exemple pourrait être après a fork(), pour déclarer les variables nécessaires uniquement par le processus enfant. Pour moi, cet indicateur visuel est un rappel utile de leur objectif.

Adam Liss
la source
27
J'utilise l'option 2 ou 3, il est donc plus facile de trouver les variables - parce que les fonctions ne devraient pas être si grandes que vous ne pouvez pas voir les déclarations de variables.
Jonathan Leffler
8
L'option 3 n'est pas un problème, sauf si vous utilisez un compilateur des années 70.
edgar.holleis
15
Si vous avez utilisé un IDE décent, vous n'aurez pas besoin de chercher du code, car il devrait y avoir une commande IDE pour trouver la déclaration pour vous. (F3 dans Eclipse)
edgar.holleis
4
Je ne comprends pas comment vous pouvez garantir l'initialisation dans l'option 1, peut-être que vous ne pouvez obtenir la valeur initiale que plus tard dans le bloc, en appelant une autre fonction ou en effectuant une calcul.
Plumenator
4
@Plumenator: l'option 1 n'assure pas l'initialisation; J'ai choisi de les initialiser lors de la déclaration, soit à leurs valeurs «correctes», soit à quelque chose qui garantira que le code suivant se cassera si elles ne sont pas définies correctement. Je dis "choisi" parce que ma préférence est passée au n ° 2 depuis que j'ai écrit ceci, peut-être parce que j'utilise plus Java que C maintenant, et parce que j'ai de meilleurs outils de développement.
Adam Liss
6

Comme d'autres l'ont noté, GCC est permissif à cet égard (et peut-être à d'autres compilateurs, selon les arguments avec lesquels ils sont appelés) même en mode 'C89', à moins que vous n'utilisiez la vérification 'pédantique'. Pour être honnête, il n'y a pas beaucoup de bonnes raisons de ne pas avoir de pédantisme; le code moderne de qualité doit toujours compiler sans avertissements (ou très peu où vous savez que vous faites quelque chose de spécifique qui est suspect pour le compilateur comme une erreur possible), donc si vous ne pouvez pas faire compiler votre code avec une configuration pédante, il a probablement besoin d'une certaine attention.

C89 exige que les variables soient déclarées avant toute autre instruction dans chaque portée, les normes ultérieures permettent une déclaration plus proche de l'utilisation (ce qui peut être à la fois plus intuitif et plus efficace), en particulier la déclaration et l'initialisation simultanées d'une variable de contrôle de boucle dans les boucles `` for ''.

Gaidheal
la source
0

Comme on l’a noté, il existe deux écoles de pensée à ce sujet.

1) Déclarez tout en haut des fonctions car l'année est 1987.

2) Déclarez le plus proche de la première utilisation et dans la plus petite portée possible.

Ma réponse à cela est FAITES LES DEUX! Laisse-moi expliquer:

Pour les fonctions longues, 1) rend la refactorisation très difficile. Si vous travaillez dans une base de code où les développeurs sont contre l'idée de sous-routines, alors vous aurez 50 déclarations de variables au début de la fonction et certaines d'entre elles pourraient simplement être un "i" pour une boucle for qui est au plus bas de la fonction.

J'ai donc développé la déclaration au sommet du SSPT à partir de cela et j'ai essayé de faire l'option 2) religieusement.

Je suis revenu à la première option à cause d'une chose: des fonctions courtes. Si vos fonctions sont assez courtes, alors vous aurez peu de variables locales et comme la fonction est courte, si vous les mettez en haut de la fonction, elles seront toujours proches de la première utilisation.

De plus, l'anti-modèle de «déclarer et définir à NULL» lorsque vous voulez déclarer en haut mais que vous n'avez pas fait certains calculs nécessaires à l'initialisation est résolu car les choses que vous devez initialiser seront probablement reçues comme arguments.

Alors maintenant, je pense que vous devez déclarer en haut des fonctions et aussi près que possible de la première utilisation. Alors les deux! Et la façon de le faire est d'utiliser des sous-programmes bien divisés.

Mais si vous travaillez sur une fonction longue, mettez les choses les plus proches de la première utilisation, car de cette façon, il sera plus facile d'extraire des méthodes.

Ma recette est la suivante. Pour toutes les variables locales, prenez la variable et déplacez sa déclaration vers le bas, compilez, puis déplacez la déclaration juste avant l'erreur de compilation. C'est la première utilisation. Faites ceci pour toutes les variables locales.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Maintenant, définissez un bloc de portée qui commence avant la déclaration et déplacez la fin jusqu'à ce que le programme se compile

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Cela ne compile pas car il y a plus de code qui utilise foo. Nous pouvons remarquer que le compilateur a pu parcourir le code qui utilise bar car il n'utilise pas foo. À ce stade, il y a deux choix. La mécanique consiste simplement à déplacer le "}" vers le bas jusqu'à ce qu'il se compile, et l'autre choix est d'inspecter le code et de déterminer si l'ordre peut être changé en:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Si l'ordre peut être changé, c'est probablement ce que vous voulez, car cela raccourcit la durée de vie des valeurs temporaires.

Une autre chose à noter, est-ce que la valeur de foo doit être préservée entre les blocs de code qui l'utilisent, ou pourrait-il simplement être un foo différent dans les deux. Par exemple

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

Ces situations nécessitent plus que ma procédure. Le développeur devra analyser le code pour déterminer ce qu'il doit faire.

Mais la première étape consiste à trouver la première utilisation. Vous pouvez le faire visuellement, mais parfois, il est simplement plus facile de supprimer la déclaration, d'essayer de la compiler et de la remettre au-dessus de la première utilisation. Si cette première utilisation est à l'intérieur d'une instruction if, mettez-la là et vérifiez si elle se compile. Le compilateur identifiera ensuite d'autres utilisations. Essayez de créer un bloc de portée qui englobe les deux utilisations.

Une fois cette partie mécanique terminée, il devient plus facile d'analyser où se trouvent les données. Si une variable est utilisée dans un gros bloc de portée, analysez la situation et voyez si vous utilisez simplement la même variable pour deux choses différentes (comme un "i" qui s'utilise pour deux boucles for). Si les utilisations ne sont pas liées, créez de nouvelles variables pour chacune de ces utilisations non liées.

Philippe Carphin
la source
0

Vous devez déclarer toutes les variables en haut ou "localement" dans la fonction. La réponse est:

Cela dépend du type de système que vous utilisez:

1 / Système embarqué (notamment lié à des vies comme Avion ou Voiture): Il permet d'utiliser la mémoire dynamique (ex: calloc, malloc, new ...). Imaginez que vous travaillez dans un très gros projet, avec 1000 ingénieurs. Et s'ils allouent une nouvelle mémoire dynamique et oublient de la supprimer (quand elle ne l'utilise plus)? Si le système embarqué fonctionne pendant une longue période, cela entraînera un débordement de pile et le logiciel sera corrompu. Pas facile de s'assurer de la qualité (le meilleur moyen est d'interdire la mémoire dynamique).

Si un avion fonctionne dans les 30 jours et ne s'arrête pas, que se passe-t-il si le logiciel est corrompu (lorsque l'avion est toujours en l'air)?

2 / Les autres systèmes comme le web, le PC (ont un grand espace mémoire):

Vous devez déclarer la variable "localement" pour optimiser l'utilisation de la mémoire. Si ces systèmes fonctionnent pendant une longue période et qu'un débordement de pile se produit (parce que quelqu'un a oublié de supprimer la mémoire dynamique). Faites simplement la chose simple pour réinitialiser le PC: P Cela n'a aucun impact sur la vie

Dang_Ho
la source
Je ne suis pas sûr que ce soit correct. Je suppose que vous dites qu'il est plus facile de vérifier les fuites de mémoire si vous déclarez toutes vos variables locales au même endroit? C'est peut- être vrai, mais je ne suis pas sûr de l'acheter. Quant au point (2), vous dites que déclarer la variable localement "optimiserait l'utilisation de la mémoire"? Ceci est théoriquement possible. Un compilateur pourrait choisir de redimensionner le cadre de la pile au cours d'une fonction pour minimiser l'utilisation de la mémoire, mais je n'en connais aucun qui fasse cela. En réalité, le compilateur convertira simplement toutes les déclarations "locales" en "fonction-start dans les coulisses".
QuinnFreedman
1 / Le système embarqué n'autorise parfois pas la mémoire dynamique, donc si vous déclarez toutes les variables en haut de la fonction. Lorsque le code source est généré, il peut calculer le nombre d'octets dont ils ont besoin dans la pile pour exécuter le programme. Mais avec la mémoire dynamique, le compilateur ne peut pas faire de même.
Dang_Ho
2 / Si vous déclarez une variable localement, cette variable n'existe que dans le crochet ouvert / fermé "{}". Ainsi, le compilateur peut libérer l'espace de la variable si cette variable "hors de portée". C'est peut-être mieux que de tout déclarer en haut de la fonction.
Dang_Ho
Je pense que vous êtes confus entre la mémoire statique et la mémoire dynamique. La mémoire statique est allouée sur la pile. Toutes les variables déclarées dans une fonction, quel que soit l'endroit où elles sont déclarées, sont allouées statiquement. La mémoire dynamique est allouée sur le tas avec quelque chose comme malloc(). Bien que je n'ai jamais vu un appareil qui en est incapable, il est préférable d'éviter l'allocation dynamique sur les systèmes embarqués ( voir ici ). Mais cela n'a rien à voir avec l'endroit où vous déclarez vos variables dans une fonction.
QuinnFreedman
1
Bien que je convienne que ce serait une façon raisonnable de fonctionner, ce n'est pas ce qui se passe dans la pratique. Voici l'assemblage proprement dit pour quelque chose qui ressemble beaucoup à votre exemple: godbolt.org/z/mLhE9a . Comme vous pouvez le voir, à la ligne 11, sub rsp, 1008alloue de l'espace pour tout le tableau en dehors de l'instruction if. Cela est vrai pour clanget gccà chaque version et niveau d'optimisation que j'ai essayé.
QuinnFreedman
-1

Je vais citer quelques déclarations du manuel de gcc version 4.7.0 pour une explication claire.

"Le compilateur peut accepter plusieurs standards de base, tels que 'c90' ou 'c ++ 98', et les dialectes GNU de ces standards, tels que 'gnu90' ou 'gnu ++ 98'. En spécifiant un standard de base, le compilateur acceptera tous les programmes suivant cette norme et ceux utilisant des extensions GNU qui ne la contredisent pas. Par exemple, '-std = c90' désactive certaines fonctionnalités de GCC incompatibles avec ISO C90, telles que les mots-clés asm et typeof, mais pas d'autres extensions GNU qui n'ont pas de signification dans ISO C90, comme l'omission du terme intermédiaire d'une expression?:. "

Je pense que le point clé de votre question est de savoir pourquoi gcc n'est-il pas conforme à C89 même si l'option "-std = c89" est utilisée. Je ne connais pas la version de votre gcc, mais je pense qu'il n'y aura pas de grande différence. Le développeur de gcc nous a dit que l'option "-std = c89" signifie simplement que les extensions qui contredisent C89 sont désactivées. Donc, cela n'a rien à voir avec certaines extensions qui n'ont pas de signification en C89. Et l'extension qui ne restreint pas le placement de la déclaration de variable appartient aux extensions qui ne contredisent pas C89.

Pour être honnête, tout le monde pensera qu'il devrait être totalement conforme à C89 à la première vue de l'option "-std = c89". Mais ce n'est pas le cas. Quant au problème de déclarer toutes les variables au début, c'est mieux ou pire, c'est juste une question d'habitude.

Junwanghe
la source
se conformer ne veut pas dire ne pas accepter les extensions: tant que le compilateur compile des programmes valides et produit les diagnostics requis pour les autres, il se conforme.
Rappelez
@Marc Lehmann, oui, vous avez raison lorsque le mot «conforme» est utilisé pour différencier les compilateurs. Mais lorsque le mot «conforme» est utilisé pour décrire certains usages, vous pouvez dire «Un usage n'est pas conforme à la norme». Et tous les débutants sont d'avis que les usages non conformes à la norme devraient provoquer une erreur.
junwanghe
@Marc Lehmann, au fait, il n'y a pas de diagnostic quand gcc voit l'utilisation qui n'est pas conforme à la norme C89.
junwanghe
Votre réponse est toujours erronée, car affirmer "gcc n'est pas conforme" n'est pas la même chose que "un programme utilisateur n'est pas conforme". Votre utilisation de conform est tout simplement incorrecte. D'ailleurs, quand j'étais débutant, je n'étais pas de l'avis que vous dites, donc c'est faux aussi. Enfin, il n'est pas nécessaire qu'un compilateur conforme diagnostique le code non conforme, et en fait, cela est impossible à implémenter.
Rappelez