Cours C++ #2
Théorie
Contenu:
1. Déclaration et définition: .h et .cc
2. Objets et méthodes CONST
3. Notion de contrat
4. Objets dans des objets
5. Friend: Fonctions et classes
6. Pointeur this
7. Allocation dynamique: new & delete
8. Interface / cacher l'implémentation


0- Retour sur cours 1 et devoir

1. Déclaration et définition: .h et .cc

Comment séparer la déclaration d'une classe de la définition dans le même fichier?

L'exemple suivant montre que tout est dans le même fichier:

Fichier Exemple.cc:
------------------------------
class Heure
{
public:
    Heure();
    void asgnSec(int pSec);

private:
    int aSec, aMin, aHeure;
};

int main()
{
...
}

Heure::Heure()
{
    aSec = 0;
    aMin = 0;
    aHeure= 0;
}

void Heure::asgnSec(int pSec)
{
    if (pSec >= 0 && pSec < 60) {
        aSec = pSec;
    }
    else {
        aSec = 0;
    }
}
------------------------------

Dans un projet avec plusieurs classes, il est impossible de tout déclarer dans le même fichier!  Il faut donc faire 2 fichiers par classe: un fichier d'en-tête (xxx.h) et un fichier source (xxx.cc).

- Dans le fichier en-tête, on met la déclaration de la classe.
- On ajoute au début du fichier .h une commande préprocesseur #ifndef suivie d'une #define portant un nom relié à la classe, de même qu'un #endif à la fin du fichier, ceci afin d'éviter les inclusions multiples de la déclaration de la classe.
- On inclut TOUJOURS le fichier en-tête de la classe en PREMIER dans le fichier .cc afin d'éviter que des déclarations soient manquantes dans le fichier en-tête, ce qui aurait pour effet d'exiger des en-têtes supplémentaires dans un tierce fichier.

Voici donc le fichier Exemple.cc séparé en 3 fichiers: Heure.h, Heure.cc et Exemple.cc:

Fichier Heure.h:
------------------------------
#ifndef HEURE_H_DEJA_INCLUS
#define HEURE_H_DEJA_INCLUS

class Heure
{
public:
    Heure();
    void asgnSec(int pSec);

private:
    int aSec, aMin, aHeure;
};

#endif // #ifndef HEURE_H_DEJA_INCLUS
------------------------------

Fichier Heure.cc:
------------------------------
#include "Heure.h"

Heure::Heure()
{
    aSec = 0;
    aMin = 0;
    aHeure= 0;
}

void Heure::asgnSec(int pSec)
{
    if (pSec >= 0 && pSec < 60) {
        aSec = pSec;
    }
    else {
        aSec = 0;
    }
}
------------------------------

Fichier Exemple.cc:
------------------------------
#include "Heure.h"

int main()
{
   Heure lHeure();
}

------------------------------

2. Objets et méthodes CONST

- Un objet déclaré const ne peut être modifié par suite de son instanciation.
- Un objet déclaré const doit donc être initialisé à sa construction.
- La définition de plusieurs constructeurs (surcharge) permettra d'initialiser les objets de plusieurs façons.
- Les méthodes qui ne modifient pas les attributs peuvent être déclarées const.
- Typiquement, les méthodes qui ne modifient pas les attributs sont des méthodes de requête (pour consulter la valeur d'un attribut) ou des méthodes qui fournissent de l'information sur l'objet (ex. : le déterminant d'une matrice).
 

Exemple :

class Heure
{
...
   int reqSec() const
    {
        return aSec;
    };
...
};
 

int main()
{
    const Heure lMidi(12, 0, 0); // Appel d'un constructeur surchargé.

    int lSec = lMidi.reqSec(); // Ok, c'est une méthode const

    lMidi.asgnHeure(13,0,0);  // ERREUR! l'objet lMidi est const.
    ...
}

3. Notion de contrat

- En orienté-objets, les objets se "parlent" entre eux.
- Certains objets demandent à d'autres d'exécuter des tâches.
- Si l'autre ne fait pas bien sa partie du contrat, il y a un problème!
- La notion de contrat est là pour valider 2 choses:
    - que le demandeur demande quelque chose de valide;
    - que l'exécuteur réponde par quelque chose de valide.

