Définition & déclaration
Le langage C++ s’est construit au fil du temps et pas toujours dans un soucis d’harmonisation. Ainsi ce chapitre qui aborde les déclarations et les définitions en C++ amène tout un lot de cas particuliers et de dérogations liées à certains mots-clefs. Nous avons volontairement choisi comme pédagogie de mettre l’accent sur quelques grands principes. Pour cela, nous nous plaçons dans un cadre idéal d’un programme comportant un seul fichier source sans aucune inclusion.
Principe général
Dans cette page, nous choisissons de désigner par le terme entité : une variable, une fonction, une structure…
Définition et déclaration en C++
Avertissement
Dans l’univers du C++, déclaration et définition sont deux notions proches mais cependant différentes. Faîtes très attention, car dans d’autres langages, ces deux termes sont souvent utilisés de manière équivalente.
D’après la terminologie du langage C++, une déclaration sert à :
Introduire ou réintroduire une entité dans un programme en rappelant son type.
Une définition correspond à :
Une déclaration
Suivie de suffisamment d’informations pour que le compilateur puisse créer cette entité.
Note
Vous remarquez qu’une définition est aussi une déclaration ! On peut aussi voir une déclaration comme une définition tronquée.
Voici une règle commune à de nombreux langages :
Règle - Declaration-before-use : vous devez déclarer une entité avant de l’utiliser
Dans le langage C++, le principe intitulé One Definition Rule s’applique :
Règle ODR - One Definition Rule : dans un fichier source, une seule définition est autorisée par entité
Note
Durant la compilation d’un fichier source, si le compilateur trouve deux définitions, il émet une erreur de redéfinition.
Note
Imposer une seule définition par entité tient du bon sens. En effet, comment devrait réagir le compilateur s’il trouvait dans le même fichier source deux fonctions toto() effectuant des traitements différents !
Note
La règle ODR concerne les définitions, cependant, il n’y a pas de limite sur le nombre de déclarations associées à une même entité tant qu’elles restent identiques (non contradictoires).
Exercice
Dans le cas d’un unique fichier source présent dans le projet, indiquez si les affirmations suivantes sont vraies ou fausses :
En C++, définition et déclaration signifient la même chose.
La règle ODR indique qu’il faut que chaque entité soit définie au moins une fois durant la compilation.
Une déclaration introduit ou réintroduit un nom et un type.
On peut trouver plusieurs déclarations d’une même entité dans un fichier source.
Si une déclaration est présente, la définition devient optionnelle.
On peut utiliser un nom n’importe où dans le fichier source sans risque d’erreur.
Les variables
Vous pouvez retrouver une présentation sur les variables dans la référence sur le C++.
Syntaxe
Définition d’une variable :
Type Nom; // sans initialisation
Type Nom = ValeurInitiale; // avec initialisation
Définir une variable sans l’initialiser est risquée et doit être réservée à de très rares occasions.
Note
Les syntaxes à base d’accolades : int a = {40}; ou int a{40}; ou int a = {}; ou int a{}; sont hors programme.
Exemple
int unEntier; // définition d'une variable de type entier sans initialisation
double unNombreFloatant = 3.0; // définition de la variable unNombreFloatant de type double avec son initialisation
Avertissement
La déclaration de variable est hors programme
Initialisation des variables par défaut
Pour commencer, nous abandonnons une croyance assez tenace chez les élèves :
NON, les variables ne sont pas initialisées par défaut en C++.
NON, les variables numériques ne sont pas initialisées à 0 en C++.
Certains d’entre vous testerons cette affirmation à travers le programme suivant : le but est de créer un entier sans l’initialiser et d’afficher ensuite sa valeur à l’écran, ceci plusieurs fois.
#include <iostream>
int main()
{
for (int i = 0; i < 100 ; i++)
{
int a;
std::cout<<a;
}
return 0;
}
>> 0 0 0 0 0 0 0 ...
Avertissement
Sur 100 essais, vous obtiendrez probablement toujours la valeur 0. Mensonge, il y aurait une initialisation masquée !! Cette fausse impression est légitime, mais il y a une raison à cela. En effet, lors du lancement de votre programme, son espace mémoire est mis à zéro afin d’effacer toute trace d’informations provenant d’autres programmes. Ainsi, en début de programme, statistiquement, les variables numériques ont de forte chance d’occuper un espace mémoire contenant la valeur 0. Mais c’est un coup de chance si l’on peut dire ! Après un certain temps, le programme alloue de nouvelles variables en mémoire en lieu et place d’anciennes variables et là, les valeurs ne sont pas forcément nulles !
En transformant le programme précédent pour qu’il modifie la case mémoire de la variable a, on obtient un résultat beaucoup plus amusant :
int main()
{
for (int i = 0; i < 100 ; i++)
{
int a;
a += 1;
std::cout<<a<<" ";
}
return 0;
}
>> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35...
Cette exemple montre que la variable a lorsqu’elle est créée réutilise la case mémoire de la précédente variable a.
Avertissement
L’utilisation d’une variable non initialisée ne produit pas de message d’erreur. Il est possible de forcer le compilateur à déclencher une erreur en utilisant des paramètres de compilation supplémentaires -Wall -Werror.
Convention : tant qu’une variable n’est pas initialisée, il ne faut pas l’utiliser car son contenu est indéterminée.
Masquage
Pour une variable, la règle ODR s’applique à l’intérieur du bloc d’accolades où elle a été créée. Cependant, dans un sous-bloc d’accolades, vous pouvez redéfinir une variable portant le même nom qu’une variable précédente. Dans ce cas, la variable dans ce sous-bloc masque la précédente :
#include <iostream>
int main()
{
int a = 2;
{
int a = 3; // cette nouvelle variable a masque celle du bloc supérieur
std::cout<<a<< " ";
}
return 0;
}
>> 3
Cette situation est à éviter car elle nuit fortement à la lisibilité du programme.
Exemples d’erreurs
L’utilisation d’une variable avant toute définition produit une erreur de la forme : « le nom X n’est pas déclaré ». Voici un exemple :
#include <iostream>
int main()
{
std::cout << a;
return 0;
}
Le redéfinition d’une variable, même à l’identique, produit une erreur de compilation de la forme : « erreur de redéclaration ». Voici un exemple :
int main()
{
int a = 2;
int a = 2;
return 0;
}
Exercice 1
1 a = 0; 2 int a;
Exercice 2
1 int a = 2; 2 a = 4;
Exercice 3
1 int a; 2 int b = c; 3 int c;
Exercice 4
1 int a = 2; 2 int i = 3; 3 int c; 4 int d = a + j;
Le compilateur parcourt le code source de haut en bas. Dès qu’il rencontre un problème : identificateur inconnu, variable non initialisée… il s’arrête et envoie un message d’erreur. Pour chaque exemple, indiquez la ligne où se trouve l’erreur ou 0 si tout est correct :
Exercice 1
Exercice 2
Exercice 3
Exercice 4
Indiquez si les affirmations suivantes sont vraies ou fausses :
La définition d’une variable donne son nom et son type.
Le compilateur C++ accepte d’utiliser une variable non initialisée.
En C++, les numériques sont automatiquement initialisées à la valeur 0.
Deux définitions du même nom sont possibles si les types sont identiques.
On peut utiliser une variable après sa définition.
Les fonctions
Syntaxe
La spécification du C++ sur les fonctions nous informe que :
La déclaration d’une fonction permet d’indiquer son nom et son type (type de retour et type des arguments)
La définition d’une fonction est une déclaration suivie d’un corps de fonction (function body) délimité par une paire d’accolades { } et contenant l’ensemble des traitements effectués par cette fonction.
Déclaration d’une fonction :
TypeRetour NomFonction(Type1 NomParametre1, Type2 NomParamètre2);
Définition d’une fonction :
Note
Les fonctions ne retournant pas de valeur doivent retourner le type void.
Forward declaration
Si on ne disposait pas des déclarations, cela obligerait les programmeurs à organiser les fonctions dans un certain ordre dans le code. Par exemple, si une fonction A() appelait une fonction B(), la définition de la fonction B() devrait alors être placée avant la définition de la fonction A() dans le source.
Heureusement, l’utilisation des déclarations permet de ne plus avoir à se soucier de l’ordre dans lequel les définitions des fonctions A() et B() doivent se trouver. En effet, il suffit d’écrire une déclaration (Forward declaration) de la fonction B() avant toute utilisation de cette fonction dans le programme. Voici un exemple :
void B(); // Forward declaration de la fonction B
void A() // définition de la fonction A
{
B(); // la fonction B peut être appelée car elle a été déclarée auparavant
}
void B() // la définition de la fonction B est placée après celle de A grâce au mécanisme de Forward Declaration
{
...
}
Exemples d’erreur
L’utilisation d’une fonction avant toute définition ou déclaration produit une erreur de la forme : « le nom n’est pas déclaré ». Voici un exemple :
int main()
{
F(); => ERREUR : "le nom F n'est pas déclaré".
return 0;
}
void F() {}
Le redéfinition d’une fonction, même à l’identique, produit une erreur de compilation de la forme : « Le nom a déjà été défini ». Voici un exemple :
void F() {}
void F() {} => ERREUR : Le nom F a déjà été défini
int main()
{
F();
return 0;
}
La déclaration d’une fonction sans définition produit une erreur de la forme « aucune définition trouvée » :
void F();
int main()
{
F();
return 0;
}
Indiquez si les affirmations suivantes sont vraies ou fausses :
En C++, définition et déclaration de fonctions signifient la même chose.
Si la fonction ne retourne pas de valeur, son type de retour est absent.
Une définition de fonction doit inclure un corps de fonction.
Un corps de fonction est délimité par une paire de crochets.
Un corps de fonction se termine par un point virgule ;
Un appel de fonction sans déclaration préalable déclenche une erreur.
La syntaxe d’une déclaration de fonction se termine par une parenthèse )
Si le compilateur ne trouve aucune définition, il n’émet pas d’erreur.
Si le compilateur trouve deux définitions identiques dans le même fichier, il n’émet pas d’erreur.
Les structures
Nous rappelons qu’un type structure désigne un type composé. Il s’agit d’une classe dont tous les membres sont publics par défaut.
Avertissement
Attention, définir un type structure nommée S et définir une variable de type S sont deux choses différentes. Dans le premier cas, on définit une classe dans l’autre on instancie un objet.
Syntaxe
On trouve :
La déclaration d’un type structure permet de déclarer son nom et son type (struct)
La définition d’un type structure inclut la déclaration suivie d’une liste de variables/fonctions présentes dans la structure. Cette liste est délimitée par une paire d’accolades et se termine par un ;
Déclaration d’une structure :
struct Nom;
Définition d’une structure :
Avertissement
Après une déclaration, la structure peut être utilisée de manière très limitée. En effet, aucune information n’est donnée sur les membres internes et il n’est donc pas possible d’y accéder. On peut cependant créer des pointeurs vers cette structure.
Exemples d’erreur
Si nous insérons deux fois une définition dans un même fichier, ceci produit une erreur de redéfinition :
struct A { int a; };
struct A { int a; }; // erreur : définition antérieure de struct A
int main() { }
Indiquez si les affirmations suivantes sont vraies ou fausses :
Un type structure est un type.
La syntaxe de la définition d’un type structure se termine par une accolade }
Dans un même fichier, on peut définir plusieurs fois le même type structure.