Introduction à la programmation orientée objet (en C++)
This study has not yet been peer reviewed.
Notes de lecture - cours EPFL par Chappelier et al.
Introduction
POO permet le regroupement des traitements et des données en une seule et même entité; en C++, donne davantage de robustesse, de modularité et de lisibilité; repose sur 4 concepts
- encapsulation
- abstraction
- héritage
- polymorphisme
encapsulation = regrouper dans une seule et même entité des données (les attributs) et des traitements (les méthodes) qui agissent sur celles-ci; le tout est défini au sein d’une classe, qui constitue un nouveau type de données, une catégorie d’objets; une variable de ce type est désignée par le terme d’instance
abstraction = identifie des caractéristiques et des mécanismes communs aux entités utilisées, permettant une description générique, abstraite de l’ensemble
encapsulation et abstraction consistent également à dissimuler les détails de l’implémentation à l’utilisateur, qui n’exploite plus que l’interface d’utilisation de l’objet (i.e. les diverses fonctionnalités qui lui sont accessibles pour sa manipulation); 2 niveaux de perception de l’objet: niveau externe visible du programmeur utilisateur et niveau interne du programmeur qui se charge des détails de l’implémentation
le programmeur concepteur définit différents niveaux de visibilité; l’accès limité à l’utilisateur protège le programme des erreurs
Classe, objets, attributs et méthodes
une classe se déclare avec le mot-clé class
, qui définit un nouveau type; une fois défini, ce dernier peut être utilisé pour déclarer des variables, instances de cette classe
au sein de la classe, la déclaration des attributs est similaire à celle des champs d’une structures
les méthodes sont des fonctions qui sont déclarées au sein de la classe
le prototype d’une méthode indique le type de retour, le nom de la méthode et ses paramètres (entre parenthèses); l’entête est complété par son corps (i.e. sa définition) entre accolades
portée de classe = les attributs sont des variables globales à la classe; les paramètres d’une méthode sont des variables externes à la classe: chaque méthode d’une classe a accès aux attributs de celle-ci; ils ne doivent donc pas être passés en arguments
pour rendre le code plus lisible et favoriser la modularisation, on peut choisir de définir les méthodes à l’extérieur de la classe; le prototype de la méthode devra cependant rester à l’intérieur de la classe; en dehors de la classe, pour indiquer au compilateur que l’on définit une méthode et non pas une fonction; il faut souligner la classe à laquelle elle appartient grâce à l’opérateur de résolution de portée ::
à distinguer:
- actions = méthodes qui modifient les attributs de l’instance manipuée
- prédicats = celles qui ne le font pas; il est bien d’indiquer qu’une méthode est un prédicat avec le mot
const
après la liste des paramètres pour indiquer qu’elle ne modifie pas l’instance à laquelle elle s’applique
public: et private:
le mot-clé private
est employé pour signaler quelle partie de la classe restera inaccessible à un utilisateur; tout élément de la classe qui suit private:
(attributs, méthodes internes) est inaccessible depuis l’extérieur de la classe; ne pourront être appelés qu’au sein de la classe, et pas depuis le main
par exemple; par défaut (sans spécification, tout élément d’une classe est private
)
le mot-clé public
indique quels membres d’une classe sont accessibles, visibles et utilisables
en général, on définit les attributs comme private
et, si besoin, on propose des méthodes publiques pour les manipuler
- accesseurs (getters) = méthodes qui retournent la valeur de l’attribut; ce sont des prédicats (
const
suit leur entête) - manipulateurs (setters) = permettent de modifier la valeur des attributs; sont des actions (prend en paramètre la valeur à affecter et ne retourne rien)
garder les attributs privés permet de limiter les erreurs de l’utilisateur; l’interface permet au concepteur de librement modifier sa représentation de la classe sans que l’utilisateur ne soit affecté
masquage: lorsqu’un identificateur en cache un autre; e.g. quand une variable locale prend le pas sur une variable plus globale
avec les manipulateurs notamment, risque d’avoir le même nom que l’attribut à modifier comme nom de paramètre (le même nom désigne le paramètre et l’attribut); pour éviter un message d’erreur du compilateur, on peut
- utiliser un autre nom pour le paramètre (recommandé)
- désambiguïser le nom en indiquant quand il désigne l’attribut de classe; utiliser le pointeur
this
sur l’instance courante; en tant que pointeur il faut utiliserthis->attribut
(recommandé) équivalent à(*this).attribut
il est cependant recommandé d’éviter les situations de masquage
exemple
class MaClasse
{
private:
int x ;
int y;
public:
void une_methode(int x)
{
... y...
... x ...
this -> = x;
}
}
Encapsulation et abstraction: résumé
il est conseillé de marquer les prédicats (= méthodes ne modifiant pas les attributs, avec le mot-clé const
)
une bonne encapsulation (séparation entre détails privés et interface publique) permet au programmeur concepteur de modifier plus librement son implémentation sans impact pour l’utilisateur
l’écriture d’une classe est rendue plus lisible quand ses méthodes sont uniquement prototypées à l’intérieur de celle-ci; la définition reste à l’extérieur, rattachée à la classe par l’opérateur de résolution de portée ::
prototype de classe = les éléments qui sont écrits au sein de celle-ci, i.e. le prototype de ses méthodes et de ses attributs
définition de la classe = les définitions des méthodes qui la constituent
exemple
class Rectangle
{
public:
//prototype des méthodes
double surface() const;
//accesseurs
double hauteur() const;
double largeur() const;
//manipulateurs
void hauteur(double);
void largeur(double);
private:
double hauteur_;
double largeur_;
}; // !!! ne pas oublier le ";" !!!
double Rectangle::surface() const
{
return hauteur_ * largeur_;
}
double Rectangle::hauteur() const
{
return hauteur_ ;
}
double Rectangle::largeur() const
{
return largeur_;
}
void Rectangle::hauteur(double h)
{
hauteur_ = h;
}
void Rectangle::largeur(double l)
{
largeur_ = l;
}
Constructeurs: introduction
on pourrait utiliser des manipulateurs mais cela serait contraire à l’encapsulation et obligerait l’utilisateur à initialiser explicitement tous les attributs
meilleure solution est d’utiliser le constructeur = une méthode dédiée à l’initialisation des attributs et qui prend en paramètres les valeurs que prendront les attributs; peut aussi servir à vérifier la validité des données
constructeur = méthode invoquée systématiquement lors de la création d’un objet et chargée d’effectuer les opérations d’initialisation de celui-ci, notamment de ses attributs
le constructeur a le même nom que la classe et n’a pas de type de retour; on peut faire des surcharges de constructeurs (sera appelé celui dont les paramètres correspondent aux arguments donnés) et donner des valeurs par défaut à leurs paramètres
pour appeler directement le constructeur d’un attribut-objet, mais aussi pour initialiser des attributs des types de base, on utilise la liste d’initialisation en dehors du corps du constructeur; usage recommandé pour éviter les copies superflues et par souci de lisibilité; pour les attributs de types de base, il suffit de spécifier la valeur entre parenthèses, après le nom de l’attribut, pour l’initialiser; cela évite également les problèmes de masquages: la liste d’initialisation distingue attributs et paramètres
exemple:
class Rectangle
{
public:
Rectangle(double h, double L)
{
double hauteur = h;
double largeur = L;
}
private:
double hauteur;
double largeur;
}; // !!! ne pas oublier le ";" !!!
classe RectangleColore
{
public:
RectangleColore(double h, double L, Couleur c): rectangle(h, L), couleur(c){}
private:
Rectangle rectangle;
Couleur couleur;
}
int main(){
Rectangle rect1(3.0,4.0);
}
les initialisations par cette liste peuvent ensuite être modifiées dans le corps du constructeur; un avantage de la liste d’initialisation est donc de donner une valeur initiale aux attributs avant même de rentrer dans le corps du constructeur, qui reste souvent vide
exemple:
Rectangle(double L, double h)
: hauteur(h) // seulement hauteur initialisée ici avant le corps de la fonction
{
// largeur indéfinie jusqu'ici
largeur = 2.0 * L + h;
// largeur est désormais définie
}
Constructeurs par défaut
le constructeur par défaut est appelé lors de la déclaration d’un objet sans valeurs d’initialisation; constructeur qui ne prend pas de paramètre ou dont tous les paramètres ont une valeur par défaut, e.g.
// constructeur par défaut
Rectangle(): hauteur(1.0), largeur(2.0){}
// 2ème constructeur
Rectangle(double h): hauteur(h), largeur(2.0*h){}
// 3ème constructeur
Rectangle(double h, double l): hauteur(h), largeur(l){}
l’appel au constructeur par défaut se fait avec la syntaxe:
NomClasse NomInstance;
attention: ne pas ajouter de parenthèses vides après le nom de l’instance !!! (autrement interprété comme le prototype d’une fonction)
constructeur par défaut par défaut = si aucun constructeur n’est explicité, le compilateur génère automatiquement une version minimale du constructeur par défaut; responsable d’appeler le constructeur par défaut des attributs lorsqu’il s’agit d’objets, et laisse les attributs de base (int
, double
, char
, bool
, …) non initialisés
dès qu’un constructeur, par défaut ou non, est spécifié, le constructeur par défaut par défaut n’est plus fourni; ainsi, si un constructeur est défini sans spécifier un constructeur par défaut, l’utilisateur se voit obligé d’initialiser explicitement les objets
depuis C++11 il est possible de réactiver le constructeur par défaut par défaut (choix surtout pertinent quand les attributs sont des objets); syntaxe:
NomClasse() = default;
remarques:
- réactiver une variante par défaut est généralisable à d’autres méthodes
- il est possible de supprimer une méthode avec
= delete;
après le prototype de la méthode à supprimer
depuis C++11 il est autorisé qu’un constructeur en appelle un autre en utilisant la liste d’initialisation et les arguments correspondants; plutôt que de réactiver le constructeur par défaut par défaut qui n’initialise pas les types de base, il est conseillé d’employer cette tournure
exemple:
class Rectangle
{
private:
double hauteur; double largeur;
public:
Rectangle(double h, double L): hauteur(h), Largeur(L){}
Rectangle(): Rectangle(0.0, 0.0) {} // mieux que Rectangle() = default; // constructeur par défaut qui appelle le 1er constructeur
// ...
}
il est possible de donner directement une valeur par défaut aux attributs (e.g. pour ci-dessus: double hauteur = 0.0; double largeur = 0.0;
); lorsqu’une instance de cette classe est initialisée, si le constructeur appelé ne modifie pas la valeur de cet attribut, ce dernier prend alors la valeur par défaut; il est toutefois conseillé d’affecter des valeurs par défaut aux attributs à l’intérieur des constructeurs plutôt que d’utiliser les valeurs par défaut
Constructeurs de copie
constructeur de copie permet de créer une copie d’une instance; s’utilise comme les autres et prend pour seul argument l’instance à copier
exemple:
NomClasse Instance1(liste_arguments);
NomClasse Instance2(Instance1);
après ces instructions, Instance1
et Instance2
sont 2 instances distinctes, mais leurs attributs ont les mêmes valeurs
le constructeur de copie est notamment utilisé lorsque l’argument d’une fonction passé par valeur est ainsi copié dans une variable locale
s’agissant d’un constructeur dont le paramètre est une instance de la même classe, le prototype du constructeur de copie est le suivant:
NomClasse(NomClasse const& autre) {...}
attention !!! l’usage de la référence (constante puisque l’instance copiée n’est pas modifiée) est important pour ne pas avoir de copie qui induirait un appel récursif (fonction s’appelant elle-même) de ce constructeur
exemple:
Rectangle(Rectangle const& autre): hauteur(autre.hauteur), largeur(autre.largeur){}
il n’est souvent pas nécessaire de définir explicitement le constructeur de copie: le compilateur fournit une version par défaut, souvent suffisante. Cet outil réalise alors une copie de surface, qui recopie la valeur des attributs; la redéfinition de ce constructeur est nécessaire dans certains cas particuliers (e.g. utilisation de pointeurs)
règle à mémoriser: si on redéfinit une des méthodes que sont le constructeur de copie, le destructeur et l’opérateur d’affectation, alors il faut s’interroger sur la redéfinition des deux autres.
lorsqu’une instance de classe occupe trop de mémoire pour être dupliquée, C++11 permet d’en empêcher la copie; pour cela, il faut placer =delete
à la fin du prototype du constructeur de copie
class PasCopiable {
/* ...*/
PasCopiable(PasCopiable const&) = delete;
}
Destructeurs
la gestion de la fin de vie devient nécessaire lorsque l’initialisation de ce dernier mobilise des ressources (e.g. périphériques, portions de mémoire, etc.)
destructeurs: méthodes dont la particularité est d’être invoquées automatiquement en fin de vie de l’instance; leur nom est le même que celuli de la classe, précédé de ~
; méthode sans paramètre, qui ne peut donc pas être surchargée; syntaxe:
~NomClasse () { //opération de libération}
si le destructeur n’est pas défini explicitement, le compilateur en génère automatiquement une version minimale par défaut dont le corps est vide (n’est pas toujours suffisante même sans allocation de ressources, e.g. si on maintient un compteur du nombre d’instances)
rappel de la règle à mémoriser: si on redéfinit une des méthodes que sont le constructeur de copie, le destructeur et l’opérateur d’affectation, alors il faut s’interroger sur la redéfinition des deux autres; à cette liste de méthodes liées, on peut ajouter le constructeur de déplacement et l’opérateur de déplacement
NB: sémantique de déplacement, voir http://olivierpisano.over-blog.com/article-influence-de-la-semantique-de-deplacement-dans-la-stl-de-c-11-58905115.html
Déplacement contre copie:
La nouvelle norme du langage C++ introduit une nouveauté de taille, les rvalue references, destinées à réduire le nombre de copies d’objets en privilégiant le déplacement d’un objet plutôt que sa copie lorsque cela est possible. Une rvalue reference est une référence vers un objet anonyme temporaire et s’écrit au moyen de &&
au lieu de &
. Exemple d’une forme canonique d’une classe T:
class T
{
int* pi;
public:
T() : pi(new int[100000]) { }
~T() { delete[] pi; }
// Sémantique de copie
T(const T& t) : pi(new int[100000]) { memcpy(pi, t.pi,
100000 * sizeof(int)); }
T& operator=(const T& t) {memcpy(pi, t.pi, 100000 * sizeof(int));}
// Sémantique de déplacement
T(T&& t) : pi(t.pi) {t.pi = nullptr; }
T& operator=(T&& t) {std::swap(pi, t.pi); return *this; }
};
! attention ! dans les méthodes à implémenter pour le déplacement, le paramètre n’est pas constant, puisque celui-ci est destiné à être invalidé par le déplacement.
Le constructeur de déplacement sera utilisé par le compilateur lorsqu’on affectera un temporaire anonyme, par exemple avec le code suivant :
T creerGrosObjet()
{
T obj1;
return obj1;
}
int main(int argc, char** argv)
{
T obj2 = creerGrosObjet(); // On ne copie pas, on déplace.
return 0;
}
Variables et méthodes de classe
un attribut de classe peut contenir une information commune à toutes les instances; sa déclaration est précédée du mot-clé static
; peut être private
ou public
pour les attributs d’instance, chaque instance d’une classe en a des valeurs spécifiques; mais pour l’attribut de classe, une zone mémoire unique est accessible pour toutes les instances; elle est accessible sans qu’aucune instance ne soit encore créée, au travers du nom de la classe et de l’opérateur de résolution de portée; syntaxe pour accéder à l’attribut de classe en dehors de celle-ci:
NomClasse::AttributClasse
un attribut de classe est généralement initialisé lors de la construction; un attribut de classe existe indépendamment de toute instance et ne peut donc pas être initialisé par un constructeur; pour l’initialiser il faut utiliser la syntaxe suivante en dehors de la classe
Type NomClasse::AttributClasse(Valeur);
les attributs de classe permettent d’éviter des duplications inutiles et des problèmes de maintenance; un attribut de classe stocke souvent une constante
méthode de classe = méthode définie à l’intérieur d’une classe dont la déclaration est précédée du mot-clé static
; peut être invoquée grâce à l’opérateur de résolution de portée sans qu’aucun objet de la classe n’existe (ce n’était pas le cas pour les méthodes d’instances, directement liées à une instance précise); une méthode de classe peut également être appelée au travers d’une instance (tout comme un attribut statique s’il est accessible)
exemple:
class A
{
public:
static void method1() { cout << "Meth 1" << endl;}
void method2() { cout << "Meth 2" << endl;}
};
int main()
{
A::method1(); // ok
A::method2(); // ERREUR !
A x;
x.method1(); // ok
x.method2(); // ok
}
une méthode de classe est indépendante de l’existence d’un objet courant sur lequel elle s’appliquerait et ne peut donc pas exploiter de méthode ou d’attribut d’instance dans sa définition, mais uniquement d’autres membres de la classe (“statiques”)
l’utilisation du static
est à limiter à des attributs de classe constants (si nécessaires): un membre statique brise l’approche orientée objet puisqu’il est indépendant de tout objet
Surcharge d’opérateurs: introduction
pour étendre l’utilisation des opérateurs usuels à des nouvelles classes
une expression qui contient un opérateur fait appel à une fonction et à une méthode, ainsi on peut écrire
a Op b
# soit la fonction
operatorOp(a,b)
# soit la méthode
a.operatorOp(b)
# pour les opérateurs unaires
operatorOp(a)
a.operatorOp()
à noter que l’opérateur operator=
correspond systématiquement à une méthode de classe et jamais à une fonction
surcharge = quand une fonction a le même nom qu’une autre dont elle se différencie par ses arguments
surcharge d’opérateur = étendre l’utilisation des opérateurs couramment employés à de nouvelles classes; le nom est conservé mais pas leurs arguments; peut se faire de 2 manières: 1) surcharge externe: utiliser une fonction qui prend les opérandes comme argument, prototypée et définie à l’extérieur de la classe 2) surcharge interne: méthode de la classe, dont l’instance courante est un opérateur
Surcharge d’opérateurs: surcharge externe
surcharge externe = définir les opérateurs comme des fonctions externes à la classe sur laquelle on travaille
pour optimiser l’appel on passe les arguments par référence constante (const&
) et on qualifie de const
le type de retour également; en C++11 une meilleure optimisation est encore possible
// base:
Complexe operator+(Complexe z1, Complexe z2);
// optimisation
const Complexe operator+(Complexe const& z1, Complexe const& z2);
// avancé et C++11
const Complexe operator+(Complexe z1, Complexe const& z2);
const Complexe operator+(Complexe const& z1, Complexe const&& z2);
il existe des situations, peu courantes, dans lesquelles la surcharge interne n’est pas possible; e.g. quand les opérandes sont de types différents ou pour surcharger un opérateur d’affichage
exemple:
ostream& operator<<(ostream&, Complexe const&);
il est important de passer le 1er argument par référence puisque l’affichage va modifier le flot en question; les flots ne peuvent d’ailleurs qu’être passés par référence
pour définir le corps de la fonction:
- utiliser des accesseurs
- (préférable) utiliser une méthode préalablement définie)
// via des accesseurs:
ostream& operator<<(ostream& sortie, Complexe const& z)
{
sortie << "(" << z.get_x() << ", " << z.get_y() << ")";
return sortie;
}
// via une autre méthode:
ostream& operator<<(ostream& sortie, Complexe const& z)
{
sortie << z.to_string(); // e.g. en utilisant to_string() qui convertit Complexe en chaine de caractères
return sortie;
}
// via des accesseurs:
ostream& operator<<(ostream& sortie, Complexe const& z)
{
sortie << "(" << z.get_x() << ", " << z.get_y() << ")";
return sortie;
}
lors de surcharges externes, il est parfois nécessaire d’accéder aux attributs privés; alors fortement recommandé de toujours utiliser l’interface publique de la classe, les accesseurs
mais certains préfèrent briser l’encapsulation en donnant un accès privilégié en faisant précéder le prototype de la fonction par le mot-clé friend
; cette friendship confère à la fonction le droit d’accéder aux éléments privés de la classe; peu conseillé
ostream& operator<<(ostream& sortie, Complexe const& z) // friend s'utilise dans la classe uniquement, pas dans la définition
{
sortie << "(" << z.x << ", " << z.y << ")";
return sortie;
}
class Complexe
{
friend ostream& operator<<(ostream& sortie, Complexe const& z); // utiliser le mot-clé friend à l'intérieur de la classe seulement
//...
private:
double x;
double y;
};
Surcharge d’opérateurs: surcharge interne
surcharge interne = surchage d’opérateurs à l’intérieur d’une classe (s’agit donc ici d’une méthode)
le prototype se place à l’intérieur de la classe et comme pour une méthode quelconque on peut donner sa définition à l’extérieur au moyen de l’opérateur de résolution de portée
Complexe z1, z2;
//...
z1 += z2;
class Complexe
{
public:
//...
void operator+=(Complexe const& z2);
//...
};
void Complexe::operator+=(Complexe const& z2)
{
x += z2.x;
y += z2.y;
}
une fois l’opérateur +=
défini, il est possible d’établir le lien sémantique entre celui-ci et l’opérateur +
; en principe, on préfère définir l’opérateur le plus lourd en fonction du plus léger
void Complexe::operator+(Complexe z1, Complexe const& z2)
{
z1 += z2;
return z1;
}
sauf dans certains cas, on peut généralement choisir entre surcharge interne et surcharge externe; on peut préférer la surcharge externe lorsque le corps de la fonction peut être écrit sans avoir recours au mot-clé friend
(brise l’encapsulation) et sans devoir coder des accesseurs inutiles et sans copies inutiles (par ex. pour +
qui exploite +=
); pour +=
, qui doit modifier une instance, on préfère une surcharge interne pour donner accès aux variables internes
Surcharge d’opérateurs: compléments
dans la pratique du C++, il est possible 1) de ne pas faire de surcharge des opérateurs 2) surchargement simplement les opérateurs de base 3) surcharger les opérateurs en utilisant leur version “auto-affectation” mais sans valeur de retour pour celles-ci
`void operator+=(Classe const&);`
4) faire la surcharge avec valeur de retour des opérateurs d’auto-affectation:
`Classe& operator+=(Classe const&);`
-
retour constant on a vu précédemment que le type de retour des opérateurs arithmétiques simples est précédé du mot-clé
const
; ceci déclare e.g. quez1+z2
(résultat de l’appel àoperator+(z1,z2)
appartenant à la même classe que les opérandes, n’est pas une variable (i.e. on ne peut pas modifier sa valeur); sans déclarer que la valeur de retour est constante, celle-ci pourrait être traitée comme une instance quelconque: il serait alors possible des formules absurdes comme++(z1 + z2)
ou(z1+z2)=f(x)
; ainsi, le type de retour de tels opérateurs doit êtreconst Classe
afin d’empêcher des modifications hasardeuses du résultat -
retour de l’opérateur d’affichage
cout << z1 << endl;
est équivalent àoperator<< (cout<<z1, endl);
qui est équivalent àoperator<<(operator<<(cout, z1), endl);
; la valeur de retour deoperator<<(cout, z1)
est le 1er argument d’un autre appel à l’opérateur, il s’agit donc également d’unostream&
, passé par référence pour pouvoir être modifié, et identique à celui passé en argument dans le 1er appel; avec un type de retourvoid
, enchainer des opérateurs<<
n’aurait plus de sens -
retour des opérateurs d’auto-affectation à un premier niveau de surcharge, on peut donner un type de retour
void
aux opérateurs comme+=
; en C++, toute expression a une valeur, il serait donc concevable d’écrirez3=(z1+=z2)
; le résultat dez1+=z2
- qui sera la valeur dez1
après l’opération d’auto-affectation - pourra être stocké dansz3
, qui est donc une instance de même classe quez1
; pour éviter une copie superflue, on peut donc retourner une référence surz1
: au niveau avancé, le type de retour des opérateurs d’auto-affectation est une référence sur l’instance courante, de la formeClasse&
au sein de la définition de la méthode, il faudra bien retourner, après les diverses opérations d’affecation, la valeur de l’instance courante; puisque this
est un pointeur sur celle-ci, la valeur à retourner est donc *this
dans l’exemple précédent, le type de retour est une référence puisque l’instance résultante existe déjà, ce qui permet d’éviter les copies inutiles
de manière générale il est important d’optimiser la surcharge d’opérateurs à l’aide de références afin de limiter toute copie superflue; conseillé de toujours employer une référence quand on en a la possibilité
exemple: version avec 1 à 3 copies inutiles
Complexe Complexe::operator+=(Complexe z2) // passage par valeur (copie potentielle)
{
Complexe z3; // crée une nouvelle instance Complexe qui sera retournée dans laquelle on copie l'instance courante modifiée
x += z2.x;
y += z2.y;
z3 = *this;
return z3;
}
// la valeur de retour est une nouvelle instance de la classe, copiée puisqu'elle n'est pas passée par référence
exemple: version avec 0 copie
Complexe& Complexe::operator+=(Complexe const& z2)
{
x += z2.x;
y += z2.y;
return *this;
}
opérateur d’affectation: opérateur =
est particulier, étant l’unique opérateur dit universel, i.e. fourni d’office pour toutes les classes
différence entre construction de copie et affectation: une construction de copie est appelée lors d’une initialisation d’instance
comme pour le constructeur de copie et le destructeur, la version par défaut de l’opérateur d’affectation fait une copie de surface souvent suffisante (cf. plus loin); sa redéfinition lorsqu’elle s’avère nécessaire, est souvent liée à la redéfinition du constructeur de copie et du destructeur (cf. plus haut)
dans certaines situations on peut aussi supprimer l’usage d’un tel opérateur; si une classe utilise beaucoup de mémoire par exemple; avec une syntaxe similaire à la suppression du constructeur de copie, on peut empêcher des copies d’instances:
EnormeClasse& operator=(EnormeClasse const&) = delete;
l’opérateur d’affectation ne peut être surchargé qu’en interne; son argument doit être de la même classe que l’instance courante et sera passé par référence constante par souci d’optimisation
pour pouvoir écrire a=(b=c)
, la valeur de retour d’une affectation sera une référence sur la valeur finale de l’instance courante
l’opérateur d’affectation fourni par défaut est souvent suffisant; s’il est nécessaire de le redéfinir, en C++11 il est conseillé d’utiliser le swap
swap
échange deux arguments: swap(a,b)
va échanger les valeurs entre les variables a
et b
; elle est définie dans la bibliothèque utility
pour les types de base et doit être étendue à la classe concernée par une surcharge de cette fonction; prend en argument ces 2 instances, par référence afin de pouvoir les modifier
pour redéfinir l’opérateur d’affectation de façon optimale; il faudrait utiliser la fonction swap
entre l’instance courante et l’argument passé par valeur, qui contient la valeur du 2ème opérande; puisque l’échange se fait avec une copie locale de celui-ci, seule l’instance courante, le 1er opérande, sera modifiée pour prendre sa valeur; l’affectation est ainsi optimisée
Classe& Classe::operator=(Classe source) // passage par valeur !!!
{
swap(*this, source);
return *this;
}
Héritage: concepts
l’héritage permet de regrouper des caractéristiques communes dans une super-classe dont héritent les sous-classes qui en sont des versions enrichies, étendues; cet outil permet de définir une relation “est-un”
une sous-classe est dite enrichie par des attributs et des méthodes supplémentaires, et spécialisée si elle redéfinit des méthodes héritées de C
l’héritage permet d’organiser et de clarifier le code en explicitant les relations structurelles entre des classes, tout en limitant la redondance de code
l’héritage ne doit pas être une relation “possède-un”: ce lien doit être établi par l’encapsulation par le fait de déclarer un attribut
transitivité: l’héritage est transitif, i.e. si une super-classe hérite d’une super-super-classe, alors la sous-classe possède également tous les attributs et méthodes de cette dernière
ces relations “est-un” définissent une hiérarchie de classe entre les classes les plus générales, classes parentes en haut et classes enfant spécialisées et enrichies en bas
syntaxe pour faire hériter d’une super-classe
class NomSousClasse: public NomSuperClasse{
// déclaration de méthodes et attributs spécifiques à la sous-classe
// ...
};
Héritage: droit protégé
dans une relation d’héritage, la sous-classe dispose aussi des membres privés de la super-classe mais n’y a pas accès (le droit d’accès privé limite la visibilité à l’enceinte de la classe)
il existe un autre type d’accès au sein d’une hiérarchie de classe, le droit d’accès protégé (mot-clé protected
): assure la visibilité des membres d’une classe dans toutes les classes de la descendance; extension du niveau privé qui accorde des droits d’accès privilégiés uniquement aux sous-classes (restent privés à tout autre endroit du code)
un membre protégé est uniquement accessible dans les sous-classes dans leur propre portée
la portée de classe permet de manipuler directement tous les attributs et méthodes de quelconque instance d’une classe (courante ou non) au sein de la définition; e.g. si une classe B
admet un attribut privé b
, alors on peut accéder dans les méthodes de B
au b
de l’instance courante (this->b
) mais également d’une autre instance autreB
passée en paramètre (autreB.b
); ainsi b
est accessible dans la classe B
à travers tout type d’objet de type B
(i.e. b
est de portée B
)
accès possible d’une classe B
à l’attribut protégé a
de la super-classe A
mais uniquement dans sa portée; un attribut privé de A
n’est pas accessible en dehors de la super-classe; au sein de B
, on peut accéder au a
protégé de l’instance courante ou d’une autre instance autreB
de la même classe, mais pas à celui d’une autre classe comme A (il s’agirait d’un accès externe à un attribut protégé, interdit), exemple:
class A
{
protected: int a;
private: int prive;
};
class B: public A
{
public:
//...
void f(B autreB, A autreA, int x)
{
a = x; // ok A::a est protected -> accès possible
prive = x; // ERREUR ! A::prive est private
a += autreB.prive; // ERREUR ! même raison
a += autreB.a; // ok même portée (B::)
a += autreA.a; // INTERDIT this n'est pas de la même portée que autreA
}
};
en conclusion:
- les membres publics sont accessibles à tous les utilisateurs d’une classe
- les membres privés sont accessibles qu’au programmeur de la classe
- les membres protégés sont accessibles à tous les programmeurs d’extension par sous-classes
Héritage: masquage
spécialiser en redéfinissant les méthodes de la super-classe
situation de masquage: un appel à la méthode par cette sous-classe renverra à la version spécialisée, qui l’emporte sur la version héritée de la super-classe; masquage = partage d’un même nom d’attribut ou de méthode sur plusieurs niveaux d’une hiérarchie de classe
pour les attributs, masquage souvent source de confusion: nommer un attribut par le même nom (pas forcément le même type) qu’un attribut hérité, qui masquera ce dernier
pour les méthodes, masquage courant, permet de les spécialiser; la méthode héritée est dite méthode générale, appelée par les sous-classes qui ne la masquent pas; la méthode spécifique à la sous-classe est dite méthode spécialisée
un objet dont un des membres est spécialisé dispose également du membre hérité même s’il est masqué; on utilise l’opérateur de résolution de portée pour y avoir accès; syntaxe:
NomSuperClasse::méthode // ou ::attribut
Héritage: constructeurs - partie 1
l’instanciation d’une classe s’accompagne d’une initialisation des attributs; cette tâche ne peut plus être prise en charge par le constructeur d’une sous-classe: celle-ci hérite des attributs, parfois privés, d’une super-classe qu’elle ne peut pas initialiser; l’initialisation des attributs hérités doit se faire dans la classe où ils sont explicitement définis: chaque constructeur de la sous-classe fait donc appel à un constructeur de la super-classe
l’appel au constructeur de la super-classe depuis le constructeur de la sous-classe se fait dans la liste d’initialisation (ou section deux points); syntaxe:
SousClasse(liste de paramètres): SuperClasse(arguments), Attribut1(valeur1), ...
{
// corps du constructeur}
}
le constructeur de la super-classe porte son nom et est placé au début de la section d’appel aux constructeurs des attributs
exemple:
class FigureGeometrique
{
protected: Position position;
public:
FigureGeometrique(double x, double y): position(x,y){} // constructeur
//...
};
class Rectangle: public FigureGeometrique
{
protected: double largeur; double hauteur;
public:
Rectangle(double x, double y, double l, double h): FigureGeometrique(x,y), largeur(l), hauteur(h){} // constructeur de la sous-classe qui appelle constructeur de la super-classe
//...
};
// il n'est pas nécessaire qu'une sous-classe définisse des attributs supplémentaires:
class Carre: public Rectangle{
public:
Carre(double taille): Rectangle(taille, taille){}
};
un appel explicite au constructeur de la super-classe n’est pas nécessaire si cette dernière possède un constructeur par défaut (l’appel est alors effectué par le compilateur); si la classe n’a pas de constructeur par défaut, l’appel doit être explicite
rappel: le constructeur par défaut, qui ne prend pas d’arguments, existe par défaut si la classe n’a pas de constructeur, mais disparait et doit être réécrit dès la création d’un constructeur
il est conseillé de toujours déclarer au moins un constructeur pour chaque classe et d’effectuer un appel explicite à un constructeur de la super-classe, même à celui par défaut
Héritage: constructeurs - partie 2
ordre d’appel des constructeurs: la construction d’une sous-classe appelle d’abord le constructeur de la super-classe la plus générale, puis, dans l’ordre, les constructeurs des super-classes qui en héritent, avant de terminer par l’initialisation de la partie spécifique de la classe instanciée
les destructeurs sont appelés dans l’ordre inverse des constructeurs
constructeurs de copie: une redéfinition du constructeur de copies d’une sous-classe s’accompagne toujours d’un appel explicite au constructeur de copies de la super-classe; sinon, son constructeur par défaut est appelé et la copie est souvent mal réalisée
Rectangle(Rectangle const& autre): FigureGeometrique(autre), largeur(autre.largeur), hauteur(autre.hauteur) {}
en appelant comme ci-dessus le constructeur de copie de FigureGeometrique
avec l’instance copiée en argument (étant Rectangle
, elle est aussi une FigureGeometrique
), l’attribut propre à cette super-classe, la position, sera copiée dans la nouvelle instance ; si l’on n’avait spécifié, dans le constructeur de copie de Rectangle
, que les valeurs des attributs spécifiques à Rectangle
, sans appeler le constructeur de copie FigureGeometrique
, alors un appel implicite sera fait à son constructeur par défaut
héritage des constructeurs: les constructeurs ne sont pas hérités dans une relation d’héritage; mais depuis C++11, il est possible de demander à ce qu’ils le soient:
using SuperClasse::SuperClasse;
ceci force l’héritage de tous les constructeurs de la super-classe de sorte que la sous-classe peut être construite avec les mêmes arguments; il s’agit d’une pratique peu recommandée puisque les constructeurs de la super-classe n’itialisent pas les attributs de la sous-classe: on limitera son utilisation, si nécessaire, à des sous-classes qui n’ont pas de nouvel attribut
Copie profonde
copie de surface: des constructeurs et destructeurs par défaut sont fournis par le compilateur s’ils ne sont pas définis; le constructeur de copie par défaut effectue une copie de surface qui consiste à copier membre à membre la valeur de chaque attribut; cette copie est d’ordinaire suffisante, mais elle peut poser problème notamment quand des attributs sont des pointeurs
copie profonde: dans des situations où les attributs sont des pointeurs, le constructeur de copie doit être redéfini pour réaliser une copie profonde qui ne copie pas les adresses mais duplique les zones pointées; les attributs d’un objet copié devraient pointer vers des variables à même valeur mais stockées dans des zones mémoires distinctes de l’objet d’origine; cette indépendance implique que la manipulation de l’objet copié (et notamment sa destruction), n’a plus aucune incidence sur l’objet d’origine
pour les mêmes raisons si une classe contient des pointeurs, il est parfois important de redéfinir d’une façon similaire, l’opérateur d’affectation =
: sa version par défaut effectue aussi une copie de surface (ne pas oublier de considérer également le destructeur)
Polymorphisme et résolution dynamique des liens
le polymorphisme permet à un même code de s’adapter à aux types des données auxquelles il s’applique; rend le code générique, écrit de façon unifiée pour différents types de données
dans une hiérarchie de classe, le type est hérité: on dit qu’une instance de la sous-classe est aussi une instance de la super-classe, et cette relation est transitive
l’héritage de type s’applique dans le cadre de l’affectation, mais aussi du passage des arguments
en C++, par défaut, le type de la variable détermine la méthode à appeler = résolution statique des liens: le choix de la méthode se fait statiquement avant l’exécution
le polymorphisme fait que, lors d’affectations ou de passages d’arguments, des instances d’une sous-classe soient substituables aux instances de leurs super-classes tout en gardant leurs propriétés propres; la méthode à invoquer est choisie pendant l’exécution, en fonction de la nature réelle des instances = résolution dynamique des liens
pour avoir recours à la résolution dynamique des liens, il faut réunir les 2 conditions suivantes
- les méthodes concernées doivent être déclarées comme virtuelles
- elles doivent s’exercer sur les instances réellement concernées grâce à des références ou des pointeurs
Polymorphisme: méthodes virtuelles
les méthodes virtuelles permettent la résolution dynamique des liens, au travers de références ou de pointeurs; ainsi le choix de la méthode se fait en fonction du type réel de l’instance
une méthode à résoudre dynamiquement doit être déclarée comme virtuelle, en précédant son prototype de virtual
toute spécialisation d’une méthode virtuelle dans une sous-classe est aussi virtuelle par transitivité, même sans spécifier le mot-clé virtual
pour activer la résolution dynamique des liens il est également essentiel que le passage des arguments des méthodes virtuelles soit fait par le biais de références ou de pointeurs: elles opèrent ainsi sur les instances réelles (si le passage se faisait par valeur, le polymorphsime serait impossible car lors de l’appel l’objet serait copié dans une variable du type de la super-classe, en perdant ses spécificités)
pour éviter qu’une destruction d’un objet en soit que partielle, il est conseiller de toujours déclarer les destructeurs comme virtuels
au contraire, puisqu’un constructeur est chargé d’initialiser l’instance courante, il ne peut pas être virtuel; s’il appelle dans son corps des méthodes virtuelles, la virtualité de ces méthodes est ignorée
Masquage, substitution et surcharge
surcharge = quand des fonctions ou des méthodes dans la même portée portent le même nom mais se distinguent par des paramètres différents
masquage de méthodes = lorsque des méthodes de portée différente ont le même nom; celles dont la portée est la plus proche masquent les plus lointaines, indépendamment de leurs paramètres; il suffit d’une méthode avec le même nom pour masquer plusieurs méthodes surchargées dans une autre portée
subtitution de méthodes virtuelles = redéfinition d’une méthode virtuelle héritée d’une super-classe, dans le but de résoudre dynamiquement des liens; la substitution d’une seule méthode virtuelle, même avec des paramètres différents, masque toutes les autres qui portent le même nom
depuis C++11, deux mots-clés placés après le prototype pour mieux spécifier redéfinitions ou surcharges:
override
: indique que l’on redéfinit une méthode virtuelle héritée d’une super-classe (le compilateur vérifie qu’il s’agit bien d’une substitution de méthode virtuelle; si aucun prototype identique n’existe, il signale une erreur; constitue donc une protection contre les erreurs)final
: empêche toute substitution future de la méthode dans les sous-classes; peut également interdire la création de sous-classes d’une classe en le plaçant après le nom de la classe dans son prototype
Classes abstraites
au niveau le plus élevé de la classe, il est parfois impossible de définir une méthode générale qui devra pourtant exister dans toutes les sous-classes
méthode virutelle pure = méthode abstraite = méthode qui doit exister et être redéfinie dans toutes les sous-classes que l’on souhaite instancier sans qu’il soit nécessaire de la définir dans la super-classe; la super-classe ne donne souvent que son prototype sans la définir explicitement; signalée en ajoutant =0
à la fin de son prototype
virtual Type nom_methode(liste paramètres) = 0;
une telle méthode peut être appelée au niveau de la super-classe
classe FigureFermee
{
public:
virtual double surface() const =0;
virtual double perimetre() const =0;
//une méthode virtuelle pure peut être appelée dans la super-classe
double volume(double hauteur) const
{
return hauteur*surface();
}
};
classe abstraite = classe qui contient au moins une méthode virtuelle pure et qui ne peut pas être instanciée; toute sous-classe héritant d’une classe abstraite l’est aussi tant qu’elle ne redéfinit pas toutes ses classes virtuelles pures héritées
les classes abstraites sont idéales pour construire la racine des arbres d’héritage; grâce à l’outil des méthodes virtuelles pures, le polymorphisme complète l’abstraction; elles permettent de définir des concepts génériques communs à toutes les sous-classes mais trop abstraits pour être codés à haut niveau
Collections hétérogènes
collections hétérogènes = ensemble d’objets d’une même super-classe mais traités de façon polymorphique, spécifique; sont une application importante du polymorphisme
pour permettre la résolution dynamique des liens il faut utiliser une méthode virtuelle et accéder aux instances par le biais de pointeurs ou références; lorsque c’est possible il est conseillé d’utiliser des références; cependant, on ne peut pas réaliser des tableaux de références; dans le cadre d’une collection hétérogène, on utilise donc un tableau de pointeurs vers des instances de la super-classe
il est important de passer les éléments du tableau par référence: les unique_ptr
ne peuvent pas être copiés; dans le cas contraire, plusieurs pointeurs partageraient la même zone mémoire; si la valeur n’est pas modifiée, on peut utiliser une référence constante
pour garantir l’intégrité d’une collection hétérogène, la durée de vie des éléments pointés doit être au moins aussi longue que celle des pointeurs
l’allocation dynamique est l’outil qui permet de préserver la zone mémoire allouée aussi longtemps que la collection qui la contient; grâce à l’utilisatio du mot-clé new
, la zone mémoire nouvellement allouée existe jusqu’à l’emploi du mot delete
; dans une collection d’unique_ptr
, la désallocation sera faite automatiquement (recommandé de les favoriser par rapport aux pointeurs à la C); les unique_ptr
permettent également d’assurer qu’un objet n’est pointé que par un pointeur à la fois, ce qui limite les erreurs de manipulation
la question de l’intégrité des données se pose également avec l’emploi de pointeurs dans une classe: il faut garantir la durée de vie des données pointées aussi longtemps que la classe qui les exploite (autant pour les pointeurs intelligents que pour les pointeurs à la C); d’autres problèmes se posent avec les pointeurs à la C: la désallocation et le partage de données entre collections. Selon la conception de la classe, il est souvent nécessaire de redéfinir des outils qui le permettent, comme le constructeur de copie, le destructeur et l’opérateur d’affectation
désallocation: contrairement aux pointeurs intelligents, les pointeurs à la C doivent être manuellement désalloués
une copie de surface des pointeurs fait pointer plusieurs instances vers les mêmes entités et expose souvent à des erreurs de manipulation; avec unique_ptr
la question ne se pose pas: la copie est interdite puisque ces pointeurs ne peuvent pas partager une zone mémoire; lorsqu’on utilise des pointeurs à la C, il faut s’interroger sur la nécessité de redéfinir le constructeur de copie
Héritage multiple
rappel: les notions principales de la POO
- l’encapsulation et abstraction = regrouper des données et traitements en une seule et même entité, séparer l’interface d’utilisation des détails de manipulation
- l’abstraction = établit une relation “est-un” entre différentes classes, permet le polymorphisme d’inclusion
- le polymorphisme = nécessite l’emploi de méthodes virtuelles au travers de références ou pointeurs, induit une adaptation de l’exécution selon le type de données traitées
héritage multiple = extension de l’héritage simple; une sous-classe peut hériter directement de plusieurs classes parentes; une sous-classe va hériter du type, des attributs et des méthodes (hormis constructeurs et destructeurs) de toutes ses super-classes
pour déclarer une relation d’héritage multiple, on indique l’ensemble des super-classes dont hérite une sous-classe dans sa déclaration, séparées par des virgules; attention: l’ordre de déclaration des liens d’héritage joue un rôle important dans la destruction et la destruction d’une instance !
class Sous-Classe: public SuperClass1, ..., public SuperClasseN
{
//...
};
de façon analogue à l’héritage simple, l’initialisation d’attributs hérités se fait dans la liste d’initialisation du constructeur de la sous-classe, par appel aux constructeurs des classes parentes
si une super-classe adment un constructeur par défaut, une invocation explicite n’est pas obligatoire, mais recommandée pour éviter tout oubli
attention: dans un contexte d’héritage multiple, l’ordre d’appel des constructeurs parents n’est pas donné par la liste d’initialisation du constructeur de la sous-classe mais par l’ordre de la déclaration de l’héritage; l’ordre d’appel des destructeurs se fait toujours dans l’ordre inverse de l’appel des constructeurs
la plupart des compilateurs donneront un message d’alerte lorsque la liste d’initialisation ne respecte pas l’ordre des liens d’héritage
Héritage multiple: masquage
dans l’héritage mulitple les droits d’accès s’appliquent comme dans une relation d’héritage simple: une sous-classe peut directement accéder aux membres publics et protégés de ses super-classes; des situations d’ambiguïté surviennent lorsque plusieurs super-classes contiennent un membre portant le même nom
dans l’exemple ci-dessous, invoquer la méthode afficher()
d’une instance Ovovivipare
provoquera une erreur de compilation due à un problème de résolution de portée; il s’agit en effet d’une situation de masquage, qui pose souci même si les méthodes héritées prennent des paramètres différents; la portée des méthodes étant différente, il ne s’agit pas d’une situation de surcharge !
classe Ovipare
{
// ...
void afficher() const;
};
classe Viviapre
{
// ...
void afficher(string const& entete) const;
};
class Ovovivipare: public Ovipare, public Vivipare
{
//...
};
int main()
{
Ovovivipare o(...);
o.afficher("un orvet");
return 0;
}
solutions possibles à ce problème:
- lever l’ambiguïté par l’opérateur de résolution de portée qui indique au compilateur la classe dont on appelle la méthode; cette solution est déconseillée car elle délègue à l’utilisateur externe le choix de comment afficher un objet de cette classe
- ajouter dans la super-classe une déclaration spéciale explicitant quelle méthode héritée sera invoquée lors d’un appel ambigu, avec la syntaxe:
using SuperClasse::nomMethodeouAttributAmbigu;
attention: pas de parenthèses ni type de retour; cette déclaration ne donne que le nom du membre concerné
- clarifier en définissant proprement une méthode au sein de la sous-classe, par exemple
class Ovovivipare: public Ovipare, public Vivipare { //... public: void afficher() const { Ovipare::afficher(); Vivipare::afficher("mais aussi pour sa partie vivipare"); } };
Classes virtuelles
dans certaines situations d’héritage multiple, une sous-classe hérite plusieurs fois, indirectement, d’une même classe parente, ce qui peut s’avérer problématique (e.g. iostream
qui hérite des classes ostream
et istream
, super-classes qui héritent elles-mêmes toutes deux de la casse ` ios`)
pour éviter la duplication des attributs d’une super-classe plusieurs fois incluses lors d’héritages multiples, il faut déclarer son lien d’héritage avec toutes ses sous-classes comme virtuel; une telle super-super-classe est appelée super-classe virtuelle
attention! différence entre classe abstraite et classe virtuelle: une classe abstraite est une classe avec des méthodes virtuelles pures; une classe virtuelle a un lien d’héritage virtuel vers ses sous-classes
signaler un héritage virtuel se fait lors de la déclaration du lien d’héritage
class SousClasse: public virtual SuperClassVirtuelle
{
//...
};
c’est la classe pouvant être héritée plusieurs fois qui est virtuelle et non pas directement les classes utilisées dans l’héritage multiple; l’héritage virtuel établit donc une dépendance au niveau de l’implémentation entre différents niveaux d’héritage, raison pour laquelle certains langages refusent l’héritage multiple
dans un héritage simple, le constructeur d’une sous-classe ne fait appel qu’aux constructeurs de ses super-classes direces; mais dans un contexte d’héritage virtuel, le constructeur de la super-super-classe virtuelle doit être explicitement appelé par les constructeurs des sous-classes instanciables qui initialisent ainsi l’unique jeu d’attributs hérité de la classe virtuelle. Par la suite, les appels au constructeur de la super-classe virtuelle par le constructeur des classes intermédiaires sont ignorés; exemple:
Ovovivipare::Ovovivipare(//...): Animal(nom, habitat, regime), Ovipare(nb_oeufs), Vivipare(gestation), espece_rare(rarete){}
le constructeur appelle directement le constructeur de la super-super-classe virtuelle Animal
pour initialiser son unique objet Animal
hérité; ensuite appel aux super-classes directes Ovipare
et Vivipare
, dans leur constructeur, l’appel à la classe virtuelle est alors ignoré; enfin le constructeur d’Ovovivipare
peut initialisé ses propres attributs
dans le cas où la super-classe virtuelle a un constructeur par défaut, il n’est pas nécessaire d’expliciter l’appel à ce dernier mais il sera toujours effectué dès la création de l’instance la plus dérivée
l’ordre d’appel des constructeurs de copie vérifie aussi ces règles
Etude de cas
le verbe avoir souligne une relation d’encapsulation le verbe être signifie une relation d’héritage varier selon indique l’emploi du polymorphisme
puisqu’une méthode polymorphique s’adapte à la nature de l’instance traitée, les valeurs qu’elle utilise doivent le faire également (utilisation de pointeurs ou références)
un bon réflexe à adopter lors de l’introduction d’une méthode virtuelle est de rendre aussi le destructeur virtuel; ainsi on s’assure que sa descendance est correctement détruite
pour rendre une classe abstraite, il faut lui donner une méthode virtuelle pure: en créer une pour cette seule raison est dénué de sens, on préférera donner cette caractéristique au destructeur, qui existe automatiquement dans chaque classe; il est impératif de doter tout destructeur d’un corps et toute méthode virtuelle pure peut avoir un corps à condition qu’il soit externalisé
pour éviter qu’une sous-classe possède plusieurs fois un même attribut, utiliser un lien d’héritage virtuel; les liens d’héritage virtuels ont une incidence sur la construction qui doit en premier lieu appeler le constructeur de la classe virtuelle
par défaut, le constructeur de copie réalise une copie de surface, qui copie leur valeur, c’est-à-dire l’adresse de l’élément pointé, dans une autre instance; de plus, les copies ne sont pas possibles pour les unique_ptr
; il faut dans certains cas donc réaliser une copie profonde, qui consiste à copier les objets pointés et non leur adresse
copie polymorphique = copie qui conserve la nature des instances
redéfinition de l’opérateur d’affectation: retourner une référence sur l’instance courante permet d’enchainer plusieurs affectations, ex:
Montre& operator=(Montre source)
{
swap(coeur, source.coeur);
swap(accessoires, source.accessoires);
return *this;
}