OVH Cloud OVH Cloud

Effet et usage d'extern ?

30 réponses
Avatar
mpg
Bonjour,

Je suis en train de lire le bouquin de Braquelaire pour essayer
d'acquérir quelques bases consistantes en C (j'ai déjà bidouillé un peu
de C en me basant essentiellement ce que j'avais autrefois appris en
Pascal et sur une lecture très partielle et rapide du K&R).

Je viens de finir le chapitre deux (déclarations) et n'arrive toujours
pas à comprendre ce que fait et à quoi sert la spécification de
rangement 'extern', alors que je devrais l'avoir compris à ce stade de
ma lecture, si j'ai bien saisi l'organisation du bouquin (à moins que le
secret ne réside dans le chapitre sur la modularisation). Je viens donc
demander votre aide.

Les exemples présentés sur l'usage d'extern font en général intervenir
deux fichiers, mettons a.c et b.c, qui utilisent une même variable
globale var. Ils évoquent le fait que si on n'y prend pas garde, on peut
par exemple déclarer var comme int dans a.c, comme float dans b.c, et
que les deux désigneront quand même le même objet. Je comprends très
bien pourquoi un tel état de fait est à éviter à tout prix (je ne
comprends même pas que le compilateur (ou l'éditeur de liens, enfin
quelqu'un) ne m'envoie pas sur les roses si j'essaie de faire ça, en
fait).

Ensuite on voit comment l'inclusion dans a.c et b.c d'un même fichier
d'en-tête déclarant var évite de problème (cette fois, le compilateur
geule si on fait n'importe quoi). Bien. Je n'ai pas saisi le rôle
qu'extern était censé jouer là-dedans.

J'ai cherché dans la FAQ du groupe, la question 5.3 indique aussi
conseille aussi d'utiliser extern pour ses déclarations de variables
globales (quand on tient à en utiliser), et précise : « Ceci évitera des
redéfinitions de la variable lors des inclusions d'en-têtes. » Je
persiste à avoir l'impression que ce qui évite la re-définition de la
variable, c'est l'inclusion du fichier d'en-tête, pas la déclaraiont
'extern'.

Merci d'avance pour tout éclaircissement.

PS : je ne sais pas si la question est reliée, mais pour une variable, y
a-t-il une différence entre déclaration et définition ?

--
Manuel Pégourié-Gonnard Institut de mathématiques de Jussieu
http://weblog.elzevir.fr/ http://people.math.jussieu.fr/~mpg/

10 réponses

1 2 3
Avatar
mpg
Antoine Leca scripsit:

Le 23/04/2009 22:29, mpg écrivit :
--- BEGIN a.c ---
float var;

--- BEGIN b.c ---
int var = 1;




Je compile tout ça,



On s'arrêtera là si tu veux bien.
Certains compilateurs (c'est la tradition Unix) acceptent cela. Mais pas
tous. D'autres vont gueuler, en particulier certains vont te dire que
«var» est _défini_ à deux endroits distincts, et c'est un endroit de trop.



Ok. Je suis tout à fait content de savoir que certains compilateurs vont
gueuler à cet endroit, je les approuve.

Utilise gcc -W -Wall : -pedantic ne sert en pratique à rien, et
certaines versions de GCC ont des options dans -W qui ne sont pas dans
-Wall (et de mémoire, cela a une influence sur l'édition des liens).



Merci pour ce conseil. Je n'avais en effet pas saisi que -W n'était pas
un sous-ensemble de -Wall.

De fait, extern (devant la déclaration de float var, par exemple) sert à
dire au compilateur « ceci n'est *pas* une définition de var, seulement
une déclaration » (c'est-à-dire la velléité de pouvoir utiliser var un
peu plus loin dans ce source). Le résultat sera donc accepté par les
compilateurs C.



Ok, je pense que maintenant c'est clair.

Comme tu le remarques, cela ne résout pas le problème de la disjonction
des types (et les options de GCC ne vont pas aider: l'information du
type de la variable a été perdue lors de la phase compilation, et à la
phase édition-des-liens qui suit, seul moment où on met en relation a et
b, GCC n'a plus la possibilité de s'apercevoir du problème).
Pour résoudre cela, il faut ajouter dans "ab.h"
extern int var;
Et à partir de là, il n'est plus possible de laisser passer l'erreur à
la compilation de b.c.



Ok, donc il y a bien deux problèmes indépendants : distinguer
déclaration de définition (ça c'est le rôle de extern), et éviter le
problème des disjonction de types : ça c'est le rôle du fichier
d'en-tête commun. J'avais bien saisi le deuxième problème de prime
abord, mais pas le premier : c'est maintenant chose faite.

Déclarer les variables globales dans des fichiers d'en-tête devrait être
obligatoire, en tous cas c'est une bonne habitude de documentation à
prendre (indépendamment du sujet extern).



C'est bien noté.

--
Manuel Pégourié-Gonnard Institut de mathématiques de Jussieu
http://weblog.elzevir.fr/ http://people.math.jussieu.fr/~mpg/
Avatar
Vincent Lefevre
Dans l'article ,
mpg <mpg+ écrit:

Merci. J'en a profité pour aller lire aussi #definitions. Du coup je
crois avoir compris qu'en fait 'int x;' est une définition de x qui vaut
aussi déclaration, alors que 'extern int x;' est une déclaration mais
pas une définition, c'est-à-dire que le compilateur ne va pas réserver
d'espace mémoire pour x, comptant trouver une définition de x plus tard
ailleurs.



Non. Tout d'abord, le extern est souvent facultatif. 6.2.2#5:

If the declaration of an identifier for a function has no
storage-class specifier, its linkage is determined exactly as if
it were declared with the storage-class specifier extern. If the
declaration of an identifier for an object has file scope and no
storage-class specifier, its linkage is external.

"extern int x;" est une déclaration uniquement. "int x;" est une
déclaration, mais aussi une tentative de définition. 6.9.2#2:

A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or
with the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains
no external definition for that identifier, then the behavior is
exactly as if the translation unit contains a file scope declaration
of that identifier, with the composite type as of the end of the
translation unit, with an initializer equal to 0.

Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).

À noter que le linkeur de Mac OS X (10.5.x uniquement il me semble[*])
est buggé: quand il y a une tentative, mais pas de définition externe,
le linkeur considère que l'identifieur n'est pas défini (il ne respecte
pas 6.9.2#2), et le link échoue si l'identifieur est utilisé.

[*] Alors que MPFR fonctionnait bien avec 10.4.x, on a eu un rapport
de bug par un utilisateur de 10.5.x. J'ai alors demandé de retester
avec des définitions au lieu de tentatives, et le problème a disparu!
Comme les tentatives (au lieu de définitions) n'étaient pas vraiment
nécessaires, on les a remplacées par des définitions.

D'ailleurs, si j'essaie de compiler un programme avec deux fichiers et
que je mets 'extern int var;' dans les deux sans qu'aucun ne contienne
de définition de var, alors effectivement ça foire en me disant que
"undefined reference to `var'" à l'édition de liens.



Si var n'est jamais utilisé, ça ne devrait pas foirer (6.9#5).

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)
Avatar
espie
In article <20090427014823$,
Vincent Lefevre <vincent+ wrote:
Dans l'article ,
mpg <mpg+ écrit:

Merci. J'en a profité pour aller lire aussi #definitions. Du coup je
crois avoir compris qu'en fait 'int x;' est une définition de x qui vaut
aussi déclaration, alors que 'extern int x;' est une déclaration mais
pas une définition, c'est-à-dire que le compilateur ne va pas réserver
d'espace mémoire pour x, comptant trouver une définition de x plus tard
ailleurs.



Non. Tout d'abord, le extern est souvent facultatif. 6.2.2#5:

If the declaration of an identifier for a function has no
storage-class specifier, its linkage is determined exactly as if
it were declared with the storage-class specifier extern. If the
declaration of an identifier for an object has file scope and no
storage-class specifier, its linkage is external.

"extern int x;" est une déclaration uniquement. "int x;" est une
déclaration, mais aussi une tentative de définition. 6.9.2#2:

A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or
with the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains
no external definition for that identifier, then the behavior is
exactly as if the translation unit contains a file scope declaration
of that identifier, with the composite type as of the end of the
translation unit, with an initializer equal to 0.

Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).



Marrant, j'ai l'impression que ca a ete specifie en C99.
J'avais comme souvenir qu'il y avait un bout "implementation dependent"
dans tout ca. J'ai pas de norme C89 sous la main, mais de memoire du
Harbison & Steele, le modele classique est le modele inspire du "common"
du Fortran, ou tu peux avoir effectivement plusieurs tentative definitions
de la meme variable dans diverses unites de compilation, et ou le linker va
fusionner toutes les definitions (caveat: meme type, bien sur), mais ce n'est
pas le seul modele, et meme en C, il peut tres bien se faire que tu aies un
linker qui gueule si tu as plusieurs definitions de la meme variable.
Evidemment, c'est peu frequent sous Unix, mais ca se rencontrait dans la
nature... et il me semble que c'etait explicite en C89.
Avatar
mpg
Vincent Lefevre scripsit:

Merci. J'en a profité pour aller lire aussi #definitions. Du coup je
crois avoir compris qu'en fait 'int x;' est une définition de x qui vaut
aussi déclaration, alors que 'extern int x;' est une déclaration mais
pas une définition, c'est-à-dire que le compilateur ne va pas réserver
d'espace mémoire pour x, comptant trouver une définition de x plus tard
ailleurs.



Non.



Bon, merci de ne pas me laisser dans l'erreur.

Tout d'abord, le extern est souvent facultatif. 6.2.2#5:

If the declaration of an identifier for a function has no
storage-class specifier, its linkage is determined exactly as if
it were declared with the storage-class specifier extern. If the
declaration of an identifier for an object has file scope and no
storage-class specifier, its linkage is external.




Donc si je comprends bien, les déclarations de fonctions, ainsi que
celles de variables dont la porté est le fichier ont un extern implicite
sauf si on dit explicitement autre chose.

"extern int x;" est une déclaration uniquement. "int x;" est une
déclaration, mais aussi une tentative de définition. 6.9.2#2:

A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or
with the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains
no external definition for that identifier, then the behavior is
exactly as if the translation unit contains a file scope declaration
of that identifier, with the composite type as of the end of the
translation unit, with an initializer equal to 0.

Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).



Bon, je crois que mon problème (un de mes problèmes) est que je ne
saisis pas la différence entre déclaration et définition, à la fois au
niveau syntaxique et au niveau sémantique, en ce qui concerne les
variables.

Pour l'instant, j'ai tendance à croire que déclarer un identifiant, ça
revient à dire au compilateur « ceci est l'indentifiant d'un objet de
tel type, ne gueule pas si tu me vois utiliser cet identifiant
conformément à ce type » alors que la définition, c'est ce qui fait que
le compilateur va effectivement réserver un place en mémoire pour
l'objet. Suis-je à côté de la plaque sur ce point ?

Par contre, niveau syntaxique, j'ai beaucoup de mal à voir ce qui
différencie les deux. J'ai l'impression qu'une déclaration qui comporte
aussi une initialisation est de fait une définition. Pour le reste,
visiblement le compilateur ne se décide pas au moment où il lit la
déclaration pour savoir si c'est aussi une définition : il attend la fin
de l'unité de compilation (le fichier ?) pour fusionner les déclarations
et en faire quelque chose ? Quoiqu'en lisant le message de Marc, il
semble qu'il y a aussi l'éditeur de lien qui fusionne les définitions
(ou les tentatives)...

Bon, je vais essayer d'aller fouiller dans la norme pour voir si j'y
trouve une définition des expressions suivantes :
- déclaration
- définition
- « tentative definition »
Mais la dernière fois que j'ai essayé de chercher des trucs dans la
norme, j'étais un peu perdu.

D'ailleurs, si j'essaie de compiler un programme avec deux fichiers et
que je mets 'extern int var;' dans les deux sans qu'aucun ne contienne
de définition de var, alors effectivement ça foire en me disant que
"undefined reference to `var'" à l'édition de liens.



Si var n'est jamais utilisé, ça ne devrait pas foirer (6.9#5).



J'ai oublié de préciser que var était utilisé.

--
Manuel Pégourié-Gonnard Institut de mathématiques de Jussieu
http://weblog.elzevir.fr/ http://people.math.jussieu.fr/~mpg/
Avatar
mpg
mpg scripsit:

J'ai oublié de préciser que var était utilisé.



Puis j'ai failli oublier autre chose : merci à toi et à Marc pour vos
réponses détaillées, et désolé d'être un peu long à la comprenette.

--
Manuel Pégourié-Gonnard Institut de mathématiques de Jussieu
http://weblog.elzevir.fr/ http://people.math.jussieu.fr/~mpg/
Avatar
Antoine Leca
Le 27/04/2009 11:03, mpg <mpg+ écrivit :
désolé d'être un peu long à la comprenette.



Oh, il n'y a vraiment pas de quoi être désolé, si tu as tout compris au
sujet des déclarations/définitions/tentatives en si peu de temps, tu
serais plutôt dans les rapides...


Antoine
Avatar
Antoine Leca
Le 27/04/2009 8:51, Marc Espie écrivit :
In article <20090427014823$,
Vincent Lefevre wrote:
Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).



