HERITAGE - DERIVATION


 

Un des apports de la programmation Objet est le concept d'inclusion de type.

La possibilité de définir des inclusions de type a une grande incidence sur la stratégie de programmation, de conception et d'analyse.

 

 

Grâce à ce mécanisme nous allons écrire une seule fois le concept associé à la classe A et l'utiliser dans les classes B et les classes C et récursivement dans toutes les classes inclues.

- quelle est l'importance de la relation type / sous-type ?
- quels sont les problèmes de programmation qui sont aisément traités par cette syntaxe / ce mécanisme ?
- Comment appliquer les idées de la Poo et le mécanisme d'héritage ou C++ ?

 

Un exemple de dérivation

Le mécanisme de dérivation du C++ permet de fabriquer une classe dite " dérivée " à partir d'une classe "de Base " .

La classe " dérivée " hérite des potentialités de la classe de base, tout en lui en ajoutant de nouvelles

- sans changer la classe de base
- sans avoir la source de l'implémentation de la classe de Base (le fichier ".h" c'est à dire, l'interface suffit).

Un exemple classique de dérivation nous vient de la librairie X qui utilise (peut-être abusivement) ce mécanisme pour simplifier la construction et la compréhension de l'ensemble des fonctionnalités de la librairie.

 
  - position  
core - affichable - Classe "Racine"
  - taille  
composite  - container  
Shell - interaction avec le Window Manadger
 
 

Pas d'héritage, répété dans les librairies X.

Les librairies de la famille X sont écrites en C, il n'est donc pas possible de faire de l'héritage répété.

Il faut donc utiliser une " Dérivation " ad hoc par exemple :

class mon nouveau Super Shell {
private :
Mon super Shell Base ;
} ;
Maintenant je veux utiliser ma nouvelle class comme l'ancienne...

Pour utiliser la nouvelle classe.

Mon nouveau super Shell MNSS ;

soit MNSS. Base. Clear () ; // si mon nouveau... //est une classe friend//de mon Super Shell // soit réécrire clear () inline Mon nouveau Super Shell :: clear () {Base-Clear () ;} et utiliser directement MNSS. Clear () ; mais maintenant, MNSS. Base. Clear () et MNSS. Clear () peuvent appeler des fonctions différentes...

 

P.O.O. & commercial & Héritage

L'objectif de la dérivation est de pouvoir utiliser les membres d'une autre classe comme si il appartenait à la classe.

En C++ la dérivation permet exactement cette approche. La dérivation d'une classe X à partir d'une classe Y, nous donne des instances de X contenant tous les membres de la class Y.

La portée de ces différents membres hérités, dépend de la portée définie sur le membre et du mode de dérivation.

 

Syntaxe de Dérivation

 
Class dérivée : Public Base {
                       // ajout de la class dérivée
};
: opérateur de dérivation
public, private modes de dérivation protected
 
Pour un héritage multiple :
 
Class dérivée : Public Base 1
  private Base 2
  public Base 3
  {
  // corps de la class dérivée
};  
 
La dérivation répétée est possible :
           class spécialisée : public dérivé {// corp spécial
           } ;

 

Coércition standard & Dérivation

Pour que le principe de sous-typage/inclusion de type fonctionne, il nous faut un mécanisme qui permet d'accepter un sous-type à la place du type (ex : un shell à la place d'un core).

Le C a un mécanisme de transformation de type la coercition. L'inclusion de type en C++ est réalisée par des cast standard : un type de class dérivé est en standard transformable en un type de ses classes de Base.

Ex : Base - 1* p1 ;
Base - 2 * p2 ;
Dérivée d,* p,
p1 = & d ; // ok conversion
p2 = & d ; // ok standard ?
p = p1  ; // non une instance de Base 1 n'est pas toujours une instance de Dérivée
Ceci permet d'avoir des fonctions polymorphes :
           extern dumpImage (core *c) ;
qui peuvent fonctionner sur des instances de toutes les classes dérivées de core.

Attention : les conversions sont réelles au sens du C ; l'opération de conversion de Dérivée en Base -1 ou en Base -2 n'est pas la même !

 

