Parcours
/
C++
C++
/
Programme
/
En-têtes
En

En-têtes en C++

2 exercices

À propos de En-têtes

En C++, les déclarations sont souvent séparées des définitions. Les déclarations sont regroupées dans ce qu'on appelle des fichiers d'en-tête, les implémentations correspondantes étant placées dans des fichiers source. On peut voir les fichiers d'en-tête comme une API. Le fichier d'en-tête t'indique ce qu'une base de code a à offrir sans entrer dans les détails du comment.

En-tête et source

L'extension de fichier la plus courante pour les fichiers d'en-tête est .h. Certains projets utilisent .hpp ou omettent complètement l'extension.

Les définitions se trouvent dans un fichier .cpp séparé. Pour réunir les parties, le fichier source commence par inclure le fichier d'en-tête correspondant.

Si tu veux écrire une bibliothèque appelée « quick_math » qui propose une fonction « super_root » que tu veux utiliser souvent, les fichiers ressembleraient à ceci :

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

Si tu dois inclure un en-tête qui n'est requis que par l'implémentation, la ligne #include correspondante n'est nécessaire que dans le fichier source. Tout ce qui est inclus dans l'en-tête est également disponible dans le fichier .cpp, comme la bibliothèque string dans l'exemple ci-dessous. Attention : le ; est nécessaire après la déclaration dans le fichier d'en-tête, mais pas après la définition dans le fichier source.

Note

Beaucoup d'exercices C++ sur Exercism commencent par deux fichiers presque vides : en-tête et source. Tu dois consulter le fichier *_test.cpp pour voir les noms et les espaces de noms des fonctions attendues afin de résoudre l'exercice.

Classes et en-têtes

Les classes peuvent devenir très complexes, et leur rapport à la séparation en-tête / source peut être déroutant. Une organisation possible consiste à garder tous les détails d'implémentation dans le fichier source et toutes les déclarations et variables membres dans l'en-tête :

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

Quand l'en-tête sert d'aperçu de l'API, c'est là qu'on cherchera des informations comme les valeurs par défaut. La valeur par défaut du paramètre size du constructeur est donc gérée dans l'en-tête et non dans l'implémentation. Les définitions dans le fichier source sont préfixées par l'espace de noms robots et le type de classe Flower.

Une autre organisation possible est une bibliothèque header only, qui n'a pas du tout de fichier .cpp :

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

Les projets peuvent combiner ces organisations, et les discussions vont bon train sur ce qui convient le mieux à chaque cas d'usage.

Gardes d'inclusion

Tu as peut-être remarqué la ligne #pragma once dans le fichier d'en-tête d'exemple ci-dessus. C'est ce qu'on appelle une garde d'inclusion, et elle garantit que le contenu du fichier n'est inclus qu'une seule fois pendant la compilation afin d'éviter des erreurs. Il existe une autre variante, plus complexe, d'une garde d'inclusion, qui commence par #ifndef et se termine par #endif, détaillée ci-dessous.

Advanced

Déclarations anticipées

Le code C++ est évalué de manière procédurale. Si tu veux utiliser une fonction, elle doit être connue du compilateur au moment où tu l'utilises. Il n'est parfois pas possible de trouver un ordre linéaire pour disposer les définitions dans le code source. Regarde l'exemple ci-dessous :

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

Quand myFunction est définie, le compilateur ne connaît pas encore myOtherFunction. Malheureusement, le problème de référence circulaire ne peut pas être résolu en changeant l'ordre.

C++ propose les déclarations anticipées pour faire connaître myFunction et myOtherFunction au compilateur avant qu'elles soient définies. Le compilateur suppose que la définition suivra à un moment donné après la déclaration. L'exemple suivant montre comment une déclaration anticipée est utilisée pour les fonctions.

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

Gardes d'inclusion via ifndef

Peu importe qu'un même fichier soit inclus plusieurs fois dans un projet. Les fichiers d'en-tête ne doivent pas contenir de définitions. Un projet complet ne peut pas contenir la même définition plus d'une fois. C'est ce qu'on appelle la « règle de définition unique ». Elle est appliquée par le compilateur.

Il est facile d'éviter les inclusions multiples non voulues d'une même définition grâce aux gardes d'inclusion. Elles sont mises en place par une procédure spéciale lors des étapes de compilation. Le but est de n'inclure un fichier que si une certaine variable n'a pas été définie, puis de la définir une fois le fichier inclus. Souvent, le nom de cette variable est choisi comme une variante du nom du fichier. Une autre méthode consiste à générer un UUID pour réduire le risque d'utiliser accidentellement la même séquence deux fois. La syntaxe est visible ci-dessous, avec MY_HEADER_FILE_H comme variable.

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

Le problème avec #pragma once, c'est que les pragmas ne font pas officiellement partie du langage C++ et que leur implémentation diffère d'un compilateur à l'autre. De nombreux grands projets sont passés à la méthode du pragma, plus simple, mais quelques-uns restent prudents.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends En-têtes