Fichiers d’en-tête (C++)

Les noms des éléments de programme tels que les variables, les fonctions, les classes, etc. doivent être déclarés avant de pouvoir être utilisés. Par exemple, vous ne pouvez pas simplement écrire x = 42 sans d’abord déclarer « x ».

int x; // declaration
x = 42; // use x

La déclaration indique au compilateur si l’élément est un int, un double, une fonction, une class ou une autre chose. De plus, chaque nom doit être déclaré (directement ou indirectement) dans chaque fichier .cpp dans lequel il est utilisé. Lorsque vous compilez un programme, chaque fichier .cpp est compilé indépendamment dans une unité de compilation. Le compilateur n’a aucune connaissance des noms déclarés dans d’autres unités de compilation. Cela signifie que si vous définissez une classe ou une fonction ou une variable globale, vous devez fournir une déclaration de cette chose dans chaque fichier .cpp supplémentaire qui l’utilise. Chaque déclaration de cette chose doit être exactement identique dans tous les fichiers. Une légère incohérence entraîne des erreurs ou un comportement inattendu lorsque l’éditeur de liens tente de fusionner toutes les unités de compilation dans un seul programme.

Pour réduire le risque d’erreurs, C++ a adopté la convention d’utilisation de fichiers d’en-tête pour contenir des déclarations. Vous effectuez les déclarations dans un fichier d’en-tête, puis utilisez la directive #include dans chaque fichier .cpp ou un autre fichier d’en-tête qui nécessite cette déclaration. La directive #include insère une copie du fichier d’en-tête directement dans le fichier .cpp avant la compilation.

Remarque

Dans Visual Studio 2019, la fonctionnalité modules C++20 est introduite comme amélioration et remplacement éventuel des fichiers d’en-tête. Pour plus d’informations, consultez Vue d’ensemble des modules en C++.

Exemple

L’exemple suivant montre un moyen courant de déclarer une classe, puis de l’utiliser dans un autre fichier source. Nous allons commencer par le fichier d’en-tête. my_class.h Il contient une définition de classe, mais notez que la définition est incomplète ; la fonction do_something membre n’est pas définie :

// my_class.h
namespace N
{
    class my_class
    {
    public:
        void do_something();
    };

}

Ensuite, créez un fichier d’implémentation (généralement avec une extension .cpp ou similaire). Nous allons appeler le fichier my_class.cpp et fournir une définition pour la déclaration de membre. Nous ajoutons une #include directive pour le fichier « my_class.h » afin d’insérer la déclaration my_class à ce stade dans le fichier .cpp, et nous incluons <iostream> l’extraction dans la déclaration pour std::cout. Notez que les guillemets sont utilisés pour les fichiers d’en-tête dans le même répertoire que le fichier source et les crochets d’angle sont utilisés pour les en-têtes de bibliothèque standard. En outre, de nombreux en-têtes de bibliothèque standard ne disposent pas de .h ou d’une autre extension de fichier.

Dans le fichier d’implémentation, nous pouvons éventuellement utiliser une using instruction pour éviter d’avoir à qualifier toutes les mentions « my_class » ou « cout » avec « N :: » ou « std :: ». Ne placez using pas d’instructions dans vos fichiers d’en-tête !

// my_class.cpp
#include "my_class.h" // header in local directory
#include <iostream> // header in standard library

using namespace N;
using namespace std;

void my_class::do_something()
{
    cout << "Doing something!" << endl;
}

À présent, nous pouvons utiliser my_class dans un autre fichier .cpp. Nous #include le fichier d’en-tête afin que le compilateur extrait la déclaration. Tout le compilateur doit savoir que my_class est une classe qui a une fonction membre publique appelée do_something().

// my_program.cpp
#include "my_class.h"

using namespace N;

int main()
{
    my_class mc;
    mc.do_something();
    return 0;
}

Une fois que le compilateur a terminé la compilation de chaque fichier .cpp dans des fichiers .obj, il transmet les fichiers .obj à l’éditeur de liens. Lorsque l’éditeur de liens fusionne les fichiers objet, il trouve exactement une définition pour my_class ; il se trouve dans le fichier .obj généré pour my_class.cpp, et la build réussit.

Inclure des gardes

En règle générale, les fichiers d’en-tête ont une protection include ou une #pragma once directive pour s’assurer qu’ils ne sont pas insérés plusieurs fois dans un seul fichier .cpp.

// my_class.h
#ifndef MY_CLASS_H // include guard
#define MY_CLASS_H

namespace N
{
    class my_class
    {
    public:
        void do_something();
    };
}

#endif /* MY_CLASS_H */

Éléments à placer dans un fichier d’en-tête

Étant donné qu’un fichier d’en-tête peut potentiellement être inclus par plusieurs fichiers, il ne peut pas contenir de définitions susceptibles de produire plusieurs définitions du même nom. Les éléments suivants ne sont pas autorisés ou sont considérés comme très mauvais :

  • Définitions de type intégrées à l’espace de noms ou à l’étendue globale
  • définitions de fonction non intégrées
  • définitions de variables non const
  • définitions d’agrégation
  • espaces de noms sans nom
  • Directives using

L’utilisation de la using directive n’entraîne pas nécessairement une erreur, mais peut entraîner un problème, car elle place l’espace de noms dans l’étendue de chaque fichier .cpp qui inclut directement ou indirectement cet en-tête.

Exemple de fichier d’en-tête

L’exemple suivant montre les différents types de déclarations et définitions autorisées dans un fichier d’en-tête :

// sample.h
#pragma once
#include <vector> // #include directive
#include <string>

namespace N  // namespace declaration
{
    inline namespace P
    {
        //...
    }

    enum class colors : short { red, blue, purple, azure };

    const double PI = 3.14;  // const and constexpr definitions
    constexpr int MeaningOfLife{ 42 };
    constexpr int get_meaning()
    {
        static_assert(MeaningOfLife == 42, "unexpected!"); // static_assert
        return MeaningOfLife;
    }
    using vstr = std::vector<int>;  // type alias
    extern double d; // extern variable

#define LOG   // macro definition

#ifdef LOG   // conditional compilation directive
    void print_to_log();
#endif

    class my_class   // regular class definition,
    {                // but no non-inline function definitions

        friend class other_class;
    public:
        void do_something();   // definition in my_class.cpp
        inline void put_value(int i) { vals.push_back(i); } // inline OK

    private:
        vstr vals;
        int i;
    };

    struct RGB
    {
        short r{ 0 };  // member initialization
        short g{ 0 };
        short b{ 0 };
    };

    template <typename T>  // template definition
    class value_store
    {
    public:
        value_store<T>() = default;
        void write_value(T val)
        {
            //... function definition OK in template
        }
    private:
        std::vector<T> vals;
    };

    template <typename T>  // template declaration
    class value_widget;
}