En théorie, le contenu de cette page ne devrait être qu’une redite de vos cours de C et de compilation de l’an dernier.
Cependant, je sais qu’à la réminiscence des moments passés à réviser ces matières, la plupart d’entre vous sentez la chaleur de vos larmes couler le long de vos joues.
Nous allons donc reprendre le sujet en douceur pour faire en sorte que vous compreniez un peu mieux ce qu’il se passe pendant la compilation et à quoi elle sert.
Cela vous aidera, j’espère, à comprendre un peu mieux les injures du compilateur.
La compilation désigne le procédé consistant à transformer du code-source en un code-objet (c’est-à-dire des instructions machines) comme un fichier-objet, un programme ou une librairie.
Dans ce cours, on utilisera le terme de “compilation” pour faire référence soit à la génération complète d’un exécutable (= compilation d’un programme), soit à la génération d’un fichier-objet (= compilation d’un fichier-source).
La compilation d’un programme est constituée de deux phases bien distinctes :
.cpp en fichier-objet, réalisée par le compilateur,g++ est à la fois un compilateur et un linker.
Lorsque vous exécutez g++ -o program.exe a.cpp b.cpp c.cpp, l’outil réalise donc 4 opérations :
a.cpp ➔ g++ -c a.cppb.cpp ➔ g++ -c b.cppc.cpp ➔ g++ -c c.cppprogram.exe ➔ g++ -o program.exe a.o b.o c.oflowchart TD;
L(Edition des liens)
C1(Compilation)
C2(Compilation)
C3(Compilation)
Acpp[a.cpp] --- C1 --> Ao[a.o]
Bcpp[b.cpp] --- C2 --> Bo[b.o]
Ccpp[c.cpp] --- C3 --> Co[c.o]
Ao --- L
Bo --- L
Co --- L
L --> E[program.exe]
Concentrons nous d’abord sur la phase de compilation.
Le compilateur attend en entrée un fichier .cpp et écrit le fichier-objet correspondant.
Ce fichier est un binaire contenant les instructions des fonctions et l’instanciation des variables globales définies dedans.
Lorsque vous lancez la compilation, il y a tout d’abord le préprocesseur qui lit et récrit le fichier.
Il remplace notamment chaque instruction #include par le contenu du fichier inclus et toutes les occurrences de macros préprocesseur (#define) par leur définition.
Ensuite, nous avons l’analyse syntaxique et l’analyse sémantique.
Plutôt qu’expliquer précisément ce que fait le compilateur pour chacune d’entre elles, nous allons décrire ce qu’il se passe de façon plus globale et, j’espère, plus intuitive.
Le compilateur lit le fichier instruction par instruction, en partant du haut du fichier.
Si l’instruction contient :
Au fur-et-à-mesure de l’analyse, le compilateur ajoute également dans le fichier-objet les instructions binaires correspondant aux fonctions et aux variables globales définies dans le fichier.
Supposons que l’on a le code suivant dans math.hpp :
|
|
Et ce code dans main.cpp :
|
|
Tout d’abord, le préprocesseur copie-colle le contenu de math.hpp à la place de la directive d’inclusion.
On obtient donc :
|
|
Puis le compilateur lit les instructions au fur-et-à-mesure :
ligne 1
📚 Ajout dans la table des symboles
Fraction: type partiellement définiligne 3
📚 Ajout dans la table des symboles
Fraction: type partiellement définiFraction.num: variable intligne 4
📚 Ajout dans la table des symboles
Fraction: type partiellement définiFraction.num: variable intFraction.den: variable intligne 4
📚 Modification de la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intligne 7
🧐 Utilisation de Fraction dans le cadre d’une déclaration de fonction
Fraction est au moins partiellement défini dans la table des symbolesFraction est bien utilisé en tant que type📚 Ajout dans la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionligne 9
🧐 Utilisation de Fraction dans le cadre d’une définition de variable globale
Fraction est entièrement défini dans la table des symbolesFraction est bien utilisé en tant que typeint📚 Ajout dans la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionhalf: variable Fraction⚙️ Ecriture du fichier-objet
half de valeur { 1, 2 }ligne 13
🧐 Utilisation de Fraction dans le cadre d’une définition de variable
Fraction est entièrement défini dans la table des symbolesFraction est bien utilisé en tant que typeint📚 Ajout dans la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionhalf: variable Fractionthird: variable Fractionligne 13
🧐 Utilisation de Fraction dans le cadre d’une définition de variable
Fraction est entièrement défini dans la table des symbolesFraction est bien utilisé en tant que typeint📚 Ajout dans la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionhalf: variable Fractionthird: variable Fractionligne 15
🧐 Utilisation de add dans le cadre d’un appel de fonction pour définir une variable
add est bien une fonction déclarée dans la table des symboles🧐 Utilisation de half et third en tant qu’arguments de type int
half et third sont bien des variables déclarées dans la table des symbolesint donc peuvent bien être passées à la fonction📚 Ajout dans la table des symboles
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionhalf: variable Fractionthird: variable Fractionres: variable Fractionligne 16
🧐 Utilisation de res.num dans le cadre d’un retour de fonction
res est bien une variable déclarée dans la table des symbolesres est de type Fraction, qui est bien entièrement défini dans la table des symbolesFraction contient bien un attribut numres.num sont bien compatiblesligne 17
📚 Suppression des symboles définis dans le bloc
Fraction: type défini avec deux attributs intFraction.num: variable intFraction.den: variable intadd: fonction (Fraction, Fraction) -> Fractionhalf: variable Fractionthird: variable Fractionres: variable Fraction⚙️ Ecriture du fichier-objet
half de valeur { 1, 2 }main()Dans le cas ci-dessus, la compilation s’est bien passée.
Le fichier-objet en sortie contient :
half et les instructions binaires permettant de l’initialiser à { 1, 2 },main qui n’attend aucun paramètres et les instructions binaires qui la constituent.Il nous apparaît utile de faire un petit tour des situations d’erreurs les plus courantes, afin que vous puissiez identifier les problèmes plus rapidement si vous les rencontrez.
Et vous les rencontrerez forcément, eheheh… 😈
Commencez par vous placer dans le répertoire chap-01/3-build-errors, car c’est là que vous devrez compiler les différents fichiers.
1-structs.cpp (et seulement de le compiler, pas de générer un exécutable).
Celui-ci ne devrait pas compiler.On doit ajouter l’option -c pour s’arrêter après la phase de compilation.
g++ -std=c++17 -c 1-structs.cpp
expected ';' after struct definition.Contrairement au Java, il faut penser à écrire ; après la définition de tout type (class, struct, enum, etc).
struct A
{
int a = 0;
};
struct B
{
int b = 0;
};
int add(A a, B b)
{
return a.a + b.b;
}
2-class.cpp.1-structs.cpp ?L’erreur signifie qu’on essaye d’accéder à un champ privé d’une classe depuis l’extérieur.
Lorsqu’on ne spécifie pas de modificateur de visibilité, les champs sont privés dans une class et publics dans une struct.
C’est pour ça que nous n’avons pas eu l’erreur dans le fichier précédent.
class A
{
public:
int a = 0;
};
int get_a(A a)
{
return a.a;
}
3-hello.cpp.Le compilateur n’a jamais rencontré les déclarations des symboles std::cout, std::cin, std::endl et std::string.
Lorsqu’il analyse les instructions qui les utilisent, il ne les trouve donc pas dans la table des symboles et émet donc des erreurs du style '...' is not a member of 'std'.
On a également l’erreur 'name' was not declared in this scope.
Celle-ci est plus étonnante, puisque name est bien définie une ligne plus haut.
Cependant, comme l’instruction définissant la variable name n’a pas compilé, elle n’a pas pu être ajoutée à la table des symboles.
Le compilateur ne la trouve donc pas au moment où il analyse l’instruction std::cin >> name;, ce qui explique cette erreur.
#include <iostream> // pour std::cin, std::cout et std::endl
#include <string> // pour std::string
int main()
{
std::string name;
std::cin >> name;
std::cout << "Hello " << name << std::endl;
return 0;
}
Une erreur de compilation est parfois la conséquence d’une autre erreur, ce qui peut rendre la sortie du compilateur très difficile à lire.
Il est possible de spécifier l’option -Wfatal-errors à l’invocation du compilateur pour s’arrêter dès la première erreur et simplifier la compréhension du problème.
4-main.cpp.Après le passage du préprocesseur, le fichier devrait ressembler à :
// =========================================//
// Inclusion de 4-car.hpp depuis 4-main.cpp //
// =========================================//
//
/** le contenu de <string> **/ //
//
struct Car //
{ //
std::string brand; //
}; //
//
// =========================================//
// ================================================//
// Inclusion de 4-driver.hpp depuis 4-main.cpp //
// ================================================//
//
// =========================================// //
// Inclusion de 4-car.hpp depuis 4-main.cpp // //
// =========================================// //
// //
/** le contenu de <string> **/ // //
// //
struct Car // //
{ // //
std::string brand; // //
}; // //
// //
// =========================================// //
//
/** le contenu de <iostream> **/ //
//
struct Driver //
{ //
void drive(Car car) //
{ //
std::cout << "I'm driving a " //
<< car.brand //
<< std::endl; //
} //
}; //
//
// ================================================//
int main()
{
Car car { "golf" };
Driver driver;
driver.drive(car);
return 0;
}
Lorsque le compilateur parcourt le fichier, il rencontre deux fois la définition du type Car.
Il est donc logique qu’il émette l’erreur redefinition of 'Car'.
#pragma once en haut du fichier.4-main.cpp compile désormais.On modifie d’abord 4-car.hpp, puisque c’est ce fichier qui est inclus en double.
#pragma once
#include <string>
struct Car
{
std::string brand;
};
Une fois ce changement fait, on constate que le code compile.
Pour éviter que le problème ne se reproduise si on décidait d’inclure 4-driver.hpp dans un autre header, on modifie également ce dernier.
#pragma once
#include "4-car.hpp"
#include <iostream>
struct Driver
{
void drive(Car car)
{
std::cout << "I'm driving a " << car.brand << std::endl;
}
};
Prenez l’habitude de toujours placer la directive #pragma once au sommet de vos headers.
Cela vous évitera quelques migraines.
La plupart du temps, l’erreur ci-dessus apparaît lorsque vous oubliez d’inclure un header.
Cependant, elle peut également se produire dans la situation présentée ci-dessous, et est dans ce cas beaucoup plus difficile à identifier et corriger.
5-main.cpp avec l’option permettant de s’arrêter dès la première erreur.g++ -std=c++17 -Wfatal-errors -c 5-main.cpp
5-tac.hpp:8:15: error: 'Tic' has not been declared.5-tic.hpp est bien inclus dans 5-tac.hpp.Tic ?Dans un premier temps, il faut savoir ce que le compilateur analyse une fois la précompilation terminée :
// =====================================================//
// Inclusion de 5-tic.hpp depuis 4-main.cpp //
// =====================================================//
//
#pragma once // -> 1e inclusion de 5-tic.hpp //
//
// ==============================================// //
// Inclusion de 5-tac.hpp depuis 5-tic.cpp ======// //
// ==============================================// //
// //
#pragma once // -> 1e inclusion de 5-tac.hpp // //
// //
// =====================// // //
// #include "5-tic.hpp" // -> déjà inclus ! // //
// ---------------------// // //
// //
struct Tac // //
{ // //
// Invert value with tic. // //
void swap(Tic& tic); // //
// //
int value = 0; // //
}; // //
// //
// ==============================================// //
//
struct Tic //
{ //
// Invert value with tac. //
void swap(Tac& tac); //
//
int value = 0; //
}; //
//
// =====================================================//
int main()
{
Tic tic { 1 };
Tac tac { 5 };
tic.swap(tac);
tac.swap(tic);
return 0;
}
Effectivement, lorsque le compilateur arrive à l’instruction void swap(Tic& tic);, la définition de Tic n’a encore jamais été rencontrée.
Le symbole n’existant pas encore dans la table des symboles, on obtient l’erreur 'Tic' has not been declared.
Il s’agit d’une inclusion cyclique.
Effectivement 5-tic.hpp inclut 5-tac.hpp qui inclut à son tour 5-tic.hpp et ainsi de suite.
Pour indiquer au compilateur qu’un type existe, on a deux possibilités : le définir ou le pré-déclarer (on employera plus communément le terme anglais forward-declare).
Il suffit pour cela d’écrire :
class A; // forward-declare d'une class A
struct B; // forward-declare d'une struct B
enum C; // forward-declare d'un enum C
Les directives d’inclusions problématiques sont celles situées dans les fichiers 5-tic.hpp et 5-tac.hpp.
On remplace donc le code de 5-tic.hpp par :
#pragma once
struct Tac;
struct Tic
{
// Invert value with tac.
void swap(Tac& tac);
int value = 0;
};
Et celui de 5-tac.hpp par :
#pragma once
struct Tic;
struct Tac
{
// Invert value with tic.
void swap(Tic& tic);
int value = 0;
};
Certaines directives d’inclusion peuvent être remplacées par des forward-declarations, mais pas toutes !
En effet, ici, cela fonctionne parce que dans le fichier 5-tac.hpp, on ne fait que déclarer une référence de type Tic.
Si on avait voulu accéder à l’un des champs de la classe, ou bien à sa taille pour réserver de l’espace mémoire, la définition complète de Tic, et donc l’inclusion du header, aurait été nécessaire.
Je vous conseille d’abord de faire une pause, si vous venez de finir de lire tout ce qu’il y avait au-dessus.
Idéalement, attendez même d’avoir passé une bonne nuit de sommeil avant de reprendre la lecture 😴
Une fois le fonctionnement du compilateur bien assimilé, il est temps de passer à celui du linker.
Dans le cadre de la génération d’un programme, l’objectif du linker est de regrouper, depuis les fichiers-objet fournis, les instructions strictement nécessaires à son exécution.
Supposons que l’on essaye de linker deux fichiers objets main.o et math.o avec la commande : g++ -o program.exe main.o math.o.
On suppose que main.o est le résultat de la compilation de :
|
|
que math.o est le résultat de :
|
|
et que math.hpp contient le code suivant :
|
|
Dans ce cas, main.o contient les instructions de la fonction main() et les instructions d’initialisation de la variable globale half, tandis que math.o contient les instructions des fonctions create_half(), mult(Fraction, Fraction) et invert(Fraction).
graph LR
subgraph A[main.o]
main(["main()"])
half(["half: Fraction"])
end
graph LR
subgraph B[math.o]
create_half(["create_half()"])
mult(["mult(Fraction, Fraction)"])
invert(["invert(Fraction)"])
end
Le linker identifie les éléments dont les instructions seront exécutées quoi qu’il arrive :
main,graph LR
subgraph A[main.o]
main(["main()"])
half(["half: Fraction"])
end
classDef fat stroke-width:3px
class main,half fat
graph LR
subgraph B[math.o]
create_half(["create_half()"])
mult(["mult(Fraction, Fraction)"])
invert(["invert(Fraction)"])
end
Il analyse ensuite les instructions associées à chaque élément de manière à créer les liens vers les bonnes fonctions.
graph LR
subgraph A[main.o]
main(["main()"])
half(["half: Fraction"])
main -.->|l.9| half
end
subgraph B[math.o]
create_half(["create_half()"])
mult(["mult(Fraction, Fraction)"])
invert(["invert(Fraction)"])
end
half -.->|l.3| create_half
main -.->|l.9| mult
classDef fat stroke-width:3px
class main,half fat
Cela lui permet d’identifier l’ensemble des éléments à placer dans l’exécutable final.
graph LR
subgraph D[program.exe]
f_main(["main()"])
f_half(["half: Fraction"])
f_create_half(["create_half()"])
f_mult(["mult(Fraction, Fraction)"])
end
subgraph A[main.o]
main(["main()"])
half(["half: Fraction"])
end
subgraph B[math.o]
create_half(["create_half()"])
mult(["mult(Fraction, Fraction)"])
invert(["invert(Fraction)"])
end
main -.->|l.9| half
half -.->|l.3| create_half
main -.->|l.9| mult
main ==> f_main
half ==> f_half
create_half ==> f_create_half
mult ==> f_mult
classDef fat stroke-width:3px
class main,half,create_half,mult fat
L’exécutable final ne contient donc que les symboles effectivement utilisés par le programme.
La fonction invert(Fraction) présente dans math.o et qui n’est jamais appelée n’en fait donc pas partie.
De la même façon que nous l’avons fait avec les erreurs de compilation, nous allons vous présenter quelques situations d’erreurs émises au cours de l’édition des liens.
Commencez par vous placer dans le répertoire chap-01/4-link-errors.
1-hello_wordl.cpp pour en faire un fichier-objet, puis essayez de créer un programme à partir de ce fichier-objet.Pour récupérer une erreur humainement lisible, il faut faire un peu de ménage dans la sortie.
g++ -std=c++17 -c 1-hello_wordl.cpp
# => Ok
g++ -o program 1-hello_wordl.o
# => [plein de trucs horribles à lire] undefined reference to `main' (ou WinMain sous Windows)
On note également le message ld returned 1 exit status à la toute fin.
Le programme chargé de l’édition des liens est donc ld.
Il y a une typo dans le nom de la fonction main, ce qui empêche le linker de la trouver.
#include <iostream>
int main()
{
std::cout << "Hello world!" << std::endl;
return 0;
}
Lorsque la sortie d’erreurs se termine par quelque chose comme ld returned 1 exit status, cela indique que l’erreur se produit durant l’édition des liens.
g++ -std=c++17 -o program 2-main.cpp ?La commande réalise d’abord la compilation de 2-main.cpp en fichier-objet, puis invoque le linker pour générer un exécutable à partir de ce fichier.
2-main.cpp en fichier-objet s’est-elle bien passée ?On obtient quelque chose comme :
2-main.cpp: undefined reference to `add(int, int)'
error: ld returned 1 exit status
La compilation du fichier 2-main.cpp s’est donc bien passée, puisque c’est la phase d’édition des liens (ld) qui échoue.
Après la précompilation, le fichier 2-main.cpp est transformé en :
|
|
A la suite de la compilation de ce fichier, 2-main.o contient uniquement les instructions de la fonction main.
En effet, la ligne 3 est une déclaration de fonction.
Le compilateur ne connaissant pas la définition de add(int, int), il ne peut pas générer les instructions binaires qui lui seraient associées.
La définition de la fonction add(int, int) est présente dans le fichier 2-add.cpp.
Il suffit donc d’ajouter ce fichier à la ligne de commande :
g++ -std=c++17 -o program 2-main.cpp 2-add.cpp
g++ -std=c++17 -o program 3-main.cpp 3-sub.cpp.3-sub.o ?L’erreur est la suivante, et il s’agit à nouveau d’une erreur de link.
3-main.cpp: undefined reference to `sub(int, int)'
Après précompilation, le fichier 3-sub.cpp contient :
#pragma once
int sub(int a, int b);
float sub(float a, float b)
{
return a - b;
}
Le fichier 3-sub.o contient donc les instructions binaires de la fonction sub(float, float), et non pas celles de sub(int, int).
3-main.cpp et 3-sub.cpp ne produit pas d’erreur ?Le fichier 3-main.o précompilé devrait avoir ce contenu :
#pragma once
int sub(int a, int b);
int main()
{
return sub(1, 2);
}
Il n’y a donc aucune raison qu’il ne compile pas, puisque la fonction sub(int, int) est correctement déclarée avant son appel, et que les arguments fournis ont un type compatible avec la signature de la fonction.
En ce qui concerne le fichier 3-sub.cpp, nous avons :
#pragma once
int sub(int a, int b);
float sub(float a, float b)
{
return a - b;
}
Ici, on pourrait se demander pourquoi le compilateur accepte d’avoir une déclaration de sub qui attend des int, et une définition de sub qui attend des float.
C’est tout simplement parce qu’en C++, la surcharge de fonctions est autorisée (comme en Java).
On pourrait par exemple avoir dans un autre fichier-source, ou bien dans le même que celui-ci, une définition valide de sub(int, int), en plus de la définition déjà existante de sub(float, float).
En fonction du type des arguments passés à l’appel, le compilateur choisirait la fonction avec la signature la plus proche.
Il y a plusieurs solutions possibles.
sub dans 3-sub.cpp pour qu’elle accepte des int plutôt que des float.sub(int, int) dans 3-sub.o.sub dans 3-sub.hpp pour indiquer qu’elle attend des float.3-main.cpp, le symbole associé à sub dans la table des symboles aura pour signature sub(float, float) au lieu de sub(int, int),sub(float, float) dans 3-sub.hpp et la définition de sub(int, int) dans 3-sub.cpp, pour que les deux versions existent pendant la phase de link.L’essentiel, c’est de faire en sorte que les déclarations présentes dans le header correspondent bien aux définitions présentes dans le fichier-source.
Si vous rencontrez une erreur de type undefined reference to ..., commencez par vérifier que vous n’avez pas oublié de compiler un fichier-source.
Si le problème ne vient pas de là, assurez-vous que la signature de votre fonction (nom + type des paramètres) est bien strictement la même dans sa définition et dans ses déclarations.
g++ -std=c++17 -o prog 4-main.cpp 4-sub.cpp 4-add.cpp.En supprimant tous les caractères bizarres de la sortie, on obtient ces erreurs :
4-add.cpp: multiple definition of `debug(char const*, int, int)'
4-sub.cpp: first defined here
4-add.o et 4-sub.o ?Après la précompilation de 4-add.cpp, on a :
#pragma once
int add(int a, int b);
//-----------------------------
#pragma once
// le contenu de <iostream>
void debug(const char* fcn, int p1, int p2)
{
std::cout << fcn << " called with " << p1 << " and " << p2 << std::endl;
}
//-----------------------------
int add(int a, int b)
{
debug("add", a, b);
return a + b;
}
Les symboles présents dans 4-add.o sont donc :
debug(const char*, int, int)add(int, int)Similairement, après la précompilation de 4-sub.cpp, on a :
#pragma once
int sub(int a, int b);
//-----------------------------
#pragma once
// le contenu de <iostream>
void debug(const char* fcn, int p1, int p2)
{
std::cout << fcn << " called with " << p1 << " and " << p2 << std::endl;
}
//-----------------------------
int sub(int a, int b)
{
debug("sub", a, b);
return a - b;
}
Les symboles présents dans 4-sub.o sont donc :
debug(const char*, int, int)sub(int, int)Le symbole debug(const char*, int, int) est bien présent deux fois, d’où l’erreur du linker.
On pourrait créer un fichier 4-debug.cpp dans lequel on déplacerait la définition de debug(const char*, int, int).
Dans le fichier 4-debug.hpp, on aurait uniquement la déclaration de la fonction.
Ainsi le symbole ne serait plus présent dans 4-add.o ni 4-sub.o, seulement dans 4-debug.o.
Pour contraindre le linker à accepter qu’un symbole soit présent dans plusieurs fichiers-objet, vous pouvez placer le mot-clef inline devant sa définition dans le code-source.
Dans ce cas, au moment d’écrire le symbole dans l’exécutable final, le linker utilisera la version trouvée dans n’importe lequel des fichiers-objet.
Cela vous permet donc de définir vos fonctions directement dans les headers. S’ils sont inclus depuis plusieurs fichiers-source, le linker fera comme si les fonctions n’étaient présentes que dans l’un d’entre eux et n’émettera pas d’erreurs.
inline.On remplace le contenu de 4-debug.hpp par :
#pragma once
#include <iostream>
inline void debug(const char* fcn, int p1, int p2)
{
std::cout << fcn << " called with " << p1 << " and " << p2 << std::endl;
}
Dans le cadre de petits programmes, il n’y a aucune contre-indication à utiliser inline pour coder un maximum de choses dans les headers.
En revanche, pour de plus gros programmes, sachez que plus vous mettrez de choses dans les headers, plus la compilation prendra du temps.
Il faut retenir que la compilation d’un programme se fait en deux étapes : la compilation des fichiers-source suivie de l’édition des liens.
Durant la compilation, le compilateur :
Durant l’édition des liens, le linker :
main, et en ajoutant récursivement les instructions des fonctions appelées dedans.Quelques bonnes pratiques lorsqu’on code le contenu d’un header :
#pragma once.inline devant les définitions de fonctions (pas les déclarations).