Marrant, j'ai l'impression que ca a ete specifie en C99.



Pas de différences entre les deux normes à ce niveau.

J'avais comme souvenir qu'il y avait un bout "implementation dependent"
dans tout ca.



Yep.

J'ai pas de norme C89 sous la main, mais de memoire



Cf. le rationale, pages 32-34 (imprimées) de la version C99.
Et c'est nettement plus complexe que ce que tu as écrit...


Antoine
Avatar
Antoine Leca
Le 27/04/2009 2:30, Vincent Lefevre écrivit :
Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).



À la condition expresse que cela se passe dans la même unité de
compilation ! Si tu as des tentatives dans deux unités distinctes, cela
devrait être interprété comme deux déclarations externes, et peut donner
des erreurs à l'éditions des liens (techniquement le comportement est
indéfini, viol de /shall/ dans 6.9p5).


À noter que le linkeur de Mac OS X [...] est buggé:



Bzzz. Si quelque chose est bogué à ce niveau, du point de vue de la
conformité C ce sera toujours le compilateur en propre, pas l'éditeur de
liens. Cf. le rationale, qui explique que le modèle choisi pour C89 est
justement un hybride où les diverses tentatives d'une même unité sont
promues au rang de définitions externes (ce que fait le compilo Unix
depuis toujours et en toutes circonstances), *mais* que par la suite
(donc, à l'édition des liens), il ne faut plus voir qu'une seule tête ;
et que ce modèle est le résultat d'âpres négociations justement en ce
qui concerne les éditeurs de liens, et qu'il est différent à la fois de
celui prévu au départ ou de celui spécifié par K&R (donc ce n'est pas
prêt de changer...)

Donc à moins que le linker version machin ne sache pas lire correctement
le Mach-o fourni par le compilateur (ce qui semblerait surprenant), le
défaut réel est que le compilateur génère une définition (probablement
un common, plus précisement un .undef de taille non nulle) qui n'est
pas^W plus conforme à ce qu'attend cette version-là de ld ; autrement
dit, la solution correcte du point de vue de la norme devrait être de
modifier le compilateur pour ne plus générer de variables
communautarisées... (évidemment, d'un point de vue plus général de
qualité de l'implémentation, cela ne sera pas résolu de cette façon,
c'est ld qui va plier devant GCC + la quantité de code qui assume de
facto le modèle Unix.)



Antoine
Avatar
Wykaaa
Vincent Lefevre a écrit :
Dans l'article ,
mpg <mpg+ écrit:

Merci. J'en a profité pour aller lire aussi #definitions. Du coup je
crois avoir compris qu'en fait 'int x;' est une définition de x qui vaut
aussi déclaration, alors que 'extern int x;' est une déclaration mais
pas une définition, c'est-à-dire que le compilateur ne va pas réserver
d'espace mémoire pour x, comptant trouver une définition de x plus tard
ailleurs.



Non. Tout d'abord, le extern est souvent facultatif. 6.2.2#5:

If the declaration of an identifier for a function has no
storage-class specifier, its linkage is determined exactly as if
it were declared with the storage-class specifier extern. If the
declaration of an identifier for an object has file scope and no
storage-class specifier, its linkage is external.

"extern int x;" est une déclaration uniquement. "int x;" est une
déclaration, mais aussi une tentative de définition. 6.9.2#2:

A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or
with the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains
no external definition for that identifier, then the behavior is
exactly as if the translation unit contains a file scope declaration
of that identifier, with the composite type as of the end of the
translation unit, with an initializer equal to 0.

Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).

À noter que le linkeur de Mac OS X (10.5.x uniquement il me semble[*])
est buggé: quand il y a une tentative, mais pas de définition externe,
le linkeur considère que l'identifieur n'est pas défini (il ne respecte
pas 6.9.2#2), et le link échoue si l'identifieur est utilisé.

[*] Alors que MPFR fonctionnait bien avec 10.4.x, on a eu un rapport
de bug par un utilisateur de 10.5.x. J'ai alors demandé de retester
avec des définitions au lieu de tentatives, et le problème a disparu!
Comme les tentatives (au lieu de définitions) n'étaient pas vraiment
nécessaires, on les a remplacées par des définitions.

D'ailleurs, si j'essaie de compiler un programme avec deux fichiers et
que je mets 'extern int var;' dans les deux sans qu'aucun ne contienne
de définition de var, alors effectivement ça foire en me disant que
"undefined reference to `var'" à l'édition de liens.



Si var n'est jamais utilisé, ça ne devrait pas foirer (6.9#5).




Je pense que cette page résume bien le sujet :
http://www-clips.imag.fr/commun/bernard.cassagne/Introduction_ANSI_C/node113.html

Je précise que je suis partisan des "refs et des defs" et non de la
méthode dite du "common", avec une seule def et la ref dans un fichier
d'include.
Avatar
Alexandre Bacquart
mpg wrote:
Vincent Lefevre scripsit:

Merci. J'en a profité pour aller lire aussi #definitions. Du coup je
crois avoir compris qu'en fait 'int x;' est une définition de x qui vaut
aussi déclaration, alors que 'extern int x;' est une déclaration mais
pas une définition, c'est-à-dire que le compilateur ne va pas réserver
d'espace mémoire pour x, comptant trouver une définition de x plus tard
ailleurs.


Non.



Bon, merci de ne pas me laisser dans l'erreur.

Tout d'abord, le extern est souvent facultatif. 6.2.2#5:

If the declaration of an identifier for a function has no
storage-class specifier, its linkage is determined exactly as if
it were declared with the storage-class specifier extern. If the
declaration of an identifier for an object has file scope and no
storage-class specifier, its linkage is external.




Donc si je comprends bien, les déclarations de fonctions, ainsi que
celles de variables dont la porté est le fichier ont un extern implicite
sauf si on dit explicitement autre chose.

"extern int x;" est une déclaration uniquement. "int x;" est une
déclaration, mais aussi une tentative de définition. 6.9.2#2:

A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or
with the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains
no external definition for that identifier, then the behavior is
exactly as if the translation unit contains a file scope declaration
of that identifier, with the composite type as of the end of the
translation unit, with an initializer equal to 0.

Donc tu peux très bien mettre plusieurs "int x;" sans extern, ce
qui aurait été impossible si c'était une définition (puisqu'il ne
peut y avoir qu'au plus une définition).



Bon, je crois que mon problème (un de mes problèmes) est que je ne
saisis pas la différence entre déclaration et définition, à la fois au
niveau syntaxique et au niveau sémantique, en ce qui concerne les
variables.

Pour l'instant, j'ai tendance à croire que déclarer un identifiant, ça
revient à dire au compilateur « ceci est l'indentifiant d'un objet de
tel type, ne gueule pas si tu me vois utiliser cet identifiant
conformément à ce type » alors que la définition, c'est ce qui fait que
le compilateur va effectivement réserver un place en mémoire pour
l'objet. Suis-je à côté de la plaque sur ce point ?



Non, tu es très proche de ce qu'en dit le K&R (c'est quand-même moins
velu que la norme) :

