CodeGym /Cours /C++ SELF /Problèmes typiques de compilation depuis la console

Problèmes typiques de compilation depuis la console

C++ SELF
Niveau 33, Leçon 4
Disponible

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
fatal error: X.hpp: No such file or directory
compile
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
error: ‘name’ was not declared in this scope
compile
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
undefined reference to ... / symbol(s) not found
link
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
multiple definition of ...
link
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.

1
Mission
C++ SELF, niveau 33, leçon 4
Bloqué
Salutation par include
Salutation par include
1
Mission
C++ SELF, niveau 33, leçon 4
Bloqué
Deux emplacements de compilation
Deux emplacements de compilation
1
Mission
C++ SELF, niveau 33, leçon 4
Bloqué
Implémentation perdue
Implémentation perdue
1
Étude/Quiz
Compilation, niveau 33, leçon 4
Indisponible
Compilation
Compilation depuis la console
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION