Redéfinition d'une variable entre plusieurs fichier .c ?

Le problème exposé dans ce sujet a été résolu.

Bonjour,

[Bien que les débats sous mon dernier post ne semblent pas terminés, je me permets de reposter ce qui devrait être un très court topic]

Voici le contenu d’un très court fichier autre.c :


int toto = 0;

Son fichier .h associé :


#ifndef AUTRE_H
#define AUTRE_H

extern int toto;

#endif

Voici le contenu d’un (très court) fichier main.c :


#include "autre.h"

int toto;

int main()
{
	return 0;
}

Tout ceci compile sans problème. Je m’attendais à un problème de multiple définition de toto qui est une variable globale accessible partout dans mes deux fichiers et dans tout mon projet, mais non. Si je pousse le raisonnement, je peux donc include <errno.h> dans mon projet, sans forcément savoir que la variable errno est déclarée dans le fichier errno.c, et donc de mon point de vue ma variable errno peut se mettre à changer de valeur toute seule sans mon code, parce que le code de la librairie standard va la faire changer ? Donc quand j’inclus un .h de la librarie standard je dois bien me renseigner sur toutes les variables définies dedans au risque d’avoir un comportement complètement imprévisible ? Il ne me semble pas avoir entendu parler d’une telle précaution à prendre dans le cours… De plus cela pose un problème supplémentaire : si on n’inclut pas le .h de stdio par exemple, est-ce que le linker va lier mon projet avec le code de stdio.o (si c’est bien comme cela qu’il s’appelle ?) ? Est-ce que le linker va détecter tout seul qu’un .h de la librairie standard du C est inclus dans une de mes sources, et il va donc lier le code qu’il faut seulement si besoin ? Si ce n’est pas le cas et que le code de la librairie standard est toujours lié à mon projet même si je n’inclus pas les .h associés, alors on n’est jamais à l’abri qu’une variable déclarée dans mes sources interfère avec les variables globales déclarées dans les sources de la librairie standard ?

Par acquis de conscience j’ai aussi essayé avec ce code pour main.c :


#include "autre.h"

int toto = 0;   //cette fois on met à 0 toto dans main.c

int main()
{
	return 0;
}

Et cette fois ça ne compile pas. Un contournement du problème serait-il de toujours initialiser à la main nos variables globales ? (je dis à la main car elles sont initialisées à 0 automatiquement je crois)

+0 -0

Tu déclares une variable globale autant de fois que tu veux.
Par-contre, tu la définies qu’une seule fois.

+1 -0

Salut,

Si tu souhaites une approche plus détaillée de ce sujet, j’ai écrit un petit tutoriel sur les identificateurs qui voit cela de long en large (du point de vue de la norme). C’était un peu trop pointu et précis pour être vu dans le tutoriel principal.

Maintenant, pour répondre à ta question, ton premier exemple n’est pas conforme à la norme et sa compilation devrait effectivement échouer. Sur le plan de la norme :

  • tu as une définition dans le fichier autre.c (int toto = 0;), c’est une définition parce qu’il y a une initialisation ;
  • tu as une déclaration dans le fichier autre.h (et donc dans autre.c et main.c également après traitement par le préprocesseur), c’est une déclaration parce qu’elle est précédée du mot-clé extern;
  • tu as une définition potentielle dans le fichier main.c (int toto;). Étant donné qu’il n’y a pas de définition en plus de cette définition potentielle, une définition implicite avec une initialisation à zéro est ajoutée (int toto = 0;).

Finalement, tu te retrouves avec deux définitions, ce qui n’est pas permis. Cependant, il s’agit ici de ce qu’en dit la norme. En effet, sous Linux et *BSD et jusqu’à récemment, les définitions potentielles ne sont pas traitées de cette manière : elles sont considérées comme des déclarations du moment qu’il n’y a qu’une seule définition pour tout le programme, ce qui est ton cas. Il est possible d’adopter le comportement imposé par la norme via l’option -fno-common (raison pour laquelle elle est utilisée dans le tutoriel). Toutefois, depuis un certains temps, cette option est activée par défaut dans GCC (j’ignore depuis quand).

