J'ai longtemps pensé qu'en C, toutes les variables devaient être déclarées au début de la fonction. Je sais qu'en C99, les règles sont les mêmes qu'en C++, mais quelles sont les règles de placement des déclarations de variables pour C89/ANSI C?
Le code suivant est compilé avec succès avec gcc -std=c89
et 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 c
et s
ne devraient-elles pas provoquer une erreur en mode C89/ANSI?
Il compile avec succès car GCC l’autorise sous la forme d’une extension GNU, bien qu’elle ne fasse pas partie de la norme C89 ou ANSI. Si vous souhaitez respecter strictement ces normes, vous devez réussir la -pedantic
drapeau.
Pour C89, vous devez déclarer toutes vos variables au début d'un bloc de portée.
Donc, votre char c
déclaration est valide car elle se trouve en haut du bloc de portée de la boucle for. Mais le char *s
déclaration devrait être une erreur.
Le regroupement des déclarations de variable en haut du bloc est un héritage probablement dû aux limitations d'anciens compilateurs C primitifs. Toutes les langues modernes recommandent et parfois même imposent la déclaration des variables locales au dernier point: l'endroit où elles sont initialisées. Parce que cela supprime le risque d'utiliser une valeur aléatoire par erreur. Séparer déclaration et initialisation vous empêche également d'utiliser "const" (ou "final") lorsque vous le pouvez.
Malheureusement, C++ continue d’accepter l’ancienne méthode de déclaration la plus ancienne pour la compatibilité ascendante avec le C (une traînée de compatibilité parmi beaucoup d’autres ...). Mais C++ essaie de s’écarter de cela:
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 scindé.
Du point de vue de la maintenabilité plutôt que de la syntaxe, il y a au moins trois courants de pensée:
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 d'un coup d'œil.
Déclarez toutes les variables aussi près que possible du lieu où elles ont été utilisées pour la première fois. Ainsi, vous saurez que pourquoi chacune d’elles est nécessaire.
Déclarez toutes les variables au début du bloc de portée le plus interne, afin qu'elles sortent du domaine le plus tôt possible et permettez au compilateur d'optimiser la mémoire et de vous avertir si vous les utilisez par inadvertance là où vous n'aviez pas prévu.
Je préfère généralement la première option, car je trouve que les autres me forcent souvent à traquer le code des déclarations. Définir toutes les variables à l’avance facilite également l’initialisation et le suivi depuis un débogueur.
Je vais parfois déclarer des variables dans un bloc de portée plus petit, mais seulement pour une bonne raison, dont j'ai très peu. Un exemple pourrait être après une fork()
, pour déclarer les variables nécessaires uniquement au processus enfant. Pour moi, cet indicateur visuel est un rappel utile de leur objectif.
Comme d'autres l'ont noté, GCC est permissif à cet égard (et éventuellement d'autres compilateurs, en fonction des arguments avec lesquels ils s'appellent) même en mode 'C89', à moins que vous n'utilisiez la vérification 'pédante'. Pour être honnête, il n'y a pas beaucoup de bonnes raisons de ne pas avoir le pédantisme; Un code moderne de qualité doit toujours être compilé sans avertissements (ou très peu de cas où vous savez que vous faites quelque chose de suspect pouvant être considéré comme une erreur possible par le compilateur). Par conséquent, si vous ne pouvez pas compiler votre code avec une configuration pédante, il devra probablement faire l'objet 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 autorisant la 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 'pour'.
Comme il a été noté, il existe deux écoles de pensée à ce sujet.
1) Tout déclarer au sommet 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 à cette question est FAIRE LES DEUX! Laissez-moi expliquer:
Pour les longues fonctions, 1) rend la refactorisation très difficile. Si vous travaillez dans une base de code où les développeurs s'opposent à l'idée de sous-routines, vous aurez 50 déclarations de variable au début de la fonction et certaines d'entre elles pourraient simplement être un "i" pour une boucle for qui est à la fois. bas de la fonction.
J'ai donc développé cette déclaration en essayant de faire l'option 2) religieusement.
Je suis revenu à l'option 1 à cause d'une chose: les fonctions courtes. Si vos fonctions sont suffisamment courtes, vous aurez peu de variables locales et, puisque la fonction est courte, si vous les mettez en haut de la fonction, elles seront toujours proches de la première utilisation.
En outre, l'anti-motif "déclarer et définir sur NULL" lorsque vous souhaitez déclarer en haut mais que vous n'avez pas effectué certains calculs nécessaires à l'initialisation est résolu car les éléments à initialiser seront probablement reçus sous forme d'arguments.
Alors maintenant, je pense que vous devriez déclarer au sommet des fonctions et le plus près possible de la première utilisation. Alors les deux! Et la manière de le faire est d'utiliser des sous-programmes bien divisés.
Mais si vous travaillez sur une fonction longue, placez les éléments les plus proches de la première utilisation, car il sera ainsi 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 en bas, compilez, puis déplacez la déclaration juste avant l'erreur de compilation. C'est la première utilisation. Faites cela 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'à la compilation du programme
{
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 un peu plus de code qui utilise foo. Nous pouvons remarquer que le compilateur a pu consulter le code qui utilise bar car il n’utilise pas foo. À ce stade, il y a deux choix. La solution mécanique consiste simplement à déplacer le "}" vers le bas jusqu'à la compilation, l'autre option consiste à inspecter le code et à déterminer si l'ordre peut être modifié en:
{
int foo = 0;
<code that uses foo>
}
<code that uses foo>
int bar = 1;
<code that uses bar>
Si vous pouvez modifier l'ordre, 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 s'agir simplement d'un foo différent dans les deux cas. 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 quoi faire.
Mais la première étape consiste à trouver le premier usage. 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 compile. Le compilateur identifiera ensuite d'autres utilisations. Essayez de créer un bloc d’étendue qui englobe les deux utilisations.
Une fois cette partie mécanique terminée, il devient alors plus facile d'analyser l'emplacement des données. Si une variable est utilisée dans un bloc de grande portée, analysez la situation et voyez si vous utilisez simplement la même variable pour deux choses différentes (comme un "i" utilisé pour deux boucles for for). Si les utilisations ne sont pas liées, créez de nouvelles variables pour chacune de ces utilisations.