- On utilise les notions de pré/post conditions, assertion et invariants pour renforcer la notion de contrat (ce sont des vérifications sur des situations qui ne doivent JAMAIS survenir).
- Une préconditionvalide les paramètres passés à une fonction ou une méthode.
- Une postconditionvalide la sortie ou l'état de la classe à la sortie d'une méthode.
- Une assertionvérifie qu'une situation spécifique (genre division par 0) ne devrait jamais se produire là où l'assertion est faite.
- Un invariantfait partie d'une classe.  On définit les invariants comme les états valides qu'une classe peut prendre.
- Erreur de programmation VS erreur lors de l'exécution: quel comportement adopter?
- Généralement, les pré/postconditions, invariants et assertionssont considérés comme des erreurs de programmation s'ils ne sont pas vérifiés correctement.  Le programme devrait alors s'arrêter complètement et l'erreur devrait être corrigée dans le programme.
- Il y a des zones grises entre les erreurs de programmation et les erreurs à l'exécution.

4. Objets dans des objets

- Un objet réveille-matin pourrait être composé de 2 objets Heure:
    - Un objet Heure pour l'heure actuelle.
    - Un autre pour l'heure de réveil.

Exemple :

#include "Heure.h"

class ReveilleMatin
{
public:
    ReveilleMatin();

    void asgnHeureActuelle(const Heure& pHeure);
    void asgnHeureReveil(const Heure& pHeure);
    ...

private:
    Heure aActuelle, aReveil;
};
 

5. Friend: Fonctions et classes

- Une classe peut déclarer des fonctions ou des classes friend, ce qui permet à la fonction ou classe déclarée friend d'accéder aux attributs et méthodes protégés et privés de la classe.
- L'amitié ça se donne, ça ne se prend pas!
- Utile dans certains cas (opérateur <<)
- À éviter autant que possible, car on donne accès au fonctionnement interne, donc on brise l'encapsulation (protection) des données.

6. Pointeur this

- Tous les objets ont accès à leur propre adresse avec le pointeur this.
- Le compilateur passe implicitement le pointeur this à chaque méthode comme premier paramètre.
- Cela veut donc dire que même les méthodes sans aucun paramètre déclaré ont au moins un paramètre, soit le this, le pointeur à l'objet.

Exemple :

class Test
{
public:
    Test (int pInt);
    void affiche() const;

private:
    int aX;
};

Test::Test( int pX) // Équivalent à: Test(Test const * this, int pX)
{
    aX = pX;
}

void Test::affiche() const  // Équivalent à: void affiche(const Test const * this)
{
    cout << " aX = " << aX << endl
         << " this->aX = " << this->aX << endl
         << " (*this).aX = " << (*this).aX << endl;
}
 

7. Allocation dynamique: new & delete

- L'allocation dynamique permet d'allouer de la mémoire sans connaître d'avance la dimension nécessaire.
- L'allocation dynamique est <<l'opposée>> de l'allocation statique.
- En C, on utilisait les malloc et free.

Exemple :

    cin >> lDim;
    int *lVect = new int[lDim];
    ...
    delete [] lVect;
    ...
    Heure * lHeurePtr = new Heure;
    ...
    delete lHeurePtr;

- Le new appelle les constructeurs pour les objets (contrairement au malloc).
- Le new est une fonction qui retourne un pointeur vers le type demandé.
- Le delete appelle les destructeurs des objets (contrairement au free).
- Il existe 2 types de new et delete: avec et sans crochet.
- Il est très important d'appeler le delete[] sur un new [], car c'est de cette façon que le destructeur sera appelé sur chacun des objets.
- Le new retourne 0 en cas d'erreur.
- On pourra allouer une matrice grâce au new de 2 façons:
    1- en faisant un seul new pour le nombre total d'entrées dans la matrice (ex. : int * lMat = new int[lNbLignes*lNbColonnes];);
    2- en faisant un new pour le nombre de ligne et ensuite un new du nombre de colonnes pour chaque ligne:

int main()
{
    // On crée une matrice 4x4

   int ** lMat = new int*[4];
   lMat[0] = new int[4];
   lMat[1] = new int[4];
   lMat[2] = new int[4];
   lMat[3] = new int[4];
}
 

8. Interface /cacherl'implémentation

- On doit se considérer comme l'utilisateur d'une classe et non le concepteur
- Comme pour l'utilisation de votre voiture: vous tournez la clef, sans vous demander comment le moteur fait pour fonctionner... Dans la vie de tous les jours, il y a des choses pour lesquelles on ne connaît rien, d'autres plus...