1. Surcharge des opérateurs
2. Opérateur comme méthode ou fonction friend
3. Héritage
4. Constructeur / destructeur & héritage
5. Conversion implicite des enfants en parents
6. Composition VS héritage
7. Héritage multiple
0. Retour sur les exercices
- Il est possible de définir le comportement des opérateurs +, -, =, <, etc. pour les types créés par le programmeur, cela s'appelle la surcharge des opérateurs.
- On peut dire que la surcharge est déjà utilisée, car pour le processeur faire une addition (+) entre 2 entiers n'est pas la même chose que de la faire entre 2 nombres à virgule flottante...
- Pour utiliser un opérateur sur une classe, il DOIT être
surchargé sauf pour 2 exceptions : pour l'opérateur d'assignation
(=) et pour l'opérateur pour prendre l'adresse (&).
- On peut surcharger les opérateurs suivants: '+', '-', '*',
'/', '%', '^', '&', '|', '~', '!', '=', '<', '>', '+=', '-=', '*=',
'/=', '%=', '^=', '&=', '|=', '<<', '>>', '>>=', '<<=',
'==', '!=', '<=', '>=', '&&', '||', '++', '--', '->*', ',',
'->', '[]', '()', 'new', 'delete', 'new[]', 'delete[]'
- On ne peut surcharger les opérateurs suivants: '.', '.*',
'::', '?:', "sizeof".
- Avec une classe créée par l'utilisateur, comme la classe
Complexe,
il serait intéressant de pouvoir écrire: lCplx1 = lCplx1
+ lCpl2;
Exemple :
class Complexe {
public:
...
void additionneComplexe(const Complexe&
pComplexe);
Complexe & operator += (const Complexe&
pComplexe);
...
};
2. Opérateur comme méthode ou fonction friend
- Il y a 3 catégories d'opérateurs: opérateurs
unaires, binaires et ternaires.
- Les opérateurs unaires ne s'appliquent que sur une variable
(ex. : moins unaire(-))
- Les opérateurs binaires s'appliquent sur deux variables: un
membre de gauche et un membre de droite (ex. : a + b, a est le membre de
gauche, b le membre de droite et + l'opérateur).
- Il y a un opérateur ternaire, soit le ?: qui représente
un if (ex. : (a == true) ? cout <<"Vrai" : cout << "Faux";).
- Il faut bien comprendre l'importance du membre de gauche pour la
surcharge des opérateurs.
- Pour l'opérateur + de la classe Complexe, si on définit
l'opération d'addition avec un double, elle ne sera pas commutative
automatiquement. Pour qu'elle le soit, il faudra faire une surcharge
supplémentaire ( "Complexe + 4" est une opération différente
de "4 + Complexe" !!!).
- Les opérateurs d'insertion et d'extraction (<<, >>)
ne peuvent être définis comme méthodes de la classe,
car le membre de gauche de ces opérateurs (pour l'écriture/lecture)
attend un ostream/istream.
Exemple :
class Complexe
{
public:
...
friend ostream& operator << (ostream&
pOs, const Complexe& pComplexe);
...
};
- Il faudra faire un retour par valeur pour certains opérateurs (+, -, /, *), souvent ceux qui sont const.
Exemple :
Complexe Complexe::operator+ (const Complexe& pComplexe) const
{
Complexe lReponse;
lReponse.aPartieReelle = aPartieReelle + pComplexe.aPartieReelle;
lReponse.aPartieImaginaire= aPartieImaginaire
+ pComplexe.aPartieImaginaire;
return lReponse;
}
- Attention à l'allocation dynamique (Vecteur) et à l'opérateur d'assignation (=):
class VectDyn
{
public:
VectDyn();
VectDyn(int pDim);
~VectDyn();
VectDyn& operator=(const VectDyn& pVectDyn);
private:
int aDim;
double *aVecteur;
};
VectDyn::VectDyn()
: aDim(0), aVecteur0)
{
}
VectDyn::VectDyn(int pDim)
{
assert(pDim > 0 && "Dimension inférieure
ou égale à 0");
aDim = pDim;
aVecteur = new double[aDim];
}
VectDyn::~VectDyn()
{
delete [] aVecteur;
aVecteur = 0;
aDim = 0;
}
VectDyn& VectDyn::operator=(const VectDyn& pVectDyn)
{
// Si la dimension du vecteur pVectDyn n'est
pas égale à la nôtre,
// alors on détruit l'ancien bloc de
mémoire et on en alloue un nouveau.
if (pVectDyn.aDim != aDim) {
delete [] aVecteur;
aDim = pVectDyn.aDim;
aVecteur = 0;
if (0 != aDim) {
aVecteur = new double[aDim];
}
}
for (int i = 0 ; i<aDim; ++i) {
aVecteur[i] = pVectDyn.aVecteur[i];
}
return *this;
}
- On parle de classe de base ou parent, ou grand-père, ou classe
de tête.
- On parle de classe dérivée ou enfant, ou héritier,
ou classe spécialisée, ou feuille, ou dérivée.
- L'enfant hérite des attributs et méthodes du parent.
- Il existe 3 sortes d'héritage: public, protected et private
- Hiérarchie d'éléments:
Exemple :
class Element {
public:
Element();
Element(int pNo);
void asgnNo(int pNo);
int reqNo() const;
void exporte(ostream& pSortie);
private:
int aNoElement;
};
class Element1D : public Element {
public:
Element1D();
Element1D(int pNoNoeud0, int pNoNoeud1);
Element1D(int pNoElement, int pNoNoeud0, int
pNoNoeud1);
void asgnNoNoeud(int pNoNoeud0, int pNoNoeud1);
void reqNoNoeud(int& pNoNoeud0, int&
pNoNoeud1);
void exporte(ostream& pSortie);
private:
int aNoNoeud0, aNoNoeud1;
};
int main()
{
Element1D lElem1D;
lElem1D.asgnNo(10); // Appel la méthode
Element::asgnNo(...) sur l'objet lElem1D
}
- On peut définir la méthode exporte(...) dans le parent et la redéfinir dans l'enfant comme étant:
void Element::exporte(ostream& pSortie)
{
pSortie << pNo;
}
void Element1D::exporte(ostream& pSortie)
{
Element::exporte(pSortie);
cout << " " << aNoNoeud0 <<
" " << aNoNoeud1;
}
4. Constructeur / destructeur& héritage
- Une classe dérivée hérite des attributs de la
classe de base, donc l'appel du constructeur de la classe de base est fait
pour que les attributs soient initialisés.
- La première chose qui est faite dans un constructeur est l'appel
du constructeur de la classe parent. S'il y a plusieurs niveaux d'héritage,
l'appel du constructeur de la classe parent sera fait jusqu'à ce
que l'on atteigne la tête de la hiérarchie.
- Les constructeurs et opérateurs d'assignation de la classe
de base ne sont pas hérités.
- On peut faire l'appel explicite des constructeurs dans les constructeurs
de la classe dérivée.
Exemple :
Element1D::Element1D(int pNoElement, int pNoNoeud0, int pNoNoeud1)
: Element(pNoElement), aNoNoeud0(pNoNoeud0), aNoNoeud1(pNoNoeud1)
{
assert (aNoNoeud0 >= 0 && pNoNoeud1 >= 0);
}
- Il est intéressant de suivre la trace de l'exécution avec cout ou ddd.
5. Conversion implicite des enfants en parents
- Même si l'enfant "est un" parent, ce sont 2 types différents.
- Les objets enfants peuvent être traités comme des objets
parents. C'est logique puisque l'enfant a toutes les propriétés
du parent (attributs et méthodes).
- Par contre, l'inverse n'est pas vrai. C'est-à-dire que
l'on ne peut traiter un parent comme un enfant, car il n'en possède
pas nécessairement tous les attributs et méthodes.
Exemple :
int main() {
Element1D lElem1D;
Element lElem(lElem1D); // Valide car Elem1D EST un Element...
Element1D lElem1DInvalide(lElem); // Invalide!
}
- Un pointeur ou référence vers la classe de base peut pointer vers n'importe quel héritier.
Exemple :
int main() {
Element1D lElem1D;
Element lElem;
Element *lPtrElem = &lElem1D;
lPtrElem = &lElem;
}
Autre exemple :
void fctAsgnZero(Element& pElement)
{
pElement.asgnNo(0);
}
int main()
{
Element1D lElem1D;
fctAsgnZero(lElem1D);
}
- Un employé "a une" date de naissance ( et non "est une").
- Un employé "a une" #de téléphone (et non "est
un").
- Un réveille-matin "a une" heure de réveil et "a une"
heure actuelle.
- Un élément 1D "est un" élément ou "a
un" élément?
- Un nouveau programmeur fera souvent par enthousiasme des conceptions
avec plusieurs héritages, car il voit ce concept comme une solution
à tous les problèmes. Il y a souvent d'autres solutions
beaucoup plus simples...
- On peut hériter de plusieurs classes à la fois.
Assez peu utilisé, mais parfois pratique.