Des fonctions génériques & des membres virtuels

Si l'on cherche à écrire une fonction générique dans laquelle on veut une réaction différente en fonction du type effectif du paramètre, il nous faut en C écrire quelque chose comme ça :
void f _ générique (core * truc)
{
   switch (truc ® class - id)
   {case SHELL : (( SHELL*) truc) ®doit () ; break  ;
   case Button : ((Button *) truc) doit ®() ; break  ;
    ...
   }
}
Les défauts de ce code sont nombreux, le plus grave est qu'il faut l'éditer, le compiler, le tester pour chaque nouvelle classe qui hérite de core !  Avec les fonctions virtuelles et l'édition de liens dynamiques, la fonction s'écrit :
Void f _ générique (core * truc)
{ truc ® doit ( ) ;}
Mieux, non ?

 

Mécanisme de liaison dynamique

En C++ c'est le concepteur de la classe de Base qui décide quelles fonctions membres seront dynamiquement liées. Ce sont les fonctions membres virtuelles.
 
Class core { ---
  public
--- virtual void doit () ;
} ;  
 
Une fonction membre ne sera déclarée virtual, que si les conditions de conception suivantes sont vérifiées :

- la classe va être l'objet de dérivation
- l'implémentation de la fonction varie suivant le type effectif de l'instance.

En X le contenu (Image) dépend de la classe de l'objet d'où une virtual.

La relation entre les types s'appelle la hiérarchie d'héritage.

Le passage d'une technique à une méthodologie a eu lieu avec small Talk, où non seulement c'est une méthode, mais aussi une modélisation du monde qui aide dans les phases d'analyse et de conception.

 

Un exemple d'héritage multiple

Le premier objectif dans une hiérarchie de typage est de modéliser correctement l'univers de notre projet .

L'héritage doit permettre non seulement de minimiser la quantité de code mais surtout de simplifier la compréhension et l'utilisation de nouveaux types.

Un Zoo

 

 

 

Spécification d'Héritage

Une fois notre graphe d'héritage " stabilisé " c.a.d. les différents concepteurs sont d'accord.

Nous pouvons écrire le squelette d'implémentation suivant :

- la classe de Base Principale :
            class AnimalDeZoo {...} ;
Nous ne manipulerons que des instances de sous-classes de Animal de Zoo.
- les autres classes de Bases :
            class enVoied'Extinction {...} ;
            class Herbivore {...} ;
            class Carnivore {...} ;
            ---
Ces classes apportent des possibilités qui ne sont pas communes à tous les animaux du zoo.

- les classes Dérivées, classes opératoires :

 
class ours :  public animaldezoo {...}
class félins :  public AnimaldeZoo,
  public carnivore {...} ;
class Panda : public ours,
  private herbivore,
  private envoied'extinction {...}
 
Les classes dont nous allons effectivement manipuler des instances.

 

Spécification d'héritage (suite)

Nous avons ici l'occasion d'utiliser le mot clef protected qui va nous permettre d'avoir des attributs privés utilisable dans une classe dérivée.
D'autre part, le mot clef virtual devient important, surtout que c'est le concepteur de la classe de Base qui fait le choix virtual.
 
Class AnimalDeZoo {  
  public : AnimalDeZoo
  Virtual a AnimalDeZoo (char*,char*, short);
  Virtual void dessine ();
  Void informations () ;
  Short localisation () ;
  protected
  char*name ;
  char*infos ;
  short location ;
  short nombre ;
} ;  
class ours :  public AnimalDeZoo {
  public : ours (char*,char*,short*,char,char*) ;
                void locate (int)
                int Est affiché () ;
  protected : char nourrit, dangereux, affiché, date ;
};  
 
Par défaut la classe de Base est private.

 

Accès aux membres hérités

Le choix du mode de dérivation influence la portée des membres hérités :

Dérivation public :
La portée des membres hérités ne change pas.

Dérivation protected :
Les membres publics de la classe de Base deviennent protected dans la classe dérivée ; (les autres ne changent pas de portée).

Dérivation private :Tous les membres de la classe de Base sont privés dans la classe dérivée.

 

