Cours C++ #3
Théorie
Contenu:
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

1. Surcharge des opérateurs

- 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;
}

3. Héritage

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

6. Composition VS héritage

- 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...

7. Héritage multiple

- On peut hériter de plusieurs classes à la fois.  Assez peu utilisé, mais parfois pratique.