Définition & déclaration

Le langage C++ s’est construit au fil du temps par ajouts successifs de fonctionnalités. Ainsi, chaque nouveau besoin s’est traduit par la mise en place d’une syntaxe et de son intégration dans la chaîne de compilation. Le langage s’est donc construit par strates. Cependant, lorsqu’on cherche à décrire un langage, on a tendance à essayer de présenter des grands principes afin d’extraire la vision de ses créateurs. Malheureusement, pour le langage C++, ces principes, même s’ils existent, n’ont pas toujours pu être harmonisés d’un élément du langage à l’autre. On ne pourra ainsi énoncer ses principes qu’en leur donnant des contours assez floues pour regrouper des éléments du langage assez différents. L’exposé du principe général se voit ainsi rapidement contrebalancé par une présentation au cas par cas. Ne l’oublions pas, le langage C++ a été conçu dans un soucis de performance et de souplesse dans le codage. Il n’a pas été conçu pour être facile à appréhender ou intuitif. Pour toutes ces raisons, ce langage est particulièrement difficile à prendre en main pour les débutants. Ce chapitre sur les déclarations et les définitions en C++ est particulièrement complexe à mettre en place du fait de la variété des situations. Dans ce chapitre, nous avons voulu éviter de noyer notre présentation dans un déluge de cas particuliers et de dérogations de règles associées à certains mots clefs. Nous allons ainsi nous placer dans un cadre idéal où dans ce chapitre nous traitons un programme comportant un seul fichier source sans aucune inclusion. Le cas général sera vu plus tard dans le chapitre sur la compilation croisée.

Principe général

La notion de définition en C++

Nous désignons par entité : une variable, une fonction, une structure…

Dans la terminologie du langage C++, une définition sert à deux choses :

  • Elle déclare un nom et l’associe à un type.

  • Elle donne suffisamment d’informations au compilateur pour la mise en place de l’entité.

Nous donnons maintenant deux règles fondamentales en C++ :

REGLE : Après avoir défini une entité, on peut l’utiliser dans le code.

Lors de la compilation d’un fichier source, le principe intitulé One Definition Rule s’applique.

REGLE ODR (One Definition Rule) : Lors de la compilation d’un fichier cpp, une seule définition par variable / fonction / structure / classe / enumeration / template est autorisée.

Note

Durant la compilation d’un fichier source, le compilateur doit trouver une et une seule définition. S’il existe deux définitions, le compilateur émet une erreur de redéfinition.

Chaque type d’entités utilise une syntaxe spécifique pour sa définition.

La notion de déclaration en C++

Dans la terminologie du langage C++, déclaration et définition sont deux notions proches mais différentes. Faîtes très attention, car dans les 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 un nom dans un programme C++ en rappelant son type.

Nous donnons le principe général des déclarations en C++ :

REGLE 1 : Après avoir déclaré une entité, on peut utiliser son nom dans le code comme habituellement ou avec des restrictions.

REGLE 2 : Lors de la compilation d’un fichier cpp, zéro à plusieurs déclarations d’une même entité sont autorisées.

Chaque type d’entités utilise une syntaxe spécifique pour sa déclaration.

Définition vs déclaration

Dans la logique du C++, comme une définition remplit les critères d’une déclaration, une définition est aussi une déclaration. On peut voir une déclaration comme une définition tronquée dont on ne garde que le nom et le type.

Une définition sert à décrire et à créer une entité. Ainsi, si vous suivez la logique du C++, la définition d’une entité doit être unique afin d’éviter des versions contradictoires durant une phase de compilation.

Dans le cas d’un unique fichier source présent dans le projet, indiquez si les affirmations suivantes sont vraies ou fausses :

  1. En C++, définition et déclaration signifient la même chose.

  2. La règle ODR indique qu’il faut que chaque entité soit définie une fois ou plus durant la compilation.

  3. Une déclaration introduit ou réintroduit un nom et un type.

  4. On peut trouver plusieurs déclarations d’une même entité dans un fichier source.

  5. Si la déclaration d’un nom est présente dans le fichier source, la définition de ce nom devient optionnelle.

  6. 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++.

Définition

Rappel

La définition d’une variable :

  • Déclare son nom et son type (partie déclaration)

  • Alloue physiquement de la mémoire pour stocker les informations correspondantes à cette variable, ce qui lui permettra de fonctionner.

Les règles de base s’appliquent :

  • Une fois définie, une variable peut être utilisée.

  • La définition d’une variable doit être unique.

Note

La définition d’une variable est unique si l’on considère un bloc entre accolades.

Usage

La règle suivante n’est pas une règle du C++ mais une règle de bon sens :

REGLE : Pour utiliser une variable, il faut l’avoir initialisée précédemment.

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. Par bon sens, tant qu’une variable n’est pas initialisée, il ne faut pas l’utiliser car son contenu est indéterminée.

Avertissement

