1. Diagnostic : compilation ou édition de liens
Quand la construction échoue, l'esprit du débutant passe souvent en mode « il/elle me déteste », et le compilateur — en mode expected…, undefined…, fatal…, et tout cela ressemble à un sortilège. Nous commençons donc non pas par corriger mais par poser le diagnostic : à quelle étape ça a planté. Cela économise beaucoup de temps, car les erreurs de compilation se règlent en modifiant le code et les chemins d'include, alors que les erreurs de liaison se règlent par la liste des fichiers .cpp/.o et la correspondance des définitions.
Modèle simple « où ça fait mal »
Imaginez la compilation comme une chaîne de montage :
flowchart LR A[.cpp + #include] -->|compile| B[.o] B -->|link| C[executable]
Si vous voyez des mots comme fatal error: ... No such file or directory, expected, undeclared, no member named — c'est presque toujours de la compilation (étape compile). Si vous voyez undefined reference, symbol(s) not found, ld: ... — c'est presque toujours de la liaison (étape link).
Et un point psychologique important : « symbole introuvable » signifie presque jamais « header introuvable ». Ce sont des classes de problèmes différentes. Soigner la liaison avec le drapeau -I — c'est un peu comme soigner un rhume en repeignant les murs : ça peut améliorer l'humeur, mais la fièvre restera.
2. Erreur «header not found» : chemins d'include et -I
L'erreur « header introuvable » donne l'impression que le compilateur est paresseux et ne veut pas chercher le fichier. En pratique il veut bien le faire, mais il cherche strictement à des endroits déterminés. Et si votre en-tête est dans include/ et que vous compilez depuis un autre dossier, le compilateur n'est vraiment pas tenu de « deviner » où vous avez caché my_header.hpp. Ici il est important de comprendre : #include signifie « insère le texte du fichier », pas « ajoute un module du projet ».
Symptôme typique
Par exemple, dans src/main.cpp vous avez écrit :
#include "tracker/task.hpp"
int main() {
return 0;
}
Et lors de la compilation vous obtenez quelque chose comme :
fatal error: tracker/task.hpp: No such file or directory
Cela signifie : le compilateur a essayé de trouver tracker/task.hpp dans les emplacements standards (includes système), et aussi dans les répertoires que vous lui avez autorisés (répertoires -I...), et il n'a pas trouvé.
Le drapeau -I : comment donner au compilateur une carte
Le drapeau -I<dir> permet de dire au compilateur : « Quand tu vois #include "..." ou parfois <...>, essaie aussi de chercher dans ce répertoire ». Important : vous ajoutez non pas un fichier précis, mais la racine de recherche, par rapport à laquelle est écrit le chemin dans le #include.
C'est ici que les débutants se trompent le plus souvent : « où indiquer -I » et « que mettre dans le #include ». Et c'est précisément là que naît l'impression que la compilation est de la divination.
Structure de mini‑projet pratique pour s'entraîner
Imaginons que nous travaillons sur une application console pédagogique « TaskTracker » (un tracker de tâches fictif). Structure des dossiers :
project/
include/
tracker/
task.hpp
src/
task.cpp
main.cpp
L'idée est la suivante : tout ce que l'utilisateur « inclut » comme interface de la bibliothèque se trouve dans include/. Tout ce qui est implémentation se trouve dans src/.
Couplage correct : #include ↔ -I
Si dans le code vous écrivez :
#include "tracker/task.hpp"
alors -I doit pointer sur le dossier include/, pour que le chemin tracker/task.hpp « corresponde » :
g++ -std=c++23 -Iinclude src/main.cpp src/task.cpp -o tasker
Si par erreur vous écrivez -Iinclude/tracker, alors "tracker/task.hpp" ne sera plus trouvé (parce que le compilateur cherchera include/tracker/tracker/task.hpp), et vous obtiendrez le même « No such file ».
Guillemets "" et chevrons <>
Dans les projets pédagogiques on suit généralement la règle simple : nos headers via guillemets, les headers standard via chevrons.
#include <iostream> // bibliothèque standard
#include "tracker/task.hpp" // notre projet
On peut approfondir les priorités exactes de recherche, mais à ce stade l'essentiel est ailleurs : vos headers doivent se trouver soit « à côté » (relativement au fichier courant), soit via des -I... explicitement indiqués. Et il vaut mieux ne pas compter sur le « à côté », mais rendre le projet compilable depuis la racine via -Iinclude — ainsi il y aura moins de surprises.
Petit piège : la commande est lancée depuis le mauvais dossier
Si vous êtes dans le terminal à l'intérieur de src/ et que vous lancez :
g++ -std=c++23 -Iinclude main.cpp task.cpp -o tasker
alors pour le compilateur -Iinclude désignera src/include, et non project/include. Et soudainement «header not found», bien que «ça marchait hier».
Donc, quand vous écrivez des commandes avec des chemins relatifs, gardez en tête : la relativité est évaluée depuis le répertoire courant du terminal.
3. Erreur undefined reference : ce qui manque à l'éditeur de liens
Après «header not found», la douleur suivante la plus commune est «symbole introuvable». En clair : «Le compilateur a vu la déclaration de la fonction (ou méthode), a compilé le code qui l'appelle, mais à l'étape de la liaison il n'a pas trouvé la définition (implémentation) nécessaire pour produire l'exécutable».
Ici il est très utile de garder à l'esprit la phrase : #include aide la compilation à voir les déclarations, mais n'ajoute pas l'implémentation à la phase de liaison. Les implémentations sont ajoutées parce que vous avez passé à la commande de construction les bons fichiers .cpp (ou .o).
Cas de base : il y a la déclaration, l'implémentation ne participe pas à la liaison
include/tracker/task.hpp :
#pragma once
#include <string>
namespace tracker {
std::string make_title(int id);
}
src/task.cpp :
#include "tracker/task.hpp"
namespace tracker {
std::string make_title(int id) {
return "Task #" + std::to_string(id);
}
}
src/main.cpp :
#include <iostream>
#include "tracker/task.hpp"
int main() {
std::cout << tracker::make_title(7) << '\n'; // Task #7
}
Si vous compilez seulement main.cpp :
g++ -std=c++23 -Iinclude src/main.cpp -o tasker
la compilation passera (parce que la déclaration est visible via le header), mais la liaison échouera avec quelque chose comme :
undefined reference to `tracker::make_title(int)'
La commande correcte doit inclure src/task.cpp :
g++ -std=c++23 -Iinclude src/main.cpp src/task.cpp -o tasker
Et voilà — c'est la cause la plus fréquente de undefined reference à notre niveau : «on a oublié d'ajouter le fichier avec l'implémentation».
«Le fichier est ajouté mais l'erreur persiste» : trois causes fréquentes
Parfois vous avez ajouté tous les .cpp, mais «symbole introuvable» persiste. Ce n'est plus «j'ai juste oublié un fichier», mais une histoire un peu plus subtile. La bonne nouvelle : subtil n'est pas synonyme de compliqué, il suffit d'apprendre à vérifier les hypothèses dans l'ordre.
Discordance de signature : on a déclaré une chose et défini autre chose
Classique : dans le header on a écrit une chose, et dans le .cpp on a par erreur fait légèrement différemment.
Dans le .hpp :
#pragma once
namespace tracker {
int parse_id(const char* s);
}
Et dans le .cpp :
namespace tracker {
int parse_id(char* s) { // différent : pas const
return 0;
}
}
Le compilateur compilera cela (deux fonctions différentes peuvent exister), mais le linker cherchera parse_id(const char*) et ne la trouvera pas. Dans le message d'erreur on voit souvent le «nom» exact avec les types. Au début il n'est pas nécessaire de maîtriser le name mangling — il suffit de comparer littéralement les types dans la déclaration et la définition caractère par caractère.
Autre namespace (ou namespace oublié)
Un autre cas fréquent : on a déclaré la fonction dans le namespace tracker, mais l'implémentation a été faite dans l'espace global.
Header :
#pragma once
namespace tracker {
int next_id();
}
Implémentation (erronée) :
int next_id() { // pas de namespace tracker
return 1;
}
En résultat tracker::next_id() n'est pas défini. Le linker le signale honnêtement, et cela ressemble encore à de la «magie» tant qu'on n'est pas habitué : l'espace de noms fait partie du nom du symbole.
Vous linkez le mauvais ensemble d'objets
Si vous travaillez avec des objets compilés, il est facile de linker «le mauvais ensemble». Par exemple, vous avez fait :
g++ -std=c++23 -Iinclude -c src/main.cpp -o main.o
g++ -std=c++23 -Iinclude -c src/task.cpp -o task.o
Puis vous avez accidentellement linké seulement main.o :
g++ main.o -o tasker
Et de nouveau undefined reference. C'est la même classe de problème «pas d'implémentation dans la liaison», simplement sous forme de .o plutôt que de .cpp.
4. Checklist et mini‑enquête
La checklist a l'avantage d'éteindre la panique. Vous n'essayez pas de «tout vous rappeler», vous vérifiez les points dans l'ordre. J'aime comparer cela à la recherche de clés perdues : inutile de plonger immédiatement dans la physique quantique, commencez par vérifier la poche de la veste.
Ci‑dessous la checklist qui marche vraiment pour nos problèmes typiques : chemins d'include, header not found, undefined reference. Essayez de suivre l'ordre et de ne changer qu'une seule chose à la fois : ainsi vous saurez précisément ce qui a résolu le problème.
Tableau «symptôme → étape → que vérifier → correction rapide»
| Symptôme dans la sortie | Où ça a cassé | Ce que cela signifie en général | Que vérifier en premier | Correction typique |
|---|---|---|---|---|
|
|
header introuvable | le fichier existe‑t‑il ; le chemin dans #include est‑il correct ; y a‑t‑il un -I... | ajouter/corriger -I, rectifier le #include |
|
|
le compilateur n'a pas vu la déclaration | le bon header est‑il inclus ; avez‑vous oublié std::/namespace | ajouter #include, corriger le namespace |
|
|
la déclaration existe, la définition est absente de la liaison | les fichiers .cpp/.o nécessaires sont‑ils ajoutés ; la signature correspond‑elle | ajouter le fichier d'implémentation ; corriger la signature |
|
|
la définition a été dupliquée | n'avez‑vous pas défini une fonction/variable dans un header | c'est une histoire liée à l'ODR : garder les définitions dans les .cpp |
La dernière ligne sur multiple definition est donnée ici seulement pour reconnaître la classe du problème. Nous avons déjà étudié cela plus en détail ailleurs, et il est important de ne pas confondre : ce n'est pas «symbole introuvable», mais «symbole trouvé trop de fois».
Mini‑algorithme en 4 questions
Quand la compilation échoue, essayez de formuler les choses à voix haute.
D'abord je regarde si le log contient fatal error ou undefined reference. Si c'est fatal error, je ne touche pas à la liste des .cpp et je ne pense pas à la liaison — je vérifie le #include et le -I. Si c'est undefined reference, je n'essaie pas de «réajouter -I» — je vérifie quels fichiers participent à la liaison et où se trouve l'implémentation. Ensuite je compare la signature de la déclaration et de la définition. Et seulement après, si tout concorde, je commence à suspecter des choses plus rares comme «mauvais namespace» ou «je linke les mauvais .o».
Petite enquête sur un exemple
Il est très utile de voir comment la checklist fonctionne en vrai, car alors on cesse d'avoir peur des erreurs : elles deviennent simplement des branches dans un arbre de décision.
Étape 1. On casse volontairement l'include.
Supposons que src/main.cpp contient :
#include <iostream>
#include "tracker/task.hpp"
int main() {
std::cout << "Hello\n"; // Hello
}
Et vous lancez la commande depuis la racine du projet, mais vous avez oublié -Iinclude :
g++ -std=c++23 src/main.cpp src/task.cpp -o tasker
Si tracker/task.hpp se trouve bien dans include/tracker/task.hpp, le compilateur dira logiquement «header introuvable». La checklist indique : c'est une erreur de compilation, donc on corrige les include paths. On ajoute -Iinclude :
g++ -std=c++23 -Iinclude src/main.cpp src/task.cpp -o tasker
Étape 2. On casse volontairement la liaison.
Maintenant faisons en sorte que main.cpp appelle une fonction de task.cpp :
#include <iostream>
#include "tracker/task.hpp"
int main() {
std::cout << tracker::make_title(3) << '\n'; // Task #3
}
Et compilons seulement main.cpp :
g++ -std=c++23 -Iinclude src/main.cpp -o tasker
On obtient undefined reference. La checklist dit : c'est une erreur de link, donc -I n'est plus en cause — on ajoute le fichier d'implémentation :
g++ -std=c++23 -Iinclude src/main.cpp src/task.cpp -o tasker
Étape 3. On casse volontairement la signature.
Si maintenant dans le header vous déclarez make_title(long long) et que vous définissez make_title(int), vous obtiendrez à nouveau undefined reference, même si task.cpp est présent dans la commande. C'est précisément le cas où «ajouter le fichier» ne suffit pas, parce que le linker cherche un symbole différent. C'est pourquoi la troisième vérification après «tous les fichiers inclus» est de confronter les signatures.
5. Erreurs typiques lors de la compilation depuis la console
Erreur n°1 : soigner «header introuvable» en ajoutant un .cpp à la commande.
Quand le compilateur écrit fatal error: ... No such file or directory, il lui manque littéralement le texte du header à l'étape de compilation. Ajouter un autre .cpp n'élargit pas les chemins de recherche des headers et n'"amène" pas les fichiers via les includes. La solution est de corriger le chemin dans le #include et d'ajouter le -I approprié (généralement -Iinclude), et de lancer la commande depuis le répertoire attendu.
Erreur n°2 : soigner undefined reference avec le drapeau -I.
-I n'affecte que la recherche des headers, donc la compilation. Le linker ne lit pas vos headers et ne comprend pas qu'il «y a une implémentation quelque part». Si vous voyez undefined reference, vous avez presque toujours soit oublié d'ajouter le fichier contenant la définition à la liaison, soit la définition ne correspond pas par nom/signature/namespace à la déclaration.
Erreur n°3 : croire que #include «apporte l'implémentation».
C'est une erreur très naturelle pour un débutant : «J'ai inclus task.hpp, pourquoi la fonction est‑elle introuvable ?». Parce que le header contient généralement des déclarations, l'implémentation est dans le .cpp. Le header aide le compilateur à savoir que la fonction existe, mais le linker a besoin de voir l'objet compilé contenant le code de cette fonction. Ainsi, la commande de build (ou la phase de liaison des .o) doit inclure toutes les unités de traduction avec des définitions.
Erreur n°4 : construire le projet «depuis des endroits différents» avec des chemins relatifs différents.
Aujourd'hui vous lancez la commande depuis la racine du projet — tout va bien. Demain vous lancez depuis src/ — et -Iinclude pointe soudainement ailleurs. Ces problèmes ressemblent à «ça s'est cassé tout seul». En fait c'est le point de référence des chemins relatifs. Le remède est simple : soit construisez toujours depuis la racine du projet, soit utilisez des chemins absolus/soignés (et plus tard — un système de build, mais ce n'est pas le sujet de cette leçon).
Erreur n°5 : oublier que le namespace fait partie du nom du symbole.
Si dans le header la fonction est déclarée comme tracker::next_id(), et que dans le .cpp vous définissez simplement next_id(), alors pour le linker ce sont deux fonctions différentes, et celle qui est nécessaire restera «introuvable». Cela arrive particulièrement quand on copie du code, qu'on vérifie rapidement la compilation, et qu'on ne remarque pas qu'un bloc namespace tracker { ... } est présent à un endroit et absent à un autre.
GO TO FULL VERSION