Attention : une fonction de même nom dans la classe dérivée cache la fonction de la classe de Base, même si elles n'ont pas la même signature ce n'est pas considéré comme de la surcharge.

Dans le cas où l'on veut cacher le membre hérité, mais l'appeler dans le membre dérivé, on utilisera l'opérateur :: de révolution de portée dans le membre dérivé.

 

Définition double dans les classes de Base

(Héritage multiple)
 
Class herbivore { public : void fluo () ; }
Class en voie d'extinction {public : void fluo () ; }
Class panda :  public ours,
  public en voie d'extinction
  public herbivore {
  public : void fluo () ;
  public : void truc () 
};  
void panda :: fluo ()  
{  
herbivore :: fluo ()  
en voie d'extinction:: fluo () ;  
}  
void panda :: truc ()  
{  
fluo () ; // erreur ambiguité:  
}  
panda :: fluo ()  
 
cache les fonctions fluo des sous-classes, ainsi les classes dérivées de panda n'ont pas à choisir.

 

Initialisation des membres de la classe de Base

 
Panda ::  (char * nomi, short loc, char sex) :
  en voie d'extinction ("Chine"),
  Herbivore (Bambou),
  Ours ("BlaBlaBla")
  "panda",
  o,
  Panda, Miocene
  {name = new char [strlen (nomi)+ i ] ;
  strepy (name, nomi) ;
  tell = loi ; // membre de Animaldezoo
  ---
  }
 
il n'est pas possible d'écrire
            Panda :: panda () : animal de Zoo (---)
si les membres de Animal de zoo sont private et que le constructeur ours :: ours () ne peut transmettre les valeurs d'initialisation, nous ne pouvons pas initialiser les membres de Animal de zoo dans Panda !

 

Ordre d'appel des constructeurs

1. le constructeur de chaque classe de Base dans l'ordre de dérivation.

             AnimaldeZoo, Ours, En voie d'Extinction, Herbivore.

2. chaque membre de type class dans l'ordre de déclaration.

3. le corp du constructeur de la classe.

 


Fonctions virtuelles & Edition de lien dynamique

Le C++ permet d'écrire des expressions dont l'évolution dépend du type des instances de classe qui la compose. Plus exactement le choix de l'implémentation d'une fonction membre dépend du type de l'instance mais pas du type du pointeur sur l'instance.
Base * p ;
p = new dérivée ; // dérivé sous class de Base
p ® non virtual (); // Base :: non virtual () ;
p ® virtuelle () ; // dérivée :: virtuelle () ;
 

L'édition de lieu dynamique est une forme d'encapsulation

L'édition de lieu dynamique permet d'encapsuler du code, en effet cela permet d'écrire du code définitivement pour un ensemble de types (plus exactement pour un type et ses sous types).

L'intérêt de cette encapsulation est énorme car la gestion manuelle ce de polymorphisme est toujours délicate surtout quand elle se conjugue avec des difficultés de conception ou de maintenance.

Alors pourquoi ne pas tout définir en virtual ?

- Pour une raison d'efficacité, les fonctions non virtuelles peuvent être "inlinées", ce qui est faux pour les virtual ! la différence de vitesse d'exécution est très importante. (Par contre la ¹ de vitesse entre édition de liens statiques et dynamiques est peu importante.)

 

Définition d'une fonction virtuelle

 
Class Base {---
  public :
  virtual void Boom () {cout << "Boom" ;} // virtual
  virtual void k Boom (int i) = 0 // pure virtual
---  
};  
 
Cette classe Base est une classe ABSTRAITE car nous avons déclarer une fonction membre pure virtual, la classe Base ne peut être instanciée. Toute classe dérivée qui ne définit pas la fonction void Kboom (int) sera aussi abstraite.
 
Class Dérivée : public Base {
  public :
  void Boom () {cout <<"Boom dérivée";}//.virtual
  void Boom (int) {x---}//non virtuelle, surcharge locale
  //Int Boom () {---} // n'est pas la fonction virtuelle
  Void k Boom (int i) {cout << "hi=" << i << ende ;
                      // obligatoire pour instancier
                      // dérivée
---  
};  
 
Z la signature et la valeur de retour d'une fonction virtuelle doit être exactement les mêmes dans toutes les classes.

 

Destructeurs virtuels

Si l'on manipule des collections d'un type ayant des sous types, il faut déclarer le destructeur du type racine virtuelle pour que le destructeur du container appel sur chaque instance le bon destructeur.

Les non ¹ pour chaque destructeur ne posent pas de problème.

 

Un appel non virtuel d'une fonction virtuelle

 
Base* p = new dérivée ;  
  p®boom () ;  // virtuel Dérivée :: Boom ()
  p® Base :: Boom ();  // non virtuel
    // mais si Boom fait un appel a une virtuelle
    (ex : kBoom (int))
    l'appel sera virtuel
 

L'héritage Virtuel & le double héritage

class véhicule {---} ;
class Avion : public véhicule {---} ;
class Bateau : public véhicule {---} ;
Que fait on avec :
            class hydravion = ???
c'est un avion et un bateau d'où :
 
class hydravion : public Avion,
  public Bateau {---}
 
 

Le graphe d'héritage :

 

 

 

Cette interprètation avec double héritage des membres de véhicule peut être la bonne si l'on considère que véhicule contient un accord des mines et que l'accord des mines pour un hydravion contient un accord pour un avion et un accord pour un bateau, cet accord étant stocké dans véhicule.

De même à une instance d'hydravion on demandera l'accord des mines soit pour un avion, soit pour un bateau.

 
Hydravion  H ;
  // H. accord () ; ambiguë d'où l'écriture de :
void hydravion :: accord ()
  {
  Avion :: accord () ;
  Bateau :: accord () ;
  }
 
Trois appels possibles :
H. accord () ;
H. avion :: accord () ;
H. bateau :: accord () ;
Attention l'appel suivant reste ambiguë.
H. véhicule :: accord () ;

 

La structure interne des instances :

 

 

Cette duplication est coûteuse et parfois inutile, il nous faut une syntaxe permettant la nom duplication. C'est le rôle de l'héritage virtuel.

 

Structure interne avec de l'héritage virtuel

 

 

Problèmes :

Initialiser la partie virtuelle.

Dans le cas d'une dérivation virtuelle, il est possible d'appeler le constructeur de la classe virtuelle.
            Hydravion :: hydravion () : véhicule (3), avion (), bateau () {}
Une classe virtuelle est initialisée par la classe la plus dérivée.

Que se passe-t-il avec une fonction membre de la classe véhicule redéfinie dans avion ?

Héritage privé et virtuel ?

 

Double héritage

Les règles d'héritage restent valable dans le cas d'un héritage virtuel.
Class avion : private virtual véhicule {---} ;
Class bateau : public virtual véhicule {---} ;
Class hydravion : public avion, public bateau {} H ;

H. accord () ; // non ambiguë seul l'héritage par
                     // bateau est visible.

 

Domination

Prédominance

 

 

L'instance la plus dérivée du membre domine la chaine d'héritage.

AX370. Localization () : // Avion :: localization ()
 

Ordre d'appel des constructeurs & destructeurs

 
Class personnage {---} ;  
class personnage de livre: public personnage
{---} ;  
class ours en peluche :  PublicPersonnageDeLivre,
  public ours,
  public virtual jouet {--} ;
 
1) Les virtuelles immédiates : ici jouet ()

2) Les virtuelles des classes de Base dans l'ordre d'héritage des classesdeBase:

           AnimalDeZoo () // dans ours.

3) Les classes de Base dans l'ordre d'héritage :

            Personnage () ; // class de Base PersonnageDeLivre
            PersonnageDeLivre () ; // ? class de Base
            Ours () ;
            Ours en peluche () ;

 

Classes template & héritage

Une classe template instanciée pour l'héritage :
class pile : private tableau < int > { } ,
Une classe template définie à partir d'une classe quelconque.
template < class types >
class dérivée : public Base ;
{
type vol ;
} ;
Extension d'un template :
template < class type >
class TableauBorné : public virtual Tableau < type > {
};
// le type
// doit être précisé
// sinon erreur