C++ Avancé
Ce cours approfondit les concepts avancés de C++ essentiels pour le développement d'applications performantes et la programmation système. Vous explorerez les fonctionnalités modernes du C++, les techniques de gestion de mémoire avancées, et les concepts qui font de C++ un langage puissant pour le hacking éthique et la sécurité informatique.
Table des matières
1. Templates et Programmation Générique
Templates de Fonctions
Les templates permettent d'écrire du code générique qui fonctionne avec différents types:
// Template de fonction
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
// Utilisation
int max_int = maximum<int>(10, 20); // 20
double max_double = maximum(3.14, 2.71); // 3.14 (déduction de type)
Spécialisation de Templates:
// Template général
template <typename T>
void afficher(T valeur) {
std::cout << "Valeur: " << valeur << std::endl;
}
// Spécialisation pour les chaînes C
template <>
void afficher<const char*>(const char* valeur) {
std::cout << "Chaîne: \"" << valeur << "\"" << std::endl;
}
Templates de Classes
Les templates peuvent également être appliqués aux classes:
// Template de classe
template <typename T, int Taille = 10>
class Tableau {
private:
T elements[Taille];
public:
Tableau() {
for (int i = 0; i < Taille; i++) {
elements[i] = T();
}
}
T& operator[](int index) {
if (index < 0 || index >= Taille) {
throw std::out_of_range("Index hors limites");
}
return elements[index];
}
int taille() const {
return Taille;
}
};
// Utilisation
Tableau<int, 5> tableau_entiers;
tableau_entiers[0] = 42;
Tableau<std::string> tableau_chaines; // Utilise la taille par défaut (10)
tableau_chaines[0] = "Hello";
2. Bibliothèque Standard (STL)
Conteneurs
La STL offre plusieurs types de conteneurs:
Conteneurs séquentiels:
#include <vector>
#include <list>
#include <deque>
// Vector (tableau dynamique)
std::vector<int> nombres = {1, 2, 3, 4, 5};
nombres.push_back(6); // Ajout à la fin
nombres[0] = 10; // Accès direct
// List (liste doublement chaînée)
std::list<std::string> noms = {"Alice", "Bob", "Charlie"};
noms.push_front("David"); // Ajout au début
noms.push_back("Eve"); // Ajout à la fin
// Deque (double-ended queue)
std::deque<float> valeurs = {1.1, 2.2, 3.3};
valeurs.push_front(0.0); // Ajout au début
valeurs.push_back(4.4); // Ajout à la fin
Conteneurs associatifs:
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
// Map (arbre binaire de recherche)
std::map<std::string, int> ages = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35}
};
ages["David"] = 40; // Insertion ou mise à jour
// Set (ensemble ordonné)
std::set<int> nombres_uniques = {3, 1, 4, 1, 5}; // {1, 3, 4, 5}
nombres_uniques.insert(2); // {1, 2, 3, 4, 5}
// Versions non ordonnées (tables de hachage)
std::unordered_map<std::string, int> scores;
std::unordered_set<int> valeurs_uniques;
Itérateurs
Les itérateurs permettent de parcourir les conteneurs de manière uniforme:
std::vector<int> nombres = {10, 20, 30, 40, 50};
// Parcours avec itérateurs
for (auto it = nombres.begin(); it != nombres.end(); ++it) {
std::cout << *it << " "; // 10 20 30 40 50
}
// Itérateurs inversés
for (auto it = nombres.rbegin(); it != nombres.rend(); ++it) {
std::cout << *it << " "; // 50 40 30 20 10
}
// For basé sur une plage (C++11)
for (const auto& n : nombres) {
std::cout << n << " "; // 10 20 30 40 50
}
Types d'itérateurs:
- Input: Lecture une seule fois (ex: std::istream_iterator)
- Output: Écriture une seule fois (ex: std::ostream_iterator)
- Forward: Lecture multiple, avance uniquement
- Bidirectional: Avance et recule (ex: std::list)
- Random Access: Accès direct à n'importe quel élément (ex: std::vector)
Algorithmes
La STL fournit de nombreux algorithmes génériques:
#include <algorithm>
#include <numeric>
#include <vector>
std::vector<int> nombres = {5, 2, 8, 1, 9, 3};
// Tri
std::sort(nombres.begin(), nombres.end()); // {1, 2, 3, 5, 8, 9}
// Recherche
auto it = std::find(nombres.begin(), nombres.end(), 5);
if (it != nombres.end()) {
std::cout << "Trouvé à la position: " << (it - nombres.begin()) << std::endl;
}
// Transformation
std::vector<int> carres(nombres.size());
std::transform(nombres.begin(), nombres.end(), carres.begin(),
[](int n) { return n * n; }); // {1, 4, 9, 25, 64, 81}
// Réduction
int somme = std::accumulate(nombres.begin(), nombres.end(), 0); // 28
// Filtrage (avec std::copy_if et back_inserter)
std::vector<int> pairs;
std::copy_if(nombres.begin(), nombres.end(), std::back_inserter(pairs),
[](int n) { return n % 2 == 0; }); // {2, 8}
3. Gestion Avancée de la Mémoire
Smart Pointers
Les pointeurs intelligents gèrent automatiquement la durée de vie des objets:
#include <memory>
// unique_ptr: propriété exclusive
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// auto ptr2 = ptr1; // Erreur: unique_ptr ne peut pas être copié
auto ptr2 = std::move(ptr1); // Transfert de propriété
// *ptr1 est maintenant indéfini, ptr1 est null
// shared_ptr: propriété partagée avec comptage de références
std::shared_ptr<int> sptr1 = std::make_shared<int>(100);
{
auto sptr2 = sptr1; // Compteur de références = 2
std::cout << *sptr2 << std::endl; // 100
} // sptr2 est détruit, compteur = 1
// Quand le compteur atteint 0, la mémoire est libérée
// weak_ptr: référence faible à un shared_ptr
std::weak_ptr<int> wptr = sptr1;
if (auto locked = wptr.lock()) { // Vérifie si l'objet existe encore
std::cout << *locked << std::endl; // 100
}
new et delete directement. Préférez les smart pointers pour éviter les fuites de mémoire et les erreurs de gestion.
Allocateurs Personnalisés
Les allocateurs permettent de contrôler précisément l'allocation de mémoire:
template <typename T>
class PoolAllocator {
private:
// Implémentation d'un pool d'allocation
public:
using value_type = T;
T* allocate(std::size_t n) {
// Allouer n objets de type T depuis le pool
}
void deallocate(T* p, std::size_t n) {
// Libérer la mémoire
}
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
// Construire un objet à l'adresse p
new(p) U(std::forward<Args>(args)...);
}
template <typename U>
void destroy(U* p) {
// Détruire un objet sans libérer sa mémoire
p->~U();
}
};
// Utilisation avec un conteneur STL
std::vector<int, PoolAllocator<int>> nombres;
Placement New et Alignement
Techniques avancées pour contrôler l'emplacement et l'alignement des objets:
// Placement new: construire un objet à une adresse spécifique
char buffer[sizeof(std::string)];
std::string* str = new(buffer) std::string("Hello"); // Construit dans buffer
str->~string(); // Destruction manuelle (pas de delete)
// Alignement (C++11)
struct alignas(16) AlignedStruct {
int x;
char c;
double d;
};
// Allocation alignée (C++17)
void* ptr = std::aligned_alloc(64, 1024); // 1024 octets alignés sur 64
// Utilisation...
std::free(ptr);
4. Programmation Concurrente
Threads
C++11 a introduit un support natif pour la programmation multithreads:
#include <thread>
#include <iostream>
void fonction_thread(int id) {
std::cout << "Thread " << id << " en cours d'exécution" << std::endl;
}
int main() {
// Création d'un thread
std::thread t1(fonction_thread, 1);
// Passage d'une lambda
std::thread t2([](int id) {
std::cout << "Lambda dans thread " << id << std::endl;
}, 2);
// Attendre la fin des threads
t1.join();
t2.join();
return 0;
}
Synchronisation
Mécanismes pour coordonner l'accès aux ressources partagées:
#include <mutex>
#include <condition_variable>
std::mutex mtx; // Protège l'accès à une ressource partagée
void fonction_securisee() {
// Verrouillage manuel
mtx.lock();
// Section critique...
mtx.unlock();
// RAII avec lock_guard (préférable)
{
std::lock_guard<std::mutex> lock(mtx);
// Section critique...
} // Déverrouillage automatique
// unique_lock (plus flexible)
std::unique_lock<std::mutex> lock(mtx);
// Section critique...
lock.unlock(); // Déverrouillage explicite
// ...
lock.lock(); // Reverrouillage
}
// Condition variable
std::condition_variable cv;
bool pret = false;
void producteur() {
std::unique_lock<std::mutex> lock(mtx);
// Préparer les données...
pret = true;
cv.notify_one(); // Notifier un thread en attente
}
void consommateur() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return pret; }); // Attendre que pret soit true
// Traiter les données...
}
Modèle de Mémoire et Atomiques
Opérations atomiques et ordonnancement de la mémoire:
#include <atomic>
// Variables atomiques
std::atomic<int> compteur(0);
compteur++; // Opération atomique
// Opérations atomiques explicites
int ancien = compteur.fetch_add(5); // Ajoute 5 et retourne l'ancienne valeur
// Barrières mémoire
std::atomic_thread_fence(std::memory_order_acquire); // Barrière d'acquisition
// ...
std::atomic_thread_fence(std::memory_order_release); // Barrière de libération
// Ordres mémoire
compteur.store(10, std::memory_order_relaxed); // Ordre relaxé
int val = compteur.load(std::memory_order_acquire); // Ordre d'acquisition
5. Fonctionnalités Modernes (C++11/14/17/20)
Lambdas et Fonctions
Expressions lambda et fonctionnalités avancées:
#include <functional>
// Lambda de base
auto add = [](int a, int b) { return a + b; };
int sum = add(5, 3); // 8
// Capture
int multiplier = 10;
auto multiply = [multiplier](int x) { return x * multiplier; };
int result = multiply(5); // 50
// Capture par référence
auto increment = [&multiplier]() { multiplier++; };
increment(); // multiplier = 11
// Capture par défaut
auto lambda = [=]() { return multiplier; }; // Tout par valeur
auto lambda2 = [&]() { multiplier++; }; // Tout par référence
// Lambda générique (C++14)
auto generic = [](auto x, auto y) { return x + y; };
int sum2 = generic(5, 3); // 8
double sum3 = generic(3.5, 2.5); // 6.0
// std::function pour stocker des callables
std::function<int(int, int)> func = add;
func = multiply; // Réassignation
Move Semantics et Perfect Forwarding
Optimisation des transferts d'objets:
// Constructeur de déplacement
class Buffer {
private:
int* data;
size_t size;
public:
// Constructeur normal
Buffer(size_t s) : size(s), data(new int[s]) {}
// Constructeur de copie
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// Constructeur de déplacement
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // Voler les ressources
other.size = 0;
}
// Opérateur d'affectation par déplacement
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
~Buffer() {
delete[] data;
}
};
// Perfect forwarding
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Autres Fonctionnalités Modernes
Aperçu des fonctionnalités récentes:
C++11/14:
// auto et decltype
auto x = 42;
decltype(x) y = x; // y est de type int
// Range-based for
for (const auto& item : container) { /* ... */ }
// nullptr
void* ptr = nullptr;
// Enum class
enum class Color { Red, Green, Blue };
Color c = Color::Red;
// Initialisation uniforme
std::vector<int> v{1, 2, 3};
struct Point { int x, y; };
Point p{10, 20};
C++17:
// Structured bindings
std::pair<int, std::string> p{42, "hello"};
auto [id, name] = p;
// if constexpr
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// Code pour les types entiers
} else {
// Code pour les autres types
}
}
// std::optional
std::optional<int> maybe_value;
if (maybe_value) {
int value = *maybe_value;
}
C++20:
// Concepts
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;
template <Numeric T>
T add(T a, T b) {
return a + b;
}
// Coroutines