Modularité, compilation séparée {#part.modules}

Rappelez vous notre livre de recettes. À l’époque, nous avons vu comment découper un programme en fonctions pour plus de modularité. Cela permet de mieux le comprendre, petit bout par petit bout, d’éviter les redites, … Nous allons de même découper un programme en plusieurs fichiers.

Compilation séparée

Exemple

Considérons les deux programmes suivants

#include <iostream>
using namespace std;

int monMax(int a, int b) {
    if ( a >= b )
        return a;
    else
        return b;
}

int main() {
    cout << monMax(1, 3) << endl;
    return 0;
}
#include <iostream>
using namespace std;

int monMax(int a, int b) {
    if ( a >= b )
        return a;
    else
        return b;
}

int main() {
    cout << "Entrez a et b:" << endl;
    int a, b;
    cin >> a >> b;
    cout << "Le maximum est: "
         << monMax(a, b) << endl;
    return 0;
}

On constate une répétition: les deux programmes définissent exactement la même fonction monMax, qu’ils utilisent ensuite différemment.

Pourrait on partager la fonction monMax entre ces deux programmes?

C’est ce que nous allons faire en définissant une mini bibliothèque. Voyons à quoi cela ressemble

Exemple: la bibliothèque max

/** La fonction max
 *  @param x, y deux entiers
 *  @return un entier,
 *  le maximum de x et de y
 **/
int monMax(int a, int b);
#include "max.h"

int monMax(int a, int b) {
    if ( a >= b )
        return a;
    else
        return b;
}

Exemple: Deux programmes utilisant la bibliothèque max

#include <iostream>
using namespace std;
#include "max.h"

int main() {
    cout << monMax(1, 3) << endl;
    return 0;
}
#include <iostream>
using namespace std;
#include "max.h"

int main() {
    cout << "Entrez a et b:" << endl;
    int a, b;
    cin >> a >> b;
    cout << "Le maximum est: "
         << monMax(a, b) << endl;
    return 0;
}

Exemple: Les tests de la bibliothèque max

#include <iostream>
using namespace std;

#include "max.h"

/** Infrastructure minimale de test **/
#define CHECK(test) if (!(test)) cout << "Test failed in file " << __FILE__ << " line " << __LINE__ << ": " #test << endl

void monMaxTest() {
    CHECK( monMax(2,3) == 3 );
    CHECK( monMax(5,2) == 5 );
    CHECK( monMax(1,1) == 1 );
}

int main() {
    monMaxTest();
}

Qu’avons nous vu?

Déclaration de fonctions

Syntaxe:

int monMax(int a, int b);

Sémantique:

  • Le programme définit quelque part une fonction monMax avec cette signature: type des paramètres et type du résultat

  • Cette définition n’est pas forcément dans le même fichier

  • Si cette définition n’existe pas ou n’est pas unique, une erreur est déclenchée par le compilateur

  • Cette erreur est déclenchée au moment où l’on combine les différents fichiers (édition de liens; voir plus loin)

♣ application:

Deux fonctions qui s’appellent réciproquement

Compilation séparée (1)

  • Un programme peut être composé de plusieurs fichiers source
    Contenu:

    • Des définitions de fonctions

    • Des variables globales, …

  • Chaque fichier source est compilé en un fichier objet (extension: .o)
    Contenu:

    • Le code binaire des fonctions, …

  • L”éditeur de lien combine plusieurs fichiers objet en un fichier exécutable

Voyons cela pour un programme voulant utiliser la bibliothèque max:

Les sources sont max.cpp et programme.cpp

On les compile séparément avec:

g++ -c max.cpp
g++ -c programme.cpp

Cela produit les fichiers objets max.o et programme.o. Chacun est un bout incomplet de programmes binaires: max.o contient le code binaire de la fonction max mais pas la fonction main, et réciproquement pour programme.o.

Il ne reste plus qu’à combine ces deux bouts de programmes binaires pour obtenir un programme complet.

g++ programme.o max.o -o programme

Maintenant, on peut exécuter le programme obtenu autant de fois qu’on le souhaite:

./programme

Compilation séparée (2)

Au moment de l’édition de lien:

  • Chaque fonction utilisée doit être définie une et une seule fois

  • La fonction main doit être définie une et une seule fois

Quelques variantes autour des fichiers objets ♣:

  • Bibliothèques (.a):
    Une archive contenant plusieurs fichiers objets .o

  • Bibliothèques dynamiques (.so):
    Édition de lien dynamique au lancement du programme

Fichiers d’entête

Fichier .h contenant la déclaration des fonctions définies dans le fichier .cpp correspondant

Exemple: Fichier d’entête max.h

int monMax(int a, int b);

Syntaxe: Utilisation d’un fichier d’entête

#include "max.h"

Sémantique:

Utiliser la bibliothèque max

Implantation en C++:

  • Équivalent à copier-coller le contenu de max.h à l’emplacement du #include "max.h"

  • ♣ Géré par le préprocesseur (cpp)

Inclusion de fichiers d’entêtes standards

Syntaxe:

          #include <iostream>

Sémantique:

  • Charge la déclaration de toutes les fonctions définies dans la bibliothèque standard iostream de C++

  • Le fichier iostream est recherché dans les répertoires standards du système

  • Sous linux: /usr/include, …

Résumé

Résumé: implantation d’une bibliothèque en C++

Écrire un fichier d’entête (max.h):

  • La déclaration de toutes les fonctions publiques

  • Avec leur documentation!

Écrire un fichier source (max.cpp):

  • La définition de toutes les fonctions

  • Inclure le fichier .h!

Écrire un fichier de tests (maxTest.cpp):

  • Les fonctions de tests

  • Une fonction main lançant tous les tests

Résumé: utilisation d’une bibliothèque en C++

Inclusion des entêtes:

      #include <iostream>  // fichier d'entête standard
      #include "max.h"     // fichier d'entête perso

Compilation:

      g++ -c max.cpp
      g++ -c programme1.cpp
      g++ max.o programme1.o -o programme1

En une seule étape:

      g++ max.cpp programme1.cpp -o programme1