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
Wykaaa scripsit:

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



Effectivement, ça éclaire pas mal de points, merci pour la référence.

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.



Toutes les références que j'ai cousulté conseillaient la méthode « des
refs et des defs » il semble donc y avoir unanimité (au moins théorique,
si j'en crois la mage cité) à ce sujet.

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

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".



Bonne nouvelle.

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.



Ok.

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.



Ok.

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).



A priori, j'étais parti sur l'idée de lire linéairement le
Braquelaire, dont je suis pour l'instant très content, mais
effectivement je devrais consulter un peu plus le K&R (d'autant plus que
je l'ai sous la main).

Je ne sais pas si tout est clair pour moi, il faudra que je réfléchisse
encore un peu là-dessus, je reviendrai à la charge avec d'autres
questions au besoin.

En attendant, je pense que ça me ferait du bien de mieux comprendre le
processus de compilation : ce qu'est un fichier objet, comme se passe
l'édition de liens (statique ou dynamique), etc. Si vous avez des bonnes
lectures (mais pas trop longues) à me conseiller à ce sujet, je suis
preneur.

Merci,
--
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 <gt49ca$hch$,
Antoine Leca écrit:

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).



Bon, pas de problème avec l'ancien code de MPFR: dans une (seule)
des unités de compilation, il y avait une tentative, et dans toutes
les unités de compilation, des déclarations (par extern).

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.)



Donc le bug serait lié à une version particulière de GCC? (Je crois
qu'on n'a pas pensé à demander le compilateur utilisé: certains
utilisent sous Mac OS X un GCC officiel, non modifié par Apple, et
il est possible que ça pose des problèmes dans certains cas...)

--
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
Vincent Lefevre
Dans l'article <49f5fb53$0$12659$,
Wykaaa écrit:

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



MPFR mixait les deux méthodes, mais c'était correct d'après la norme:
une seule tentative et des références. Ceci dit, ce n'est peut-être
pas une bonne idée.

--
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
Alexandre Bacquart
mpg wrote:
Alexandre Bacquart scripsit:

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".



Bonne nouvelle.

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.



Ok.

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.



Ok.

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).



