Lorsque sur le forum, vous venez avec un code qui pose problème, vous pouvez être tenté de copier-coller des bouts votre code, ou encore pire, la totalité de celui-ci. Pourquoi est-ce une mauvaise idée ? Parce que :
Votre code contient beaucoup de choses qui n’ont aucun rapport avec le problème
Les gens abandonnent la lecture devant autant de code
On ne peut pas tester le code nous même
Tout le monde perd du temps parce que le problème est perdu dans le foulli du
code
Ce qu’il faut faire est de présenter un code minimal. C’est à dire d’extraire les lignes de code concernées de votre programme et d’écrire un nouveau programme les utilisants. Peut importe que le nouveau code ne fasse pas exactement la même chose que le programme original, l’important est que cela reproduise l’erreur.
Il faut absolument que :
Le code soit complet, que l’on puisse le tester
Le code soit le plus simple possible. Idéalement un seul main(), on ne veut
pas s’embêter à mettre en place un projet complexe pour tester votre code
Le code reproduise le problème initial
Le programme ne dépende pas de ressources externes (sauf si ça concerne le
problème)
En réduisant ainsi votre code, il y a de bonne chance pour que vous trouviez tout seul la solution à votre problème : les choses sont plus claires et plus évidente avec peu de code. Et si vous ne trouvez pas, quelqu’un vous donnera sans doute une réponse très rapidement puisque la quantité de code à lire est petite.
C’est un sujet qui pose pas mal de problèmes aux débutants, et même parfois aux moins débutants parce qu’il n’est pas si bateau qu’il n’y parait. Pour être parfaitement à l’aise avec la programmation modulaire, il faut avoir bien compris les deux étapes de la création d’un programme : la compilation et l’édition de lien.
Je vais supposer ici que vous avez lu la partie du tutoriel de M@teo21 sur la programmation modulaire ou que vous êtes tout du moins un peu familiarisé avec l’inclusion de headers. Normalement à ce stade vous savez à peu près faire mais c’est encore flou dans votre tête et parfois vous avez des erreurs bizarres du
style undefined reference to function ’foo’ in bar.o que vous mettez beaucoup de temps à résoudre.
NB : je vous invite à lire la bio de nepser pour savoir comment remédier a cette erreur de manière pratique. Ici vous comprendrez un peu mieux de quoi il s’agit.
Déclaration, Définition
J’aimerai exposer ici brièvement deux mots de vocabulaires qui nous seront utile par la suite pour comprendre le rôle des headers. En C, on fait la distinction entre la déclaration d’un symbole (i.e. une variable ou une fonction) et sa définition.
Déclarer un symbole, c’est juste signaler au compilateur que ce nom (associé, à un type pour une variable, un prototype pour une fonction) existe.
Définir un symbole, c’est allouer un espace mémoire effectif pour stocker cette
variable ou fonction.
Les variables sont, la plupart du temps, déclarées et définies en même temps. Les lignes suivantes sont des exemples de déclarations/définitions.
/* Les variables sont habituellement déclarés et définis en une fois */int truc; // déclaration et définitionchar lettre = 'F'; // déclaration, définition et initialisationfloat tab[6]; // déclaration et définition
Pour les fonctions vous connaissez déjà. On déclare une fonction en donnant son prototype et on la définit en écrivant son corps.
voidAffiche(void); // déclarationvoidAffiche(void)// déclaration et définition{
printf("test");
}
Notez que l’on peut déclarer autant de fois que l’on souhaite un symbole, à condition d’être cohérent sur son (proto)type. En revanche la définition doit être unique dans le programme, sans quoi l’éditeur de lien nous informera d’une définition multiple (on y reviendra).
Implicit declaration, undefined reference
Ce sont des messages qui reviennent très souvent et nous allons les décrypter
ici. Afin de se donner des outils pour travailler nous allons créer le « Hello
world » suivant.
Si vous êtes perdus à ce stade retournez apprendre les base du C.
Plaçons maintenant la fonction HelloWorld dans un fichier séparé hello.c, on a alors.
Lancez la compilation/édition de lien. Vous vous en doutez, cela ne devrait pas marcher. En fait, deux cas de figures sont possible :
Si le fichier hello.c a bien été ajouté a votre projet, vous aurez un warning Warning : implicit declaration of function 'HelloWorld' in main.c. C’est seulement un warning, le programme est bien créé et s’exécute normalement (c’est exceptionnel, en temps normal un warning c’est mauvais signe !)
Sinon vous aurez , en plus du warning ci-dessus, un affreux Error : undefined reference to function 'HelloWorld' in main.o
Réfléchissons à ce qui viens de se passer en commençant par le warning. En fait c’est plutôt simple, le compilateur en travaillant sur le fichier main.c voit un appel à la fonction 'HelloWorld' dont il n’a jamais entendu parler. Il se retrouve un peu bloqué, il doit placer dans le code machine généré un appel à une fonction dont il ignore l’existence. Il pourrait s’arrêter ici tout net en refusant d’aller plus loin. Mais décidément le compilateur est bonne pomme, alors il va faire un acte de fois et déclarer la fonction de lui même pour pouvoir continuer. Cependant comme ce genre d’initiative peut être mauvaise il vous prévient de cette déclaration implicite avec le fameux warning implicit declaration. Le compilateur pari sur le fait
que vous maitrisez la situation et suppose que la fonction 'HelloWorld' existe bien quelque part.
Il est important de remarquer ici que ce warning se fait à la compilation au sens propre du terme. C’est vraiment le compilateur (et non l’éditeur de lien) qui vous tape sur les doigts.
Un dernier point avant de passer au deuxième message. On est en droit de se demander a quoi sert-t-il de déclarer nous même les fonctions si le compilateur sais le faire lui même. La raison est toute simple : il ne connait pas le prototype des fonctions déclarées implicitement. Il va donc essayer de le deviner ce qui peut poser problème quand il se trompe. Et de toute façon cela nous prive de beaucoup de vérification à la compilation, et croyez moi ce genre de garde-fou ce n’est pas du luxe !
L’étape de compilation étant finis, l’éditeur de lien va récupérer toute les fonctions du projet maintenant traduites en langage machine pour faire l’exécutable. Mais que se passe-t-il si une des fonctions venaient à manquer ? Et bien l’éditeur de lien va vous faire part du cuisant échec en décrivant la/les fonctions incriminée(s).
Dans notre cas, undefined reference to function 'HelloWorld' in main.o. Remarquons que l’erreur est dans main.o et non main.c puisque c’est l’éditeur de lien qui vous envoi l’erreur et que ce dernier travail avec les fichiers objets (.o).
Déclarons nos symboles !
Ici on suppose que le fichier hello.c est bien ajouté au projet. La question est, comment faire disparaitre notre ennuyeux warning ? Je suis sur que vous l’aviez deviné, il va falloir déclarer la fonction ’HelloWorld’. Pour cela rien de plus simple, il suffit de placer son prototype au début du fichier main.c .
Ceci est peut être perturbant au début, après tout la fonction 'Helloworld' n’est pas définit dans le fichier main.c. Mais essayer, vous verrez bien que ça fonctionne.
En fait pour être tout à fait propre, il faudrait rajouter le mot clef extern devant notre prototype. Ce mot clef permet de déclarer un symbole sans le définir, mais il est optionnel ici car on a vu que donner le prototype d’une fonction ne faisait que de la déclarer.
Et c’est tout, maintenant ça compile, et ça linke (de l’anglais linker, éditeur de lien) sans problème.
Toutefois, l’utilisation du mot-clef extern s’avère indispensable pour déclarer une même variable global dans plusieurs fichiers. Supposons que, malgré toutes les mises en gardes et menaces d’apocalypses programmationnels vous voulez utilisez une variable globale. Modifions notre exemple pour illustrer la situation.
Évidement, ça plante. Le compilateur n’apprécie pas beaucoup qu’on utilise global_int sans l’avoir déclaré et nous le signal à sa façon.
hello.c: In function ’HelloWorld’:
hello.c:5:32 error : ’global_int’ undeclared (first use in this function)
(Déclarer implicitement des variables il ne sait pas faire).
D’un autre coté si on essaye de placer int global_int; aussi en haut du fichier hello.c cela ne va pas non plus car on a vu que cela revenait à déclarer et définir la variable or on ne veux pas définir un autre variable,
c’est la même que dans main.c ! Si on essaye quand même, on se retrouve donc avec error symbol 'global_int' multiply defined1. Pour se sortir de cette situation délicate il faudrait pouvoir déclarer la variable sans la définir, et c’est justement ce que extern permet de faire. Elle est pas belle la vie ?
Rajoutons donc extern int global_int; au début du fichier hello.c, et tout va pour le mieux dans le meilleur des mondes possible.
Et les headers dans tout ça ?
Maintenant que l’on a mis à plat un peu tout ce fouillis, on peut enfin comprendre pleinement le rôle du header. Dans un vrai projet, les fonctions d’un même fichier .c peuvent être utilisées à de nombreux endroits. Alors plutôt que d’écrire sans cesse au début des fichiers .c les même prototypes, on s’est dit que ce serait pas mal de réunir tout plein de déclarations de symboles dans un même fichier et d’utiliser le préprocesseur pour venir recopier ces déclarations en haut de nos fichiers sources en une ligne. Bingo, c’est justement ce que fait la directive #include.
Cela signifie que les headers sont fait uniquement pour la déclaration des fonctions (et éventuellement des variables globales), jamais pour leur définition. Donc mettez bien le mot clef extern devant les variables globales de vos fichiers .h sinon vous aurez des problèmes dès que vous inclurez votre
header plus d’une fois.
Un petit mot sur les bibliothèques
Si vous avez bien compris ce qui a été dit plus haut, vous devriez comprendre ces histoires de linkage de bibliothèques. En effet, dans le header il n’y que des déclarations donc faire par exemple #include <SDL/SLD.h> ça permet de rassurer votre compilateur mais l’éditeur de lien il s’en fiche pas mal. Il lui
faut des définitions, contenu dans les fichiers binaires de la SDL, ce sont les fameux .lib ou .a suivant votre OS. D’où l’importance d’expliciter les bibliothèque à linker.
Vous aurez peut être remarqué que lorsque l’on fait #include <stdio.h>, on ne linke rien du tout et pourtant, on peut utiliser printf(), scanf() et compagnie. C’est en fait que la bibliothèque standard est tellement indispensable qu’elle est linké par défaut. Votre éditeur de lien peut aussi linker d’autres bibliothèques par défaut d’ailleurs. Sous windows par exemple l’API windows (dont le header est windows.h) l’est généralement. Mais cela dépend de votre éditeur de lien.
Conclusion
Voila, j’espère que ces explications vous aurons été utiles, pour toute correction, remarque, suggestion, n’hésitez pas à me MP. Pour les plus curieux d’entre vous, ceux vraiment intéresser par ce qui passe
derrière le rideau je recommande le tutoriel de Taurre sur les identificateurs en lecture complémentaire (attention ce n’est pas facile d’accès).
Aller plus loin en C
Vous avez fini le tuto officiel et vous en voulez plus ?
Le langage C est relativement petit c’est vrai, mais ne croyez surtout pas que vous en avez déjà fait le tour. Voici une petite sélection de tutoriels du SdZ particulièrement intéressant et qui affuterons vos compétences. L’apprentissage en profondeur de tout ce qui est présenté dans ces tutos prend plusieurs années, ne soyez pas trop pressé et allez doucement. J’ai classé les tutos par catégories pour que vous vous y retrouviez un peu.
Cette liste n’est en aucun cas exhaustive, ce sont juste des tutos que j’ai lu et trouvé bien fait à titre personnel. J’ai des raisons de penser qu’ils peuvent être utile à beaucoup de gens qui ont finis le tuto officiel, qui veulent continuer mais qui sont privé de boussole.
Notez qu’il existe d’autres tutos sur le C qui partent prétendument de zéros mais qui sont nettement plus rigoureux, complet… et dur à suivre. Je pense à celui-ci en particulier. J’imagine qu’il serait pénible après avoir finit le tuto officiel d’en commencer un autre depuis le début mais vous pouvez toujours jeter un coup d’œil pour voir.
Arrêtez de poster le moindre de vos problèmes sur le forum
Si après avoir cherché dans la FAQ et lu la doc vous ne trouvez pas, vous pouvez encore vous en sortir tout seul, voici comment.
En effet, pas facile d’avoir un code un peu cohérent. Heureusement notre ami Fvirtman est la et ouvrez grand vos mirettes, c’est passionnant ! C’est aussi plus général que la SDL, ce sont des excellents exemples sur comment architecturer un peu son programme.
La norme c’est ce qui contient la vérité ultime sur le langage C, c’est elle qui dicte ce qui est standard et ce qui ne l’est pas, et donc par voie de conséquence, ce qui est portable et ce qui ne l’est pas. C’est assez imbitable mais dans les cas limites et tordus du langage, c’est toujours bien de se rapporter à celle-ci.
le message exacte peut varier suivant votre compilateur↩
Afficher toute la biographie
La répétition est la base de l’enseignement. — ☮ ♡