Trop souvent des étudiants croient, à tort, que les variables sont initialisées par défaut en C++. Dans cette croyance, les variables numériques seraient ainsi initialisées à zéro. Cette fausse impression existe pour deux raisons. La première raison vient du langage Java où effectivement les variables d’objets sont initialisées par défaut, les numériques étant mis par défaut à 0. La deuxième raison vient du fait que si vous affichez un entier non initialisé, vous obtiendrez probablement 0 comme résultat. Cela vient du fait que la mémoire du programme est mise à zéro au démarrage pour éviter d’avoir accès aux données d’un autre programme venant de se terminer. Ainsi, en début de programme, statistiquement, les variables numériques peuvent valoir 0. Mais c’est un coup de chance !

Syntaxe

SYNTAXE - Définition d’une variable

  1. Type Nom = ValeurInitiale;

  2. Type Nom;

La première syntaxe permet de définir une variable et de l’initialiser. Il s’agit de la forme la plus courante et la plus sécurisée. L’écriture int a; définit une variable sans l’initialiser. Cette écriture 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

Déclaration

La déclaration de variables permet la création de variables globales. Cet usage étant source de nombreux défauts de conception dans les programmes, nous avons décidé de ne pas présenter ce cas. Nous vous demandons de ne pas utiliser de variables globales dans vos projets.

Exemples d’erreurs

L’utilisation d’une variable avant toute définition produit une erreur de la forme : « le nom F n’est pas déclaré ». Voici un exemple :

#include <iostream>

int main()
{
        std::cout << a;

        int a = 4;

        return a;
}

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 b = 3;
3    int c;
4    int d = a + B;

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 :

  1. Durant la définition d’une variable, l’espace mémoire nécessaire est allouée.

  2. La définition d’une variable donne son nom et son type.

  3. Le compilateur C++ accepte d’utiliser une variable non initialisée.

  4. En C++, les numériques sont automatiquement initialisées à la valeur 0.

  5. Deux définitions du même nom sont possibles si les types sont identiques.

  6. On peut utiliser une variable après sa définition.

Les fonctions

Définition

Rappel

La spécification du C++ sur les fonctions indique qu’une définition d’une fonction doit :

  • Déclarer son nom et son type (valeur de retour et arguments)

  • Avoir un corps de fonction (function body) délimité par une paire d’accolades { } et contenant l’ensemble des traitements effectués par cette fonction.

Syntaxe

SYNTAXE - Définition d’une fonction

TypeRetour NomFonction(Type1 NomParametre1, Type2 NomParamètre2)
{
// instructions
}

Note

Les fonctions ne retournant pas de valeur doivent retourner un type void.

Déclaration

Rappel

La déclaration d’une fonction permet de :

  • Déclarer son nom et son type (valeur de retour et arguments)

  • Après une déclaration, le nom de la fonction peut être utilisée normalement.

Comparée à une définition de fonction, la déclaration de fonction est privée d’un corps de fonction.

Syntaxe

SYNTAXE - Déclaration d’une fonction

TypeRetour NomFonction(Type1 NomParametre1, Type2 NomParamètre2);

Exemple

On ne peut appeler une fonction qu’après sa définition. Cette contrainte oblige à placer les fonctions dans un certain ordre afin de gérer leurs dépendances. Par exemple, si la fonction A() appelle la fonction B() qui appelle la fonction C(), on doit trouver successivement dans le code la définition de C() puis de B() puis de A() :

void C() { ... }

void B()  // La fonction B() appelle la fonction C(), elle doit être placée après
{
        C();  // la fonction C() a été définie avant, son nom est connu, tout est ok
}

void A()  // La fonction A() appelle la fonction B(), elle doit être placée après
{
   B();   // la fonction B() a été définie avant, son nom est connu, tout est ok
}

Cependant, certains agencements sont impossibles comme dans le cas où deux fonctions s’appellent entre elles :

void A()  // définition de la fonction A()
{
   if (...) B();  => ERREUR : le nom B() est inconnu
}

void B()  // définition de la fonction B()
{
   if (...) A();  // le nom de la fonction A() est connu, OK
}

Pour résoudre ce problème, il suffit de déclarer les fonctions A() et B() en premier ce qui permet de ne plus avoir à se préoccuper de quelle fonction doit être définie en premier :

// déclaration des fonctions A() et  B()
// à ce niveau, on ne sait pas ce qu'elle fait, mais son nom est connu

void A();
void B();

void A()  // la définition de la fonction A() est identique à sa déclaration => OK
{
        B();  // le nom B() est connu => plus d'erreur maintenant
}

void B()  // la définition de la fonction B() est identique à sa déclaration => OK
{
        A();
}

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 liaison de la forme « aucune définition trouvée » :

void F();

int main()
{
        F();
        return 0;
}

Surcharge des fonctions

Il existe un mécanisme en C++ permettant, pour un même nom de fonction, d’avoir plusieurs définitions à condition que les types de leurs paramètres diffèrent. Voici un exemple :

