Programmation Orientée Objet en C++ : Composition, Gestion de la Mémoire et Conteneurs Dynamiques

Simulation d'une Interface Graphique par Composition

L'utilisation de la composition permet de construire des systèmes complexes en assemblant des objets plus simples. Ici, nous modélisons une fenêtre conetnant des boutons.

// bouton.hpp
#ifndef BOUTON_HPP
#define BOUTON_HPP

#include <iostream>
#include <string>

class Bouton {
public:
    Bouton(const std::string &nom) : intitule{nom} {}
    const std::string& obtenir_nom() const { return intitule; }
    void simuler_clic() const {
        std::cout << "Action : Clic sur le bouton [" << intitule << "]" << std::endl;
    }
private:
    std::string intitule;
};

#endif
// fenetre.hpp
#ifndef FENETRE_HPP
#define FENETRE_HPP

#include <vector>
#include <string>
#include <algorithm>
#include "bouton.hpp"

class Fenetre {
public:
    Fenetre(const std::string &titre_fenetre) : titre{titre_fenetre} {
        elements.emplace_back("Quitter");
    }

    void afficher_interface() const {
        std::string separateur(30, '-');
        std::cout << separateur << "\nFenêtre : " << titre << "\n";
        for (size_t i = 0; i < elements.size(); ++i) {
            std::cout << i + 1 << ". " << elements[i].obtenir_nom() << "\n";
        }
        std::cout << separateur << std::endl;
    }

    void ajouter_bouton(const std::string &nom) {
        if (trouver_index(nom) != -1) {
            std::cerr << "Erreur : Le bouton " << nom << " existe déjà.\n";
        } else {
            elements.emplace_back(nom);
        }
    }

    void cliquer(const std::string &nom) {
        int idx = trouver_index(nom);
        if (idx != -1) {
            elements[idx].simuler_clic();
        } else {
            std::cout << "Bouton introuvable : " << nom << std::endl;
        }
    }

private:
    int trouver_index(const std::string &nom) const {
        for (size_t i = 0; i < elements.size(); ++i) {
            if (elements[i].obtenir_nom() == nom) return static_cast<int>(i);
        }
        return -1;
    }

    std::string titre;
    std::vector<Bouton> elements;
};

#endif

Analyse de la Conception

  • Composition : La classe Fenetre possède une relation de composition avec Bouton. Le cycle de vie des boutons est géré par la fenêtre.
  • Encapsulation : La méthode trouver_index est privée car elle constitue un détail d'implémentation. Si elle était publique, cela exposerait la logique interne et risquerait de fragiliser l'intégrité de l'objet.
  • Optimisation : L'utilisation de const std::string& pour le retour de fonction évite des copies inutiles, améliorant les performances tout en garantissant que la donnée ne sera pas modifiée par l'appelant.
  • Sémantique de construction : emplace_back est préférable à push_back car il construit l'objet directement dans la mémoire du vecteur, évitant la création d'un objet temporaire et son déplacement/copie.

Validation de la Copie Profonde avec std::vector

Le conteneur std::vector de la bibliothèque standard implémente nativement la copie profonde, ce qui signifie que copier un vecteur duplique également les éléments pointés en mémoire.

#include <iostream>
#include <vector>

void inspecter(const std::vector<int> &v) {
    for (const auto &val : v) std::cout << val << " ";
    std::cout << std::endl;
}

void demonstration_copie() {
    std::vector<int> original(3, 100);
    std::vector<int> copie = original; // Constructeur de recopie

    original[0] = 999;
    
    std::cout << "Original : "; inspecter(original);
    std::cout << "Copie : "; inspecter(copie);
}

Observations techniques : 1. L'utilisation de at() au lieu de [] permet une vérification des bornes (bounds checking), lançant une expection si l'indice est invalide. 2. La méthode at() doit être surchargée en version const et non-const pour permettre l'accès en lecture seule sur des objets constants tout en autorisant la modification sur des objets variables.

Implémentation d'un Conteneur Dynamique d'Entiers

Cette implémentation manuelle illustre la gestion des ressources (RAII) et l'importance du destructeur, du constructeur de recopie et de l'opérateur d'affectation.

// conteneur_entier.hpp
#ifndef CONTENEUR_ENTIER_HPP
#define CONTENEUR_ENTIER_HPP

#include <algorithm>
#include <stdexcept>

class TableauEntier {
public:
    TableauEntier() : taille{0}, donnees{nullptr} {}
    explicit TableauEntier(int n) : taille{n}, donnees{new int[n]()} {}
    
    TableauEntier(const TableauEntier &autre) : taille{autre.taille}, donnees{new int[autre.taille]} {
        std::copy_n(autre.donnees, taille, donnees);
    }

    ~TableauEntier() { delete[] donnees; }

    TableauEntier& affecter(const TableauEntier &source) {
        if (this != &source) {
            int *nouveau = new int[source.taille];
            std::copy_n(source.donnees, source.taille, nouveau);
            delete[] donnees;
            donnees = nouveau;
            taille = source.taille;
        }
        return *this;
    }

    int& at(int i) {
        if (i < 0 || i >= taille) throw std::out_of_range("Index invalide");
        return donnees[i];
    }

    const int& at(int i) const {
        return const_cast<TableauEntier*>(this)->at(i);
    }

    int get_taille() const { return taille; }

private:
    int taille;
    int *donnees;
};

#endif

Développement d'une Classe Matrice

Une matrice peut être représentée efficacement par un tableau unidimensionnel avec un calcul d'indexation (ligne * colonnes + colonne).

// matrice.hpp
class Matrice {
public:
    Matrice(int r, int c, double init = 0.0) : rows{r}, cols{c}, data{new double[r * c]} {
        std::fill_n(data, r * c, init);
    }
    
    Matrice(const Matrice &m) : rows{m.rows}, cols{m.cols}, data{new double[m.rows * m.cols]} {
        std::copy_n(m.data, rows * cols, data);
    }

    ~Matrice() { delete[] data; }

    double& operator()(int r, int c) {
        return data[r * cols + c];
    }

    void imprimer() const {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                std::cout << data[i * cols + j] << " ";
            }
            std::cout << "\n";
        }
    }

private:
    int rows, cols;
    double *data;
};

Gestion d'un Répertoire de Contacts

L'utilisation de la bibliothèque algorithmique permet de simplifier les opérations de tri et de recherche.

// repertoire.hpp
#include <vector>
#include <string>
#include <algorithm>

struct FicheContact {
    std::string nom;
    std::string tel;
    
    void afficher() const {
        std::cout << nom << " : " << tel << std::endl;
    }
};

class Repertoire {
public:
    void ajouter(const std::string &n, const std::string &t) {
        if (trouver(n) == contacts.end()) {
            contacts.push_back({n, t});
            trier();
        }
    }

    void supprimer(const std::string &n) {
        auto it = trouver(n);
        if (it != contacts.end()) contacts.erase(it);
    }

    void lister() const {
        for (const auto &c : contacts) c.afficher();
    }

private:
    std::vector<FicheContact>::iterator trouver(const std::string &n) {
        return std::find_if(contacts.begin(), contacts.end(), [&](const FicheContact &c) {
            return c.nom == n;
        });
    }

    void trier() {
        std::sort(contacts.begin(), contacts.end(), [](const FicheContact &a, const FicheContact &b) {
            return a.nom < b.nom;
        });
    }

    std::vector<FicheContact> contacts;
};

Étiquettes: cpp OOP memory-management STL composition

Publié le 5 juillet à 21h49