Introduction
Une classe permet de regrouper des données et les fonctions qui travaillent sur ces données.
Pour du développement système, il n'est pas nécessaire de transformer tout le programme en architecture orientée objet. En revanche, il est important de comprendre les classes, car une grande partie du C++ moderne et de nombreuses bibliothèques les utilisent.
Le point le plus important de ce chapitre est le cycle de vie des objets :
- quand un objet est créé ;
- comment ses membres sont initialisés ;
- quand il est copié ;
- quand il est affecté ;
- quand il est détruit ;
- ce que le compilateur génère automatiquement.
1. Définir une classe
Une classe définit un nouveau type.
Code: Select all
class Buffer
{
public:
void setSize(size_t size);
size_t getSize() const;
private:
size_t m_size;
};
Code: Select all
Buffer buffer;
2. public et private
public rend un membre accessible depuis l'extérieur. private réserve l'accès direct à l'implémentation de la classe.
Code: Select all
class Counter
{
public:
void increment();
private:
int m_value;
};
3. Données et fonctions membres
Chaque objet possède ses propres données membres.
Code: Select all
class Counter
{
public:
void increment()
{
m_value++;
}
int getValue() const
{
return m_value;
}
private:
int m_value = 0;
};
4. Fonctions membres const
Une fonction membre qui ne doit pas modifier l'état de l'objet peut être déclarée const.
Code: Select all
int getValue() const
{
return m_value;
}
Code: Select all
void printCounter(const Counter& c)
{
std::cout << c.getValue();
}
5. Initialisation des membres
Les données membres doivent être initialisées. Une valeur par défaut peut être donnée dans la classe :
Code: Select all
class Counter
{
private:
int m_value = 0;
};
6. Le pointeur this
Dans une fonction membre non statique, this est un pointeur vers l'objet courant.
Code: Select all
void setValue(int value)
{
this->m_value = value;
}
Code: Select all
Counter c;
c.setValue(10);
On n'a généralement pas besoin d'écrire this->. Il devient utile pour désambiguïser des noms, retourner l'objet courant ou transmettre son adresse.
7. Objets sur la stack et durée de vie automatique
Code: Select all
void function()
{
Counter c;
}
C'est une propriété fondamentale du C++ : la durée de vie des objets peut directement contrôler la durée de vie des ressources.
8. Allocation dynamique
Un objet peut être créé dynamiquement :
Code: Select all
Counter* c = new Counter;
c->increment();
delete c;
Code: Select all
c->increment();
Code: Select all
(*c).increment();
9. Cycle de vie d'un objet
- obtention du stockage ;
- construction ;
- utilisation ;
- éventuelles copies ou affectations ;
- destruction ;
- libération du stockage si nécessaire.
10. Constructeurs
Un constructeur est exécuté lors de la création d'un objet. Il porte le nom de la classe et n'a aucun type de retour.
Code: Select all
class Counter
{
public:
Counter(int value)
: m_value(value)
{
}
private:
int m_value;
};
Code: Select all
Counter c{10};
11. Listes d'initialisation
Code: Select all
Counter(int value)
: m_value(value)
{
}
À distinguer de :
Code: Select all
Counter(int value)
{
m_value = value;
}
Pour certains types simples la différence est minime, mais elle devient importante pour les objets complexes, références et membres const.
12. Ordre d'initialisation
Les membres sont initialisés selon leur ordre de déclaration dans la classe et non selon l'ordre écrit dans la liste d'initialisation.
Code: Select all
class Example
{
private:
int a;
int b;
public:
Example()
: b(20), a(10)
{
}
};
Il est donc préférable d'écrire la liste d'initialisation dans le même ordre que les déclarations.
13. Constructeur par défaut
Un constructeur par défaut est utilisable sans argument.
Code: Select all
class Counter
{
public:
Counter()
: m_value(0)
{
}
private:
int m_value;
};
Code: Select all
Counter c;
14. = default
= default demande explicitement au compilateur son comportement par défaut.
Code: Select all
Counter() = default;
Counter(const Counter&) = default;
Counter& operator=(const Counter&) = default;
~Counter() = default;
15. = delete
= delete interdit une opération.
Code: Select all
class Resource
{
public:
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
};
C'est très utile pour un objet propriétaire exclusif d'une ressource : une copie naïve pourrait créer deux propriétaires de la même ressource et provoquer une double libération.
16. Plusieurs constructeurs
Une classe peut proposer plusieurs constructeurs.
Code: Select all
class Counter
{
public:
Counter()
: m_value(0)
{
}
Counter(int value)
: m_value(value)
{
}
private:
int m_value;
};
Code: Select all
Counter a;
Counter b{100};
17. Constructeurs délégués
Un constructeur peut appeler un autre constructeur de la même classe.
Code: Select all
class Counter
{
public:
Counter()
: Counter(0)
{
}
Counter(int value)
: m_value(value)
{
}
private:
int m_value;
};
18. Constructeurs de conversion et explicit
Un constructeur à un argument peut permettre certaines conversions implicites.
Code: Select all
class Number
{
public:
Number(int value)
: m_value(value)
{
}
private:
int m_value;
};
Code: Select all
explicit Number(int value)
: m_value(value)
{
}
19. Constructeur de copie
Un constructeur de copie construit un NOUVEL objet à partir d'un objet existant.
Forme classique :
Code: Select all
Buffer(const Buffer& other);
Code: Select all
Buffer a;
Buffer b = a;
Le paramètre est généralement const Buffer& afin d'éviter de devoir copier l'objet simplement pour le transmettre au constructeur de copie.
20. Copie membre par membre
Pour une classe composée de membres dont la copie normale est correcte, la copie générée par le compilateur peut suffire.
Code: Select all
class Position
{
public:
int x;
int y;
};
Le problème apparaît surtout lorsqu'une classe possède directement une ressource.
21. Copie superficielle et ressources
Code: Select all
class Buffer
{
private:
char* m_data;
};
Deux objets peuvent donc finir par pointer vers le même bloc. Si chacun pense en être propriétaire et le libère, on obtient une double libération.
Une classe propriétaire d'une ressource doit définir clairement sa politique :
- copie profonde ;
- copie interdite ;
- partage explicite ;
- transfert de propriété.
Code: Select all
void process(Buffer buffer);
Pour un gros objet, la copie peut être coûteuse.
23. Passage par référence constante
Lorsqu'une fonction doit seulement consulter un objet :
Code: Select all
void process(const Buffer& buffer);
- on évite normalement une copie de Buffer ;
- on travaille sur l'objet existant ;
- const empêche sa modification via cette référence.
24. Affectation d'un objet
Code: Select all
Buffer a;
Buffer b;
b = a;
Compare avec :
Code: Select all
Buffer b = a;
25. Opérateur d'affectation par copie
Forme classique :
Code: Select all
Buffer& operator=(const Buffer& other);
Code: Select all
Buffer& Buffer::operator=(const Buffer& other)
{
if (this == &other)
return *this;
// Copier correctement l'état.
return *this;
}
return *this renvoie l'objet courant par référence.
26. Auto-affectation
Code: Select all
a = a;
Un test classique est :
Code: Select all
if (this == &other)
return *this;
27. Destructeur
Le destructeur est exécuté lorsque la durée de vie de l'objet se termine.
Code: Select all
class Buffer
{
public:
~Buffer();
};
Code: Select all
Buffer::~Buffer()
{
// libération des ressources possédées
}
Pour :
Code: Select all
Buffer* b = new Buffer;
delete b;
28. Ordre de destruction
Les objets locaux d'une même portée sont détruits dans l'ordre inverse de leur construction.
Code: Select all
Counter a;
Counter b;
Counter c;
Code: Select all
a
b
c
Code: Select all
c
b
a
29. Construction, copie et affectation
Construction normale
Code: Select all
Buffer a;
Code: Select all
Buffer a{1024};
Code: Select all
Buffer b = a;
Affectation
Code: Select all
Buffer b;
b = a;
Destruction
Lorsque la durée de vie de b se termine, son destructeur est exécuté.
C'est LA distinction essentielle du chapitre.
30. Fonctions spéciales générées par le compilateur
Selon la définition de la classe, le compilateur peut générer plusieurs fonctions spéciales :
- constructeur par défaut ;
- constructeur de copie ;
- opérateur d'affectation par copie ;
- constructeur de déplacement ;
- opérateur d'affectation par déplacement ;
- destructeur.
Une copie automatique n'est pas forcément correcte simplement parce que le programme compile.
31. Copie autorisée ou interdite
Si la copie automatique est correcte :
Code: Select all
Buffer(const Buffer&) = default;
Buffer& operator=(const Buffer&) = default;
Code: Select all
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
32. Exemple système : propriétaire d'une ressource
Code: Select all
class Resource
{
public:
Resource();
~Resource();
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
private:
void* m_resource;
};
- le constructeur obtient la ressource ;
- le destructeur la libère ;
- la copie est interdite ;
- un seul objet en est propriétaire.
33. Rapport avec le RAII
Le RAII associe la durée de vie d'une ressource à celle d'un objet.
Code: Select all
construction -> acquisition
destruction -> libération
Même si tu préfères parfois appeler directement les fonctions de fermeture des API système, il faut comprendre ce principe car il est omniprésent en C++ professionnel.
34. Classes et développement bas niveau
Une classe système peut simplement représenter :
- un buffer ;
- une ressource ;
- un mapping mémoire ;
- un socket ;
- un fichier ;
- un contexte de thread ;
- un paquet réseau ;
- un parseur binaire.
35. Ce qu'il faut absolument retenir
- Une classe définit un type ; un objet est une instance.
- public expose l'interface et private protège l'état interne.
- Une fonction membre const ne modifie normalement pas l'objet.
- this pointe vers l'objet courant.
- Un constructeur initialise un nouvel objet.
- La liste d'initialisation initialise directement les membres.
- Les membres sont initialisés dans leur ordre de déclaration.
- Un destructeur termine la vie de l'objet et peut libérer ses ressources.
- Les objets locaux sont détruits automatiquement en sortie de portée.
- Le constructeur de copie crée un nouvel objet depuis un autre.
- L'opérateur d'affectation travaille sur un objet déjà existant.
- Buffer b = a; et b = a; ne représentent donc pas la même opération.
- const& permet souvent de transmettre un gros objet sans le copier.
- Copier un pointeur copie son adresse, pas les données pointées.
- Une classe propriétaire d'une ressource doit avoir une politique de copie claire.
- = default demande le comportement automatique.
- = delete interdit explicitement une opération.
- explicit évite certaines conversions implicites.
- Pour le développement système, comprendre le cycle de vie est plus important que construire des architectures OOP complexes.
Code: Select all
// Construction
Type a;
// Construction avec argument
Type a{10};
// Construction par copie
Type b = a;
// Affectation
b = a;
// Passage sans copie
void function(const Type& object);
// Constructeur
Type(int value);
// Constructeur de copie
Type(const Type& other);
// Affectation par copie
Type& operator=(const Type& other);
// Destructeur
~Type();
// Comportement automatique demandé
Type() = default;
// Copie interdite
Type(const Type&) = delete;
// Conversion implicite limitée
explicit Type(int value);
Pour du C++ bas niveau, l'objectif principal de ce chapitre n'est pas de devenir spécialiste de l'OOP. Il faut surtout comprendre exactement la durée de vie des objets.
Quand tu vois :
Code: Select all
Type object;
- quel constructeur est appelé ?
- quels membres sont initialisés ?
- dans quel ordre ?
- l'objet possède-t-il une ressource ?
- peut-il être copié ?
- que signifie son affectation ?
- quand sera-t-il détruit ?
- que doit faire son destructeur ?