De plus cela pose un problème supplémentaire : si on n’inclut pas le .h de stdio par exemple, est-ce que le linker va lier mon projet avec le code de stdio.o (si c’est bien comme cela qu’il s’appelle ?) ? Est-ce que le linker va détecter tout seul qu’un .h de la librairie standard du C est inclus dans une de mes sources, et il va donc lier le code qu’il faut seulement si besoin ?

AScriabine

Par défaut, ton code sera lié à la bibliothèque C entière.
Edit : Le linker n’as pas connaissance des en-têtes, uniquement des symboles (typiquement les noms de variables et de fonctions).

+2 -0

@Taurre en effet, l’option de compilation a provoqué une erreur de compil. Je suis sous gcc elle n’était pas activée par défaut bien que j’ai téléchargé ce compilateur il y a quelques semaines.

J’ai essayé le code suivant :


#include <stdio.h>

int errno = 0;

int main()
{
	printf("coucou le monde\n");

	return 0;
}

La variable errno est définie dans la bibliothèque standard du C, donc le code précédent ne devrait pas compiler ? Pourquoi ne détecte-t-il pas de multiple définition entre la variable errno dans la bibliothèque du C et ma variable errno ? Je précise que je compile avec l’option de compilation que tu as suggérée dans ton précédent message.

+0 -0

La variable errno est définie dans la bibliothèque standard du C, donc le code précédent ne devrait pas compiler ? Pourquoi ne détecte-t-il pas de multiple définition entre la variable errno dans la bibliothèque du C et ma variable errno ?

errno est défini par errno.h. Si tu l’inclus, tu verras que ton compilateur refuse.

Chez moi (tout comme chez toi, probablement), l’erreur n’est pas très claire, mais c’est parce que errno semble implémenté avec une fonction.

+1 -0

@jmiven Le code suivant que tu me suggères compile parfaitement :


#include <stdio.h>
#include <errno.h>

int errno;

int main()
{
	printf("coucou le monde\n");

	return 0;
}

Et pour la parenthèse, définir une variable dans un .h me paraît absurde car elle se retrouverait alors définie dans tous les .c dans lesquels elle est incluse. Ce qui pour le coup deviendrait impossible à utiliser ? (à moins de faire des manips préprocesseurs plus complexes) Voici une illustration simple de mon propos :

autre.h :


#ifndef AUTRE_H
#define AUTRE_H

int toto;

#endif

autre.c :


#include "autre.h"

dans main.c :


#include <stdio.h>

#include "autre.h"

int main()
{
	printf("coucou le monde\n");

	return 0;
}

Ce projet ne compile pas car toto est définie plusieurs fois

EDIT

je fais le test du code suivant :


#include <stdio.h>
#include <errno.h>

int errno = 0;

int main()
{
	printf("coucou le monde\n");

	return 0;
}

Et il ne compile pas. Le message d’erreur : error: function '_errno' is initialized like a variable int errno = 0;

Je ne comprends pas cette erreur… Je précise bien que je compile avec la commande suivante : gcc -fno-common *.c -o "./Kit programmation C/Executable/exe" donc l’option suggérée par @Taurre est présente

+0 -0

Et il ne compile pas. Le message d’erreur : error: function '_errno' is initialized like a variable int errno = 0;

Ça c’est que errno est certainement redéfini comme une macro. ^^"

+1 -0


#include <stdio.h>
#include <errno.h>

#undef errno
int errno = 0;

int main()
{
	printf("coucou le monde\n");

	return 0;
}

Ça marche mieux ?

+2 -0

Ca fonctionne, oui :waw: errno n’est donc pas une variable mais une macrofonction… Les bras m’en tombent

AScriabine