"une définition crée une variable, et lui réserve de la place mémoire;
une déclaration précise la nature d'une variable, mais sans lui réserver
de place en mémoire".

La norme ne fait qu'officialiser cela dans les détails.

Par contre, niveau syntaxique, j'ai beaucoup de mal à voir ce qui
différencie les deux. J'ai l'impression qu'une déclaration qui comporte
aussi une initialisation est de fait une définition.



Impression justifiée.

Tu ne peux pas avoir :

int x = 1;
int x = 1;

Mais tu peux avoir :

int x;
int x;

Ou même :

int x;
int x = 1;
int x;

Le compilo se démerde pour valider cette redondance, sans warning.

Pour le reste,
visiblement le compilateur ne se décide pas au moment où il lit la
déclaration pour savoir si c'est aussi une définition : il attend la fin
de l'unité de compilation (le fichier ?)



L'unité de compilation est le résultat du fichier source et des en-têtes
qui y sont inclus. Au final, oui, il n'y a qu'un fichier aux yeux du
compilo, puisque tout cela est fait par le préprocesseur.

pour fusionner les déclarations
et en faire quelque chose ? Quoiqu'en lisant le message de Marc, il
semble qu'il y a aussi l'éditeur de lien qui fusionne les définitions
(ou les tentatives)...

Bon, je vais essayer d'aller fouiller dans la norme pour voir si j'y
trouve une définition des expressions suivantes :
- déclaration
- définition
- « tentative definition »
Mais la dernière fois que j'ai essayé de chercher des trucs dans la
norme, j'étais un peu perdu.



S'il y a un exercice difficile en C, c'est bien d'interpréter la
norme... quand on débute, il est peut-être plus judicieux d'avoir une
bonne référence qui vulgarise. Le K&R est très bien pour ça (mais
strictement, il ne vulgarise pas la norme, il la précède).


--
Alex
1 2 3