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();
}
------------------------------
- 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.
...
}
- 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.
- 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.
- 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...