A priori, j'étais parti sur l'idée de lire linéairement le
Braquelaire, dont je suis pour l'instant très content, mais
effectivement je devrais consulter un peu plus le K&R (d'autant plus que
je l'ai sous la main).



Cela reste toujours une des meilleures références (si ce n'est la
meilleure), bien que ses vertus pédagogiques soient relativement
médiocres de l'avis de beaucoup (et du mien). Mais je pense que tu as
passé le stade de l'initiation si tu commences à fricoter avec la norme.

Je ne sais pas si tout est clair pour moi, il faudra que je réfléchisse
encore un peu là-dessus, je reviendrai à la charge avec d'autres
questions au besoin.

En attendant, je pense que ça me ferait du bien de mieux comprendre le
processus de compilation : ce qu'est un fichier objet,



A ce stade, il me semble pour le moins étrange que tu n'en ai pas saisi
le sens. Un fichier objet est une étape intermédiaire dans le processus
(une fois que tu as l'exécutable, tu peux t'en débarrasser).

Le compilateur interprète l'unité de compilation et crée un fichier
objet contenant la synthèse binaire de cette unité (le code, les
variables...). Ces fichiers objets sont ensuite utilisés par l'éditeur
de liens pour n'en faire qu'un : le fichier bibliothèque ou le programme
exécutable (auquel cas, il lui faudra trouver la fonction main()
quelque-part comme point de départ).

Mais je ne suis pas sûr de t'éclaircir, que ne comprends-tu pas
exactement ? Des exemples ?

comme se passe
l'édition de liens (statique ou dynamique), etc.



L'éditeur de lien corrèle les fichiers objets. Il en prend plusieurs,
les "lie" ensemble en un seul fichier objet (cas d'une bibliothèque) ou
fichier exécutable (cas d'un programme) tout en vérifiant d'un point de
vue global ce que le compilateur n'aura pu vérifier lui-même de son
point de vue local (ie. unité de compilation). Ca, je crois que tu
l'avais compris.

L'affaire "statique ou dynamique", cela sort du cadre de la norme
(d'aucun me diront que les fichiers objets aussi). Pour faire court, ce
sont les deux types de bibliothèques qu'on peut avoir.

La bibliothèque statique est le cas le plus simple : c'est un fichier
objet (le plus souvent composé de plusieurs fichiers objets) qui sera
directement "lié" dans l'exécutable à l'édition de liens, au même titre
que n'importe quel fichier objet.

Une bibliothèque dynamique, c'est grosso modo pareil, sauf qu'elle sera
chargée "dynamiquement" à l'exécution (autrement dit, l'exécutable tout
seul ne fonctionnera pas s'il ne la trouve pas d'une manière ou d'une
autre). Cela permet, notamment, de n'avoir qu'un seul fichier
bibliothèque utilisé par plusieurs programmes (plutôt que d'avoir
plusieurs programmes contenant la même bibliothèque), et d'un autre
point de vue pratique, de pouvoir modifier la bibliothèque sans avoir à
recompiler les programmes (sous réserve que les modifications ne
changent rien à l'interface, bien-entendu).

La manière dont tout cela se passe (je suppose que ton "comme" =
"comment"), ma foi, cela dépend fortement de l'âge du capitaine : chaque
système a sa manière de faire, avec moult subtilités toutes aussi
barbantes les unes que les autres. Cela dépasse largement le cadre de ce
groupe... et puis il va peut-être falloir penser à changer le sujet de
l'enfilade si ça continue ;)

Si vous avez des bonnes
lectures (mais pas trop longues) à me conseiller à ce sujet, je suis
preneur.



Diantre ! le sujet est vaste, ça va être dur de trouver quelque-chose de
court... je suis sûr que les autres contributeurs vont te dégotter
quelque-chose de correct, mais court, il ne faut pas trop rêver.


--
Alex
Avatar
PIGUET Bruno
Le Fri, 24 Apr 2009 00:29:40 +0200, mpg a écrit :

[...]
Je change la deuxième ligne de b.c en 'extern float var;', le
comportement à l'exécution ne change pas, et à la compilation je ne me
prends toujours pas d'avertissement (compilé avec gcc -Wall -pedantic).



Désolé de ne pas avoir le temps de tester en local, mais il me semble
qu'un "-Wextra" m'avait aidé à trouver une erreur de ce type, une fois.

Bruno.
Avatar
mpg
Alexandre Bacquart scripsit:

En attendant, je pense que ça me ferait du bien de mieux comprendre le
processus de compilation : ce qu'est un fichier objet,



A ce stade, il me semble pour le moins étrange que tu n'en ai pas saisi
le sens. Un fichier objet est une étape intermédiaire dans le processus
(une fois que tu as l'exécutable, tu peux t'en débarrasser).



Oui, j'avais bien compris ça. Ce que je voulais dire, c'est que
j'aimerais me faire une idée plus précise de ce qu'il y a et de ce qu'il
n'y a pas dans un fichier objet. Par exemple je déduis de certains
messages de ce fil qu'il n'y a plus d'information sur le type des
objets, il semble clair par contre que leur identifiant est stocké pour
que le linker puisse faire son travail, je ne sais pas si c'est le cas
pour toutes les variables ou pas (ça me paraît inutile pour les
variables non-globales par exemple).

En fait, une question simple : si on compile le source avec l'option -S
de gcc, est-ce que le fichier assembleur obtenu contient exactement la
même information que le fichier .o ? Si oui, il me semble d'inspecter
les fichiers .s aide à répondre à pas mal de mes question (bon, ça
montre ce que mon compilateur fait, pas ce que le norme dit, mais c'est
quand même un support concret).

Mais je ne suis pas sûr de t'éclaircir, que ne comprends-tu pas
exactement ? Des exemples ?



Bah par exemple je ne comprenais pas trop comment était représenté dans
le .o les différences entre :

int i = 1; // définition
extern int i; // référence
int i; // common

(en supposant à chaque fois qu'il n'y a pas d'autre déclaration de i
dans le scope global dans l'unité de compilation courante).

En supposant que le .s contient bien exactement la même information que
le .o, mais simplement sous un formes différente (pas binaire), j'essaie
de compiler les fichiers suivants :

--- BEGIN a.c (ou aa.c ou aaa.c) ---
extern int var; // a.c
// int var; // aa.c
// int var = 1; // aaa.c

void inc_var(void) {
var++;
}
--- END a.c ---

Et je vois, dans aaa.s :

.globl var
.data
.align 4
.type var, @object
.size var, 4
var:
.long 1

qui semble bien traduire le fait que var est /définie/ (5 premières
lignes) puis initialisée (je parle pas trop assembleur, mais bon).

Dans aa.s, rien au début, donc pas de définition de var, mais un peu
plus bas :

.LFE2:
.size inc_var, .-inc_var
.comm var,4,4
.section .eh_frame,"a",@progbits

semble bien dire que var est une variable du /common/ et j'imagine que
4,4 correspondent aux valeurs de .align et .size ci-dessus.

Dans a.s, rien de tout ça, mais ce qui semble être des références à var
(qui apparaisent aussi dans les autres bien sûr) :

.LCFI1:
movl var(%rip), %eax
addl $1, %eax
movl %eax, var(%rip)
leave
ret

Bon, je suis sans doute un peu matérialiste, mais j'ai l'impression de
comprendre mieux quand je peux « voir » les choses comme ça.

L'éditeur de lien corrèle les fichiers objets. Il en prend plusieurs,
les "lie" ensemble en un seul fichier objet (cas d'une bibliothèque) ou
fichier exécutable (cas d'un programme) tout en vérifiant d'un point de
vue global ce que le compilateur n'aura pu vérifier lui-même de son
point de vue local (ie. unité de compilation). Ca, je crois que tu
l'avais compris.



Oui. Par contre, je n'avais pas trop compris de quelle information
précisément il disposait pour ce faire. En supposant toujours qu'un .s
est une réprésentation fidèle du contenu du .o, je crois que maintenant
j'y vois plus clair.

La bibliothèque statique est le cas le plus simple [snip explications]
Une bibliothèque dynamique, c'est grosso modo pareil, [snip aussi]
La manière dont tout cela se passe (je suppose que ton "comme" =
"comment"), ma foi, cela dépend fortement de l'âge du capitaine : chaque
système a sa manière de faire, avec moult subtilités toutes aussi
barbantes les unes que les autres. Cela dépasse largement le cadre de ce
groupe... et puis il va peut-être falloir penser à changer le sujet de
l'enfilade si ça continue ;)



Ok. Disons que je me contenterai de savoir si le fait de lier
statiquement ou dynamiquement change en quoi que ce soit la façon dont
on doit concevoir et réaliser son code en C, ou si c'est indépandant.

Diantre ! le sujet est vaste, ça va être dur de trouver quelque-chose de
court... je suis sûr que les autres contributeurs vont te dégotter
quelque-chose de correct, mais court, il ne faut pas trop rêver.



Bon, alors peut-être que j'en resterai là pour le moment. Chaque chose
en son temps.

--
Manuel Pégourié-Gonnard Institut de mathématiques de Jussieu
http://weblog.elzevir.fr/ http://people.math.jussieu.fr/~mpg/
Avatar
Pierre Maurette
mpg, le 27/04/2009 a écrit :

[...]

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 ?



int x;
int* p = &x;

x est-il défini ?

et (hum hum la norme):

volatile int x;

????

--
Pierre Maurette
Avatar
Antoine Leca
Le 28/04/2009 10:32, mpg écrivit :
En fait, une question simple : si on compile le source avec l'option -S
de gcc, est-ce que le fichier assembleur obtenu contient exactement la
même information que le fichier .o ?



Normalement, le .s a plus d'information que le .o, tu peux souvent
retrouver les noms des variables internes, des labels, les
sous-sections, etc.

Au niveau du code généré, c'est grosso modo bijectif, et les détails qui
font que cela ne l'est pas complètement (genre les instructions qui
possèdent deux noms, synonymes l'un de l'autre) ne sont d'aucun intérêt
pour les programmeurs.


Antoine
Avatar
Antoine Leca
Le 27/04/2009 23:00, Vincent Lefevre écrivit :
Dans l'article <gt49ca$hch$,
Antoine Leca écrit:
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.)



Donc le bug serait lié à une version particulière de GCC?



Pas vraiment, plutôt au fait que la version "HEAD" de GCC est différente
de celle de référence d'Apple sur ce sujet de la génération des
tentatives, ou (non exclusif) que les développeurs d'Apple ont décidé de
faire l'impasse sur les commons, qui ne sont pas obligatoire en C
normalisé (sais pas pour Posix) mais que GCC HEAD considère
manifestement comme un acquis.


Mais bon, je te réponds sans avoir passé de temps à chercher ce qu'il en
est réellement, donc méfiance.


Antoine
1 2 3