int    min(int a, int b) {...}
double min(double a, double b) {...}

Le compilateur C++ utilise pour cela un mécanisme de résolution de surcharge (overload resolution). Lorsque le compilateur rencontre un appel de fonction, il liste l’ensemble des fonctions possibles et appelle la plus adéquate. Ce mécanisme est complexe et fait appel à des règles précises pour lever les indéterminés :

  • Si le compilateur trouve une fonction avec les types adéquats, il la choisit

  • Sinon, il liste toutes les fonctions compatibles quitte à effectuer des conversions implicites
    • Si le compilateur en trouve une seule, il la choisit

    • S’il en trouve plusieurs, le compilateur conserve les fonctions effectuant le moins de conversions implicites
      • S’il reste une seule fonction, le compilateur l’a choisie

      • Sinon il émet un message d’erreur car la situation est ambiguë

Voici un exemple :

#include <iostream>
int    min(int a, int b)            { return 1;   }
double min(double a, double b)      { return 1.2; }

int main()
{
        std::cout << min(3,4);      // le compilateur va choisir min(int,int);
        std::cout << min(2.0,1.0);  // le compilateur va choisir min(doule, double);
        std::cout << min(3.0,5);    // cas indécidable, 2 compatibles, mais une conversion implicite pour chacune
}

L’erreur obtenue est : call of overloaded ‘min(double, int)’ is ambiguous.

Avertissement

Dans ce mécanisme, seuls les paramètres sont utilisés et non le type de retour. Par conséquent, l’exemple suivant ne correspond pas à une surcharge de fonction et il produit une erreur :

int    test(int a)      { return a;  }
double test(int a)      { return a + 1.0;}

Indiquez si les affirmations suivantes sont vraies ou fausses :

  1. En C++, définition et déclaration de fonctions signifient la même chose.

  2. Si la fonction ne retourne pas de valeur, son type de retour est absent.

  3. Une définition de fonction doit inclure un corps de fonction.

  4. Un corps de fonction est délimité par une paire de crochets.

  5. Un corps de fonction se termine par un point virgule ;

  6. Un appel de fonction sans déclaration préalable déclenche une erreur.

  7. La syntaxe d’une déclaration de fonction se termine par une parenthèse )

  8. Si le compilateur ne trouve aucune définition, il n’émet pas d’erreur.

  9. Si le compilateur trouve deux définitions identiques dans le même fichier, il n’émet pas d’erreur.

  10. La surcharge de fonctions permet d’avoir des fonctions de même nom avec des paramètres différents.

  11. Si des fonctions sont disponibles, la résolution de surcharge trouve toujours une fonction adéquate.

Soit le programme suivant:

1 int    F(int a)               {  return a; }
2 double F(double a)            {  return a; }
3 double F(int i, int j)        {  return i; }
4 double F(double i, int j)     {  return i; }

Pour chaque chaque appel de fonction, indiquez quelle définition de F() est utilisée ou Erreur si l’appel n’est pas valide.

Appel de fonction

Définition utilisée

F(1.0)

F(2)

F(2.0,5)

F(1, 2.0)

F(1.0, 2)

F(1.0, 2.0)

Les structures

Nous rappelons qu’un type structure désigne un type composé. Attention, définir un type structure nommée S et définir une variable de type S sont deux choses différentes.

Définition

La définition d’un type structure doit :

  • Déclarer son nom et son type (struct)

  • Lister les variables présentes dans le type structure, ceci entre une paire d’accolades terminée par un ;

SYNTAXE - Définition d’un type structure :

struct Nom
{
Type1 var1;
Type2 var2;
};

Si nous insérons deux fois une définition de type structure dans le même fichier, ceci produit une erreur :

struct A { int a; };
struct A { int a; };   // erreur : définition antérieure de struct A

int main()      {       }

Déclaration

Rappel

La déclaration d’un type structure permet de :

  • Déclarer son nom et son type struct

  • Après une déclaration, le nom de la structure peut être utilisé de manière très limitée.

Syntaxe

SYNTAXE - Déclaration d’un type structure

struct Nom;

La syntaxe d’une déclaration de type structure est assez réduite car elle ne spécifie que le nom. Ainsi, la déclaration d’un type structure est relativement peu utile car elle ne permet pas de créer des variables de ce type par la suite.

Pour cette raison, les définitions des types structure ont tendance à se trouver en début de fichier afin de rendre ces types structures disponibles pour l’ensemble du code source. Les déclarations se trouvent par conséquent peu utilisées.

Indiquez si les affirmations suivantes sont vraies ou fausses :

  1. Un type structure est un type.

  2. La syntaxe de la définition d’un type structure se termine par une accolade }

  3. Dans un même fichier, on peut définir plusieurs fois le même type structure.

  4. La déclaration d’un type structure ne permet pas de créer des variables de ce type.