Et oui, errno n’est pas une simple variable. Cela est dû au fait que dans un environnement multi-threadé, chaque thread doit avoir sa propre 'variable' errno. On accède donc à cette donnée à travers une fonction. D’où errno est souvent codé comme une macro qui utilise une fonction qui fera que l’on accède au 'errno' du thread courant.

Ca fonctionne, oui :waw: errno n’est donc pas une variable mais une macrofonction… Les bras m’en tombent

AScriabine

En cas de doute sur un sujet comme ça tu peux aussi aller directement consulter le contenu du fichier d’en-tête (errno.h ici) sur ton système.
C’est pas toujours très clair mais tu y trouveras différentes informations notamment celle-ci (mais ça reste dépendant de l’implémentation présente sur ton système).

Salut,

Et pour la parenthèse, définir une variable dans un .h me paraît absurde car elle se retrouverait alors définie dans tous les .c dans lesquels elle est incluse.

C’est pour ça que définir une simple variable genre int toto = 3; dans un header fait râler clang qui va te dire que tu risques de violer l’ODR (One Definition Rule). C’est pareil avec les fonctions d’ailleurs.

Et de manière générale, le modèle de header files à la C est effectivement complètement pourri : tu es essentiellement obligé de déclarer ton API deux fois, t’as pas de scoping propre ni d’import partiel, pas de namespacing non plus. Donc tu te retrouves rapidement avec une soupe de symboles et des risques de conflits. C’est pas pour rien que tous les langages modernes utilisent des namespaces et des modules (même C++ s’y met) pour éviter les problèmes inhérents aux header files.

+4 -0

Il ne faut pas confondre deux choses aussi :

  • Les constantes
  • Les variables globales

Généralement, les variables globales de type "paramètres du code" sont même carrément définies via des macros, et pas comme des extern const.

Mais ton premier code, si au moment de la définition, tu rajoutes un const, ton compilateur va salement râler quand tu voudras la modifier.

Je précise car il arrive souvent qu’au début on confonde les deux. Ce sont deux notions souvent utilisées ensemble mais absolument pas similaires.

Dernière chose : il faut absolument t’enlever de la tête que "ça compile donc mon code est bon". C’est absolument faux en C (c’est valable beaucoup plus dans les langages fonctionnels/fortement typés, ce que n’est pas le C). Le compilateur C a juste besoin, pour chaque variable, de savoir quelle taille elle va occuper. Et c’est à peu près tout, le reste, il s’en tamponne la nouille avec une babouche. Bref, il a des prérequis assez vagues, qui fait que le programme peut foirer durant l’exécution de l’exécutable (ah, les SIGSEV :p ). Typiquement, en changeant la valeur de tes variables globales, si tu le fais en dehors de fonctions, je pense que tu peux avoir des effets de bord assez méchants qui font que tu ne pourras jamais être sûr de la valeur de ta variable globale. Bref, les variables globales, c’est à éviter.

@Lyph: Il me semble que personne n’a confondu avec const ici.

Une variable globale doit être définie comme static si elle ne concerne qu’un seul fichier. Comme cela tu as un contrôle total sur les valeurs de cette variable globale.

+0 -0

Je reviens sur une phrase que je n’ai pas compris.

en changeant la valeur de tes variables globales, si tu le fais en dehors de fonctions.

🤔
En dehors de fonctions ?

+1 -0

Typiquement, en changeant la valeur de tes variables globales, si tu le fais en dehors de fonctions Bref, les variables globales, c’est à éviter.

Lyph

Sauf erreur de ma part, on ne peut pas coder une instruction en dehors d’une fonction en C (hormis une déclaration de variable + initialisation avec une constante). Puisque rien ne dirait quand l’exécuter ou dans quel ordre. C’est d’ailleurs pour ça qu’on ne peut pas déclarer de variable globale comme ça : int toto = titi; : il faut initialiser avec des constantes pour que chaque initialisation soit indépendantes des autres lignes de code. (bon je suis débutant je ne sais pas si les termes que j’emploie sont précis)

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte