Ce document explore plusieurs concepts fondamentaux du langage C++, notamment les principes de la programmation orientée objet (POO), les mécanismes de gestion de la mémoire, et les structures de données de la Standard Template Library (STL).
- Principes Fondamentaux de la Programmation Orientée Objet en C++
1.1. Encapsulation, Héritage et Polymorphisme
L'encapsulation, l'héritage et le polymorphisme sont les piliers de la POO.
1.1.1. Encapsulation
L'encapsulation vise à regrouper les données (membres) et les méthodes (fonctions membres) qui les manipulent au sein d'une unité, améliorant ainsi la modularité et la sécurité du code. Elle contrôle l'accès aux membres de la classe via des spécificateurs d'accès :
private: Accessible uniquement par les fonctions membres de la classe et les fonctions amies. Ni les objets de la classe ni les classes dérivées ne peuvent y accéder directement.protected: Accessible par les fonctions membres de la classe, les fonctions des classes dérivées et les fonctions amies. Les objets de la classe ne peuvent pas y accéder directement.public: Accessible par toute entité, y compris les fonctions membres, les fonctions des classes dérivées, les fonctions amies et les objets de la classe.
1.1.2. Héritage
L'héritage permet à une nouvelle classe (classe dérivée) d'acquérir les propriétés et les comportements d'une classe existante (classe de base). Son objectif principal est la réutilisation du code et l'extension des fonctionnalités.
1.1.3. Polymorphisme
Le polymorphisme signifie "une interface, plusieurs implémentations". Il permet de traiter des objets de classes différentes de manière uniforme via une interface commune (généralement un pointeur ou une référence vers la classe de base). Il existe deux types de polymorphisme :
- Polymorphisme statique (ou de compilation) : Résolu au moment de la compilation. Inclut la surcharge de fonctions, la surcharge d'opérateurs et les templates. C'est une liaison précoce (early binding).
- Polymorphisme dynamique (ou d'exécution) : Résolu au moment de l'exécution. Il est implémenté en C++ à l'aide de l'héritage et des fonctions virtuelles. C'est une liaison tardive (late binding).
1.2. Différences entre class et struct en C++
Bien que similaires, class et struct présentent des différences notables :
- Par défaut, les membres d'une
classsontprivate, tandis que ceux d'unestructsontpublic. - L'héritage d'une
classestprivatepar défaut, alors que celui d'unestructestpublicpar défaut. - Les
classpeuvent être utilisées comme paramètres de template, contrairement auxstruct.
1.3. Fonctions de Destructeur, Destructeurs Virtuels et Destructeurs Purs
Les destructeurs sont appelés automatiquement lorsque la durée de vie d'un objet se termine, assurant le nettoyage des ressources. L'ordre d'appel lors de la destruction d'une hiérarchie d'héritage est "enfant d'abord, puis parenet".
- Destructeur ordinaire : S'il n'est pas virtuel, la suppression d'un objet via un pointeur de classe de base ne garantit pas l'appel des destructeurs des classes dérivées.
- Destructeur virtuel : Déclaré avec le mot-clé
virtual. Il garantit que le destructeur approprié de la classe dérivée sera appelé lors de la suppression d'un objet via un pointeur de classe de base, prévenant ainsi les fuites de mémoire. Les destructeurs virtuels ont une implémentation par défaut dans la classe de base et n'abstrait pas la classe. - Destructeur pur : Déclaré avec
virtual ... = 0;. Il rend la classe abstraite. Le destructeur pur doit être implémenté dans les classes dérivées. Il nécessite une définition (même vide) dans la classe de base, mais cette définition doit être fournie en dehors de la classe.
Principe des Destructeurs Virtuels :
L'ordre de construction est : Parent -> Membre -> Enfant. L'ordre de destruction est : Enfant -> Membre -> Parent.
#include <iostream>
class Animal {
public:
Animal() { std::cout << "Constructeur Animal" << std::endl; }
virtual void speak() = 0; // Fonction pure virtuelle
virtual ~Animal() { std::cout << "Destructeur Animal" << std::endl; }
};
class Dog : public Animal {
public:
Dog() { std::cout << "Constructeur Dog" << std::endl; }
void speak() override { std::cout << "Le chien aboie" << std::endl; }
~Dog() override { std::cout << "Destructeur Dog" << std::endl; }
};
int main() {
Animal* ptr = new Dog();
ptr->speak();
delete ptr;
return 0;
}
Sortie attendue :
Constructeur Animal
Constructeur Dog
Le chien aboie
Destructeur Dog
Destructeur Animal
1.4. Différence entre #include <filename.h> et #include "filename.h"
#include <filename.h> : Le compilateur recherche le fichier d'en-tête dans les répertoires standards de la bibliothèque.
#include "filename.h" : Le compilateur recherche d'abord le fichier d'en-tête dans le répertoire courant du projet, puis dans les répertoires standards.
1.5. Fonctions Amies (friend)
Une fonction amie est une fonction qui, bien que n'étant pas un membre de la classe, est autorisée à accéder aux membres privés et protégés de cette classe. Elle est déclarée à l'intérieur de la classe avec le mot-clé friend.
- La relation d'amitié n'est pas réciproque.
- Une fonction amie n'a pas de pointeur
thiset ne peut pas être appelée via une instance de l'objet. - L'utilisation des fonctions amies doit être faite avec parcimonie pour préserver l'encapsulation.
#include <iostream>
class Box {
private:
double width;
public:
Box(double w) : width(w) {}
friend void printWidth(const Box& box); // Déclaration de la fonction amie
};
// Définition de la fonction amie
void printWidth(const Box& box) {
std::cout << "Largeur de la boîte : " << box.width << std::endl;
}
int main() {
Box myBox(10.0);
printWidth(myBox); // Appel de la fonction amie
return 0;
}
1.6. Fonctions Virtuelles et Fonctions Pures Virtuelles
Ces deux concepts sont cruciaux pour le polymorphisme dynamique.
- Fonction virtuelle : Déclarée avec
virtualdans la classe de base. Elle a une implémentation par défaut mais peut être redéfinie dans les classes dérivées. L'appel se fait via un pointeur ou une référence de classe de base, et la version correcte est déterminée à l'exécution. Les fonctions virtuelles sans corps peuvent être déclarées (ex:virtual void func() {}). - Fonction pure virtuelle : Déclarée avec
virtual ... = 0;. Elle n'a aucune implémentation dans la classe de base et oblige les classes dérivées à fournir leur propre implémentation. Une classe contenant au moins une fonction pure virtuelle est une classe abstraite et ne peut pas être instanciée.
Mécanisme des Fonctions Virtuelles :
Il repose sur une table de fonctions virtuelles (vtable) et un pointeur de fonction virtuelle (vptr).
- Chaque classe avec des fonctions virtuelles possède une vtable unique, créée à la compilation.
- Chaque objet de telle classe contient un vptr pointant vers la vtable de sa classe.
- Lors de l'appel d'une fonction virtuelle via un pointeur, le programme utilise le vptr pour localiser la vtable et trouver l'adresse de la fonction à exécuter, permettant le polymorphisme d'exécution.
Redéfinition (Override) : Lors de la redéfinition d'une fonction virtuelle, la signature (nom, types et ordre des paramètres, type de retour, spécificateurs const) doit être identique. L'utilisation du mot-clé override est fortement recommandée pour une vérification par le compilateur.
Fonction Pure Virtuelle :
Elle sert à définir une interface. Les classes qui en hérite doivent implémenter ces fonctions pures pour ne pas être elles-mêmes abstraites.
class Animal {
public:
virtual void speak() = 0; // Fonction pure virtuelle
virtual ~Animal() = default; // Destructeur virtuel par défaut
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Le chien aboie" << std::endl; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "Le chat miaule" << std::endl; }
};
void makeAnimalSpeak(Animal* ptr) {
ptr->speak();
delete ptr;
}
int main() {
makeAnimalSpeak(new Dog());
makeAnimalSpeak(new Cat());
return 0;
}
Sortie attendue :
Le chien aboie
Le chat miaule
1.7. Constructeurs et Destructeurs Virtuels
- Constructeurs : Ne peuvent pas être virtuels. La virtualité implique une décision à l'exécution basée sur le type d'objet, ce qui n'est pas possible pendant la construction où le type exact de l'objet n'est pas encore entièrement déterminé et où la vtable n'est pas encore initialisée.
- Destructeurs : Doivent être virtuels dans les classes de base si la classe est destinée à être héritée et que des objets de classes dérivées seront supprimés via des pointeurs de classe de base. Ceci assure la libération correcte des ressources par l'appel séquentiel des destructeurs appropriés.
1.8. Polymorphisme : Concepts et Implémentation
Le polymorphisme, divisé en statique et dynamique, est une caractéristique essentielle. Le polymorphisme dynamique, basé sur l'héritage et les fonctions virtuelles, utilise le mécanisme vptr/vtable pour lier les appels de fonction au type réel de l'objet à l'exécution.
- Bibliothèques Statiques et Dynamiques
2.1. Différences Fondamentales
- Bibliothèque Statique (.a, .lib) : Le code de la bibliothèque est intégré directement dans le fichier exécutable lors de la compilation. Cela résulte en des fichiers exécutables plus volumineux mais autonomes. Les mises à jour nécessitent une recompilation complète.
- Bibliothèque Dynamique (.so, .dll) : Le code de la bibliothèque n'est pas intégré. L'exécutable contient des références qui sont résolues à l'exécution lorsque la bibliothèque est chargée en mémoire. Les exécutables sont plus petits et les bibliothèques peuvent être mises à jour indépendamment sans recompiler l'exécutable principal. L'inconvénient est la dépendance à la présence de la DLL/SO au moment de l'exécution.
2.2. Processus de Compilation et de Linkage
Le cycle de vie typique d'un programme C++ implique plusieurs étapes :
-
Préprocesseur : Traite les directives comme
#include(insertion de fichiers d'en-tête),#define(macros), et la compilation conditionnelle. -
Compilation : Traduit le code source pré-traité en code assembleur.
-
Assemblage : Convertit le code assembleur en code machine, produisant des fichiers objets (.obj, .o).
-
Linkage (Édition de liens) : Combine les fichiers objets avec les bibliothèques nécessaires (statiques ou dynamiques) pour créer le fichier exécutable final. Les bibliothèques statiques sont liées à la compilation, tandis que les bibliothèques dynamiques créent des liens qui sont résolus à l'exécution.
-
Design Patterns Courants
3.1. Singleton (Instance Unique)
Objectif : Garantir qu'une classe n'ait qu'une seule instance et fournir un point d'accès global à cette instance.
Implémentation typique :
- Constructeur privé pour empêcher l'instanciation externe.
- Un membre statique privé pour stocker l'unique instance.
- Une méthode statique publique (
getInstance()) pour retourner l'instance. - Interdiction des constructeurs de copie et des opérateurs d'affectation pour éviter la duplication.
Variantes : "Paresseux" (lazy initialization, nécessite synchronisation pour le thread-safety) et "Gourmand" (eager initialization, thread-safe par défaut).
Singleton de Meyers (recommandé en C++11+) : Utilise une variable statique locale dans la méthode getInstance(). L'initialisation des variables statiques locales est garantie thread-safe par le standard C++11.
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // Initialisation thread-safe en C++11+
return instance;
}
void doSomething() { /* ... */ }
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
3.2. Factory (Usine)
Objectif : Déléguer la responsabilité de la création d'objets à une classe ou une méthode dédiée, séparant ainsi le code de création du code d'utilisation. Ceci réduit le couplage et facilite l'ajout de nouveaux types de produits.
Types : Simple Factory, Factory Method, Abstract Factory.
Distinction avec Strategy Pattern : Le Factory Pattern est un pattern créationnel (comment créer des objets), tandis que le Strategy Pattern est un pattern comportemental (comment implémenter des algorithmes diversifiés).
- Pointeurs et Références
Les pointeurs et les références sont des mécanismes permettant d'accéder à d'autres objets en mémoire.
- Pointeur : Contient l'adresse mémoire d'un objet. Il peut être réassigné, initialisé à
nullptr(ouNULL), et sa taille est fixe (ex: 4 ou 8 octets selon l'architecture). - Référence : Est un alias pour un objet existant. Elle doit être initialisée lors de sa déclaration et ne peut pas être réassignée à un autre objet. Elle ne peut pas être nulle. L'utilisation de
sizeofsur une référence retourne la taille de l'objet référencé.
Cas d'utilisation : Les pointeurs sont nécessaires pour allouer dynamiquement de la mémoire (new/delete) ou pour représenter l'absence d'objet (nullptr). Les références sont préférées pour passer des objets par référence à des fonctions lorsque l'on veut s'assurer que l'objet existe et ne sera pas changé (sauf si la référence est non-const).
- Allocation Mémoire
La mémoire d'un programme C++ est généralement divisée en plusieurs zones :
- Pile (Stack) : Alloue automatiquement la mémoire pour les variables locales des fonctions et les paramètres. La gestion est rapide mais la capacité est limitée. La mémoire est libérée automatiquement à la sortie de la portée.
- Tas (Heap) : Alloue dynamiquement la mémoire via
new(oumallocen C). Le programmeur est responsable de la libération de cette mémoire viadelete(oufree). Offre une grande flexibilité mais peut entraîner des problèmes comme les fuites de mémoire ou la fragmentation si mal gérée. - Zone de données statiques/globales : Pour les variables globales et statiques. La mémoire est allouée au démarrage du programme et existe pendant toute sa durée de vie.
- Zone de constantes littérales : Contient les chaînes de caractères littérales et d'autres constantes.
- Zone de code : Contient le code exécutable du programme.
- Standard Template Library (STL)
6.1. std::vector
Un std::vector est un conteneur séquentiel qui encapsule un tableau dynamique. Il permet l'accès direct aux éléments par index et redimensionne automatiquement sa capacité lorsque nécessaire.
- Caractéristiques : Séquence ordonnée, accès aléatoire O(1), insertion/suppression à la fin amortie O(1).
- Opérations courantes : Construction (par défaut, avec taille, avec plage d'éléments, copie), affectation (
=,assign,swap), accès ([],at,front,back), insertion (insert), suppression (erase,clear), taille (size,capacity,resize).
6.2. std::map
Un std::map est un conteneur associatif qui stocke des paires clé-valeur. Les clés sont uniques et triées. Les éléments sont généralement implémentés sous forme d'arbres binaires de recherche équilibrés (comme les arbres rouge-noir).
- Caractéristiques : Clés uniques et triées, accès aux éléments par clé O(log N).
- Opérations courantes : Insertion (
insertavecstd::pairoumake_pair,[]), recherche (find,count,lower_bound,upper_bound), suppression (erasepar itérateur ou clé,clear).
- Multithreading et Communication Inter-Processus (IPC)
7.1. Threads vs Processus
- Processus : Une instance d'un programme en exécution. Possède son propre espace d'adressage mémoire et ses ressources. La communication inter-processus (IPC) est nécessaire pour qu'ils échangent des données.
- Thread : Une unité d'exécution au sein d'un processus. Les threads partagent le même espace d'adressage mémoire du processus, ce qui rend la communication plus facile mais nécessite des mécanismes de synchronisation pour éviter les conflits.
7.2. Résolution des Blocages dans les Interfaces Graphiques (Ex: Qt)
Un blocage de l'interface utilisateur survient souvent lorsque des opérations longues (traitement de données, I/O réseau) sont exécutées dans le thread principal de l'interface. La solution consiste à déplacer ces opérations vers des threads séparés (background threads) et à utiliser des mécanismes de communication asynchrone (comme les signaux/slots dans Qt) pour mettre à jour l'interface utilisateur une fois le traitement terminé.
7.3. Communication Inter-Threads
- Mémoire Partagée : Les threads accèdent directement aux mêmes variables. Le mot-clé
volatilepeut être utilisé pour s'assurer que les lectures/écritures ne sont pas mises en cache par le compilateur ou le processeur de manière non attendue, mais il ne fournit pas de synchronisation atomique. Des mécanismes comme les mutex sont nécessaires pour une synchronisation sûre. - Signaux et Slots (Qt) : Un mécanisme puissant pour la communication entre objets, y compris entre threads. Pour communiquer des types personnalisés entre threads via signaux/slots, il faut les enregistrer avec
qRegisterMetaType.
7.4. Communication Inter-Processus (IPC)
Les processus ont des espaces mémoire isolés. Les méthodes d'IPC incluent :
- Pipes (Tuyaux) : Communication unidirectionnelle (ou bidirectionnelle pour les pipes nommés). Simples mais efficaces pour des flux de données non structurés.
- Message Queues (Files de messages) : Permettent aux processus d'envoyer et de recevoir des messages structurés. Implique des copies de données entre l'espace utilisateur et le noyau.
- Shared Memory (Mémoire Partagée) : La méthode la plus rapide car elle évite les copies de données. Les processus accèdent à une région mémoire commune. Nécessite des mécanismes de synchronisation (comme les sémaphores) pour éviter les conflits d'accès.
- Sockets : Mécanisme polyvalent pour la communication réseau, utilisable également pour l'IPC sur une même machine.
- Signaux : Mécanisme de notification asynchrone simple.
- Sémaphores : Principalement utilisés pour la synchronisation et le contrôle d'accès aux ressources partagées (y compris la mémoire partagée).
7.5. Connexion de Signaux/Slots dans Qt
Qt::DirectConnection: Le slot est appelé directement dans le thread de l'émetteur. Dangereux si l'émetteur et le récepteur sont dans des threads différents.Qt::QueuedConnection: Le slot est mis en file d'attente pour être exécuté dans le thread du récepteur. C'est la méthode sûre pour la communication inter-threads.