Concepts Avancés en C++: Classes, Vecteurs et Gestion de Mémoire

Simulation d'Interface Utilisateur (GUI)

Cette section explore la conception de classes pour simuler des éléments d'interface utilisateur graphiques (GUI) simples, comme des boutons et des fenêtres, en utilisant la composition.

Classe Bouton

Un bouton est un élément interactif qui affiche un texte et exécute une action lorsqu'il est cliqué. Voici sa définition et son implémentation.


#pragma once
#include <iostream>
#include <string>

// Utilisation explicite des alias pour std::string et std::cout
using std::string;
using std::cout;

// Déclaration de la classe Bouton
class Bouton {
public:
    // Constructeur: initialise le bouton avec un libellé
    Bouton(const string &texte_bouton);
    // Accesseur constant pour le libellé
    string obtenir_libelle() const;
    // Méthode pour simuler un clic sur le bouton
    void cliquer();
private:
    string libelle; // Texte affiché sur le bouton
};

// Implémentation du constructeur
Bouton::Bouton(const string &texte_bouton) : libelle{texte_bouton} {}

// Implémentation de l'accesseur inline pour le libellé
inline string Bouton::obtenir_libelle() const {
    return libelle;
}

// Implémentation de la méthode cliquer
void Bouton::cliquer() {
    cout << "Le bouton '" << libelle << "' a été cliqué.\n";
}

Classe Fenêtre

La classe Fenetre représente une fenêtre d'application et peut contenir plusieurs boutons. Elle démontre le principe de composition, où une fenêtre est composée d'un titre (string) et d'une collection de boutons (std::vector<Bouton>).


#pragma once
#include "bouton.hpp" // Inclusion de la définition de la classe Bouton
#include <vector>
#include <iostream>

// Utilisation explicite des alias pour std::vector, std::cout et std::endl
using std::vector;
using std::cout;
using std::endl;
using std::string; // Nécessaire pour le titre de la fenêtre

// Déclaration de la classe Fenetre
class Fenetre {
public:
    // Constructeur: initialise la fenêtre avec un titre
    Fenetre(const string &titre_fenetre);
    // Affiche les propriétés de la fenêtre (titre et boutons)
    void afficher_details() const;
    // Simule la fermeture de la fenêtre
    void fermer();
    // Ajoute un nouveau bouton à la fenêtre
    void ajouter_bouton(const string &texte_bouton);
private:
    string titre_fenetre; // Le titre de la fenêtre
    vector<Bouton> boutons_associes; // Collection de boutons dans la fenêtre
};

// Implémentation du constructeur
Fenetre::Fenetre(const string &titre_fenetre_init) : titre_fenetre{titre_fenetre_init} {
    // Chaque fenêtre commence avec un bouton "Fermer" par défaut
    boutons_associes.push_back(Bouton("Fermer"));
}

// Implémentation de la méthode afficher_details
inline void Fenetre::afficher_details() const {
    string separateur(40, '-'); // Création d'une ligne de séparation
    cout << separateur << endl;
    cout << "Titre de la fenêtre: " << titre_fenetre << endl;
    cout << "Nombre de boutons: " << boutons_associes.size() << endl;
    cout << "Boutons disponibles: " << endl;
    for(const auto &btn : boutons_associes)
        cout << " - " << btn.obtenir_libelle() << endl;
    cout << separateur << endl;
}

// Implémentation de la méthode fermer
void Fenetre::fermer() {
    cout << "Fermeture de la fenêtre '" << titre_fenetre << "'" << endl;
    // Simule le clic sur le premier bouton (supposé être "Fermer")
    if (!boutons_associes.empty()) {
        boutons_associes.at(0).cliquer();
    }
}

// Implémentation de la méthode ajouter_bouton
void Fenetre::ajouter_bouton(const string &texte_bouton) {
    boutons_associes.push_back(Bouton(texte_bouton));
}

Utilisation des Classes Fenêtre et Bouton

Un exemple d'utilisation de ces classes pour simuler une interaction simple.


#include "fenetre.hpp" // Inclut la classe Fenetre et, par extension, Bouton
#include <iostream>

// Alias pour std::cout
using std::cout;

void executer_simulation_gui() {
    // Création d'une nouvelle fenêtre
    Fenetre maFenetre("Nouvelle Application");
    // Ajout d'un bouton "Maximiser"
    maFenetre.ajouter_bouton("Maximiser");
    // Affichage des détails de la fenêtre
    maFenetre.afficher_details();
    // Simulation de la fermeture de la fenêtre
    maFenetre.fermer();
}

int main() {
    cout << "Démonstration d'une GUI simplifiée avec composition de classes:\n";
    executer_simulation_gui();
    return 0;
}

Classes personnalisées: Les classes Bouton et Fenetre sont des abstractions personnalisées pour les éléments GUI.

Classes de la bibliothèque standard: std::string pour les textes, std::vector pour gérer les collections de boutons, et std::cout/std::endl pour les sorties console sont des exemples clés.

Relation de composition: Une Fenetre est composée d'une std::string (son titre) et d'un std::vector<Bouton>. Chaque Bouton est lui-même composé d'une std::string (son libellé).

Fonctions membres const et inline:

  • Les méthodes qui ne modifient pas l'état interne de l'objet devraient être déclarées const (ex: Bouton::obtenir_libelle(), Fenetre::afficher_details(), Fenetre::fermer()). Bien que Fenetre::fermer() appelle Bouton::cliquer() qui a un effet (sortie console), la fenêtre elle-même ne change pas d'état interne.
  • Les fonctions courtes et fréquemment appelées, comme Bouton::obtenir_libelle() ou Fenetre::afficher_details() (si la logique est simple), peuvent être déclarées inline pour potentiellement améliorer les performances en demandant au compilateur d'insérer le code de la fonction directement au point d'appel, évitant ainsi le surcoût d'un appel de fonction traditionnel.

La ligne de séparation (string s(40, '*') ou string separateur(40, '-')) est utilisée pour améliorer la lisibilité de la sortie console en encadrant visuellement les informations de la fenêtre.

Gestion de Collections avec std::vector

Cette section explore les comportements de copie et de modification de std::vector, notamment l'implication de la sémantique de copie profonde.


#include <iostream>
#include <vector>
#include <string> // Nécessaire pour \\b

// Alias pour le namespace standard
using namespace std;

// Fonction utilitaire pour afficher un vecteur d'entiers
void afficher_vecteur(const vector<int> &data_vector) {
    for(int val : data_vector)
        cout << val << ", ";
    // Supprime les deux derniers caractères (", ") et ajoute un retour à la ligne
    cout << "\b\b \n";
}

// Fonction utilitaire pour afficher un vecteur de vecteurs d'entiers
void afficher_matrice_vecteurs(const vector<vector<int>> &matrice_vecteurs) {
    for(const auto &ligne : matrice_vecteurs) {
        for(int val : ligne)
            cout << val << ", ";
        // Supprime les deux derniers caractères (", ") et ajoute un retour à la ligne
        cout << "\b\b \n";
    }
}

// Premier scénario de test: copie de vecteur simple
void test_copie_vecteur_simple() {
    // Initialise un vecteur de 5 entiers, tous à 42
    vector<int> liste_un(5, 42);
    // Crée une copie constante de liste_un. std::vector effectue une copie profonde.
    const vector<int> liste_deux(liste_un);
    // Modifie le premier élément de liste_un
    liste_un.at(0) = -999;

    cout << "Liste Un: "; afficher_vecteur(liste_un);
    cout << "Liste Deux: "; afficher_vecteur(liste_deux);
    cout << "liste_un.at(0) = " << liste_un.at(0) << endl;
    cout << "liste_deux.at(0) = " << liste_deux.at(0) << endl;
}

// Deuxième scénario de test: copie de vecteur imbriqué
void test_copie_vecteur_imbrique() {
    // Initialise un vecteur de vecteurs d'entiers
    vector<vector<int>> matrice_un{{1, 2, 3}, {4, 5, 6, 7}};
    // Crée une copie constante de matrice_un. Copie profonde s'applique aux vecteurs internes.
    const vector<vector<int>> matrice_deux(matrice_un);
    // Ajoute un élément au premier vecteur interne de matrice_un
    matrice_un.at(0).push_back(-999);

    cout << "Matrice Un:\n"; afficher_matrice_vecteurs(matrice_un);
    cout << "Matrice Deux:\n"; afficher_matrice_vecteurs(matrice_deux);

    // Accès aux derniers éléments des vecteurs internes
    vector<int> temp_un = matrice_un.at(0);
    cout << "Dernier élément de matrice_un[0]: " << temp_un.at(temp_un.size()-1) << endl;
    const vector<int> temp_deux = matrice_deux.at(0);
    cout << "Dernier élément de matrice_deux[0]: " << temp_deux.at(temp_deux.size()-1) << endl;
}

int main() {
    cout << "--- Scénario de test 1: Copie de vecteurs simples ---\n";
    test_copie_vecteur_simple();
    cout << "\n--- Scénario de test 2: Copie de vecteurs imbriqués ---\n";
    test_copie_vecteur_imbrique();
    return 0;
}

Analyse du Scénario 1:

  • vector<int> liste_un(5, 42);: Initialise liste_un avec cinq éléments, tous égaux à 42.
  • const vector<int> liste_deux(liste_un);: Crée liste_deux comme une copie de liste_un. Le constructeur de copie de std::vector effectue une copie profonde, ce qui signifie que liste_deux obtient sa propre copie des données.
  • liste_un.at(0) = -999;: Modifie le premier élément de liste_un. Puisque liste_deux est une copie indépendante, cette modification n'affecte pas liste_deux.

Analyse du Scénario 2:

  • vector<vector<int>> matrice_un{{1, 2, 3}, {4, 5, 6, 7}};: Crée une matrice (vecteur de vecteurs) avec deux sous-vecteurs.
  • const vector<vector<int>> matrice_deux(matrice_un);: Crée matrice_deux comme une copie profonde de matrice_un. Cela signifie que non seulement le vecteur externe est copié, mais aussi chacun de ses sous-vecteurs est copié individuellement.
  • matrice_un.at(0).push_back(-999);: Ajoute un élément au premier sous-vecteur de matrice_un. Cette modification n'impacte pas matrice_deux car le sous-vecteur original a été copié en profondeur.

Observations clés:

  1. Le constructeur de copie de std::vector implémente une copie profonde. Cela garantit que chaque instance de vecteur possède sa propre copie de ses éléments, ce qui les rend indépendants les uns des autres après la copie.
  2. La méthode at() de std::vector est fournie en deux versions: une version non-const qui retourne une référence modifiable (T&) et une version const qui retourne une référence constante (const T&). C'est essentiel pour permettre à la fois la modification des éléments (via des objets non-const) et un accès en lecture seule (via des objets const), respectant ainsi la const-correctness.

Implémentation d'un Vecteur Dynamique Personnalisé

Cette section présente l'implémentation d'une classe de tableau dynamique d'entiers, VecEntier, gérant explicitement la mémoire dynamique. Cela permet de comprendre les mécanismes de base des conteneurs comme std::vector, en se concentrant sur les constructeurs, le destructeur, le constructeur de copie et l'opérateur d'affectation.


#pragma once
#include <iostream>
#include <cassert> // Pour les assertions

// Alias pour std::cout et std::endl
using std::cout;
using std::endl;

// Déclaration de la classe VecEntier
class VecEntier {
public:
    // Constructeur: spécifie la taille du tableau
    VecEntier(int taille_initiale);
    // Constructeur: spécifie la taille et initialise tous les éléments
    VecEntier(int taille_initiale, int valeur_defaut);
    // Constructeur de copie: effectue une copie profonde
    VecEntier(const VecEntier &autre_vec);
    // Destructeur: libère la mémoire allouée dynamiquement
    ~VecEntier();

    // Accesseur/modificateur: retourne une référence à l'élément à l'index donné (non-const)
    int& obtenir_element(int index);
    // Accesseur: retourne une référence constante à l'élément à l'index donné (const)
    const int& obtenir_element(int index) const;
    // Opérateur d'affectation: gère l'affectation entre objets VecEntier (copie profonde)
    VecEntier& operator=(const VecEntier &source);
    // Accesseur constant: retourne la taille du tableau
    int obtenir_taille() const;

private:
    int taille; // Taille actuelle du tableau
    int *donnees; // Pointeur vers le tableau d'entiers alloué dynamiquement
};

// Implémentation des constructeurs
VecEntier::VecEntier(int taille_initiale) : taille{taille_initiale}, donnees{new int[taille_initiale]} {
    // Aucune initialisation des valeurs par défaut ici
}

VecEntier::VecEntier(int taille_initiale, int valeur_defaut) : taille{taille_initiale}, donnees{new int[taille_initiale]} {
    for (int i = 0; i < taille; ++i)
        donnees[i] = valeur_defaut;
}

// Implémentation du constructeur de copie (copie profonde)
VecEntier::VecEntier(const VecEntier &autre_vec) : taille{autre_vec.taille}, donnees{new int[autre_vec.taille]} {
    for (int i = 0; i < taille; ++i)
        donnees[i] = autre_vec.donnees[i]; // Copie chaque élément
}

// Implémentation du destructeur
VecEntier::~VecEntier() {
    delete[] donnees; // Libère le tableau alloué sur le tas
}

// Implémentation de la version const de obtenir_element
const int& VecEntier::obtenir_element(int index) const {
    assert(index >= 0 && index < taille && "Index hors limites (const)");
    return donnees[index];
}

// Implémentation de la version non-const de obtenir_element
int& VecEntier::obtenir_element(int index) {
    assert(index >= 0 && index < taille && "Index hors limites (non-const)");
    return donnees[index];
}

// Implémentation de l'opérateur d'affectation (gestion de la copie profonde et auto-affectation)
VecEntier& VecEntier::operator=(const VecEntier &source) {
    if (this == &source) { // Gérer l'auto-affectation
        return *this;
    }
    delete[] donnees; // Libérer l'ancienne mémoire
    taille = source.taille;
    donnees = new int[taille]; // Allouer une nouvelle mémoire
    for (int i = 0; i < taille; ++i)
        donnees[i] = source.donnees[i]; // Copier les éléments
    return *this; // Retourner une référence à l'objet courant pour permettre le chaînage
}

// Implémentation de l'accesseur de taille
int VecEntier::obtenir_taille() const {
    return taille;
}

Tests de la Classe VecEntier

Démonstration des fonctionnalités de VecEntier, y compris la copie et l'affectation.


#include "VecEntier.hpp" // Inclut la définition de VecEntier
#include <iostream>

// Alias pour std::cin et std::cout
using std::cin;
using std::cout;
using std::endl;

// Fonction utilitaire pour afficher les éléments d'un VecEntier
void afficher_vec_entier(const VecEntier &v_int) {
    for (int i = 0; i < v_int.obtenir_taille(); ++i)
        cout << v_int.obtenir_element(i) << ", ";
    cout << "\b\b \n"; // Supprime le dernier ", " et ajoute un retour à la ligne
}

// Premier scénario de test: constructeurs et copie
void test_vec_entier_constructeurs_copie() {
    int dim;
    cout << "Entrez une dimension n: ";
    cin >> dim;

    // Création d'un VecEntier de taille n, non initialisé
    VecEntier v_un(dim);
    for (int i = 0; i < dim; ++i)
        v_un.obtenir_element(i) = i * i; // Initialisation des données
    cout << "V_Un: "; afficher_vec_entier(v_un);

    // Création d'un VecEntier de taille n, tous les éléments à 42
    VecEntier v_deux(dim, 42);
    // Création de v_trois en utilisant le constructeur de copie de v_deux
    VecEntier v_trois(v_deux);

    v_deux.obtenir_element(0) = -999; // Modification du premier élément de v_deux
    cout << "V_Deux: "; afficher_vec_entier(v_deux);
    cout << "V_Trois: "; afficher_vec_entier(v_trois); // V_trois doit rester inchangé
}

// Deuxième scénario de test: opérateur d'affectation
void test_vec_entier_affectation() {
    // Création d'un VecEntier constant
    const VecEntier v_ref(5, 42);
    // Création d'un autre VecEntier avec une taille différente
    VecEntier v_cible(10, 0);
    cout << "V_Cible avant affectation: "; afficher_vec_entier(v_cible);

    v_cible = v_ref; // Appel de l'opérateur d'affectation
    cout << "V_Cible après affectation: "; afficher_vec_entier(v_cible);
    cout << "V_Ref.obtenir_element(0) = " << v_ref.obtenir_element(0) << endl;
    cout << "V_Cible.obtenir_element(0) = " << v_cible.obtenir_element(0) << endl;
}

int main() {
    cout << "--- Test 1: Constructeurs et constructeur de copie ---\n";
    test_vec_entier_constructeurs_copie();
    cout << "\n--- Test 2: Opérateur d'affectation ---\n";
    test_vec_entier_affectation();
    return 0;
}

Analyse du Constructeur de Copie: Le consrtucteur de copie de VecEntier réalise une copie profonde. Il alloue un nouveau bloc de mémoire pour l'objet cible et copie les éléments un par un depuis l'objet source. Cela garantit que les deux objets ont leurs propres données indépendantes, évitant les problèmes de double libération de mémoire ou de modifications inattendues si une copie peu profonde était utilisée.

Remarque sur les retours par référence (int& et const int&):

  • Les fonctions int& obtenir_element(int index) et const int& obtenir_element(int index) const retournent des références. La version non-const permet de modifier l'élément (par ex., monVec.obtenir_element(0) = 10;), tandis que la version const permet uniquement un accès en lecture (par ex., cout << monConstVec.obtenir_element(0);).
  • Si obtenir_element retournait par valeur (int au lieu de int&), il serait impossible de modifier les éléments du tableau via cette fonction, car on modifierait une copie temporaire de la valeur, et non l'élément original de l'objet. Ce serait une erreur de conception pour un accesseur modifiable.

Retour de l'opérateur d'affectasion (VecEntier& operator=): L'opérateur d'affectation retourne une référence à l'objet courant (*this). Retourner par référence permet le chaînage des affectations (par ex., v1 = v2 = v3;). Si la fonction retournait par valeur (VecEntier au lieu de VecEntier&), cela provoquerait la création d'une copie temporaire de l'objet à chaque affectation, ce qui entraînerait un surcoût inutile et empêcherait le chaînage efficace.

Conception d'une Classe Matrice

Cette section se concentre sur l'implémentation d'une classe Matrice en C++ pour gérer des matrices de nombres à virgule flottante, avec une gestion manuelle de la mémoire et la prise en charge des opérations de base comme la construction, la copie et l'accès aux éléments.


#pragma once
#include <iostream>
#include <cassert> // Pour les assertions

// Alias pour std::cout et std::endl
using std::cout;
using std::endl;

// Déclaration de la classe Matrice
class Matrice {
public:
    // Constructeur: crée une matrice de taille n*m
    Matrice(int n_lignes, int m_colonnes);
    // Constructeur: crée une matrice carrée de taille n*n
    Matrice(int dimension);
    // Constructeur de copie: effectue une copie profonde
    Matrice(const Matrice& autre_matrice);
    // Destructeur: libère la mémoire allouée
    ~Matrice();

    // Définit les éléments de la matrice à partir d'un tableau de doubles
    void definir_elements(const double* valeurs_source);
    // Met tous les éléments de la matrice à zéro
    void effacer();

    // Accesseur constant: retourne une référence constante à l'élément (i, j)
    const double& obtenir_element(int ligne, int colonne) const;
    // Accesseur modifiable: retourne une référence à l'élément (i, j)
    double& obtenir_element(int ligne, int colonne);

    // Accesseur constant: retourne le nombre de lignes
    int obtenir_lignes() const;
    // Accesseur constant: retourne le nombre de colonnes
    int obtenir_colonnes() const;

    // Affiche les éléments de la matrice ligne par ligne
    void afficher_matrice() const;

private:
    int lignes;     // Nombre de lignes
    int colonnes;   // Nombre de colonnes
    double* elements; // Pointeur vers les éléments du tableau 1D
};

// Implémentation des constructeurs
Matrice::Matrice(int n_lignes, int m_colonnes) : lignes{n_lignes}, colonnes{m_colonnes}, elements{new double[n_lignes * m_colonnes]} {}
Matrice::Matrice(int dimension) : lignes{dimension}, colonnes{dimension}, elements{new double[dimension * dimension]} {}

// Implémentation du constructeur de copie (copie profonde)
Matrice::Matrice(const Matrice& autre_matrice) 
    : lignes{autre_matrice.lignes}, colonnes{autre_matrice.colonnes}, elements{new double[lignes * colonnes]} {
    for (int i = 0; i < lignes * colonnes; ++i) {
        elements[i] = autre_matrice.elements[i];
    }
}

// Implémentation du destructeur
Matrice::~Matrice() {
    delete []elements;     
}

// Implémentation de definir_elements
void Matrice::definir_elements(const double* valeurs_source) {
    for (int i = 0; i < lignes; ++i) {
        for (int j = 0; j < colonnes; ++j) {
            elements[i * colonnes + j] = valeurs_source[i * colonnes + j];
        }
    }
}

// Implémentation de effacer
void Matrice::effacer() {
    for (int i = 0; i < lignes * colonnes; ++i) {
        elements[i] = 0.0;
    }
}

// Implémentation de obtenir_element (version const)
const double& Matrice::obtenir_element(int ligne, int colonne) const {
    assert(ligne >= 0 && ligne < lignes && "Indice de ligne hors limites (const)");
    assert(colonne >= 0 && colonne < colonnes && "Indice de colonne hors limites (const)");
    return elements[ligne * colonnes + colonne];
}

// Implémentation de obtenir_element (version non-const)
double& Matrice::obtenir_element(int ligne, int colonne) {
    assert(ligne >= 0 && ligne < lignes && "Indice de ligne hors limites (non-const)");
    assert(colonne >= 0 && colonne < colonnes && "Indice de colonne hors limites (non-const)");
    return elements[ligne * colonnes + colonne];
}

// Implémentation des accesseurs de dimensions
int Matrice::obtenir_lignes() const { return lignes; }
int Matrice::obtenir_colonnes() const { return colonnes; }

// Implémentation de afficher_matrice
void Matrice::afficher_matrice() const {
    for (int i = 0; i < lignes; ++i) {
        for (int j = 0; j < colonnes; ++j) {
            cout << elements[i * colonnes + j] << ", ";
        }
        cout << "\b\b \n"; // Supprime les deux derniers caractères (", ") et ajoute un retour à la ligne
    }
}

Tests de la Classe Matrice

Exemples d'utilisation de la classe Matrice.


#include "matrice.hpp" // Inclut la définition de Matrice
#include <iostream>
#include <cassert>

// Alias pour std::cin, std::cout, std::endl
using std::cin;
using std::cout;
using std::endl;

// Fonction utilitaire pour afficher une ligne spécifique de la matrice
void afficher_ligne_matrice(const Matrice &m, int indice_ligne) {
    assert(indice_ligne >= 0 && indice_ligne < m.obtenir_lignes() && "Indice de ligne invalide pour l'affichage.");

    for(int j = 0; j < m.obtenir_colonnes(); ++j)
        cout << m.obtenir_element(indice_ligne, j) << ", ";
    cout << "\b\b \n";
}

// Premier scénario de test: constructeurs et initialisation
void test_matrice_constructeurs() {
    double tableau_initial[] = {1.0, 3.0, 5.0, 2.0, 4.0, 6.0, 7.0, 8.0, 9.0};

    int n_lignes, m_colonnes;
    cout << "Entrez le nombre de lignes (n) et de colonnes (m): ";
    cin >> n_lignes >> m_colonnes;

    // Crée une matrice m1 de taille n_lignes x m_colonnes
    Matrice m_un(n_lignes, m_colonnes);
    m_un.definir_elements(tableau_initial); // Initialise avec le tableau

    // Crée une matrice m2 de taille m_colonnes x n_lignes
    Matrice m_deux(m_colonnes, n_lignes);
    m_deux.definir_elements(tableau_initial);

    // Crée une matrice carrée m3 de taille 2x2
    Matrice m_trois(2);
    m_trois.definir_elements(tableau_initial);

    cout << "Matrice M_Un: \n";   m_un.afficher_matrice();  cout << endl;
    cout << "Matrice M_Deux: \n";   m_deux.afficher_matrice();  cout << endl;
    cout << "Matrice M_Trois: \n";   m_trois.afficher_matrice();  cout << endl;
}

// Deuxième scénario de test: copie et modifications
void test_matrice_copie_modification() {
    Matrice m_source(2, 3);
    m_source.clear(); // Met tous les éléments à zéro
    
    // Crée une copie constante de m_source
    const Matrice m_copie(m_source);
    // Modifie un élément de m_source
    m_source.obtenir_element(0, 0) = -999.0;

    cout << "m_source.obtenir_element(0, 0) = " << m_source.obtenir_element(0, 0) << endl;
    cout << "m_copie.obtenir_element(0, 0) = " << m_copie.obtenir_element(0, 0) << endl;
    cout << "Ligne 0 de M_Source: "; afficher_ligne_matrice(m_source, 0);
    cout << "Ligne 0 de M_Copie: "; afficher_ligne_matrice(m_copie, 0);
}

int main() {
    cout << "--- Scénario de test 1: Constructeurs et initialisation ---\n";
    test_matrice_constructeurs();

    cout << "\n--- Scénario de test 2: Copie et modifications ---\n";
    test_matrice_copie_modification();
    return 0;
}

Gestion des Utilisateurs

Cette section présente une classe Utilisateur pour gérer des informations de base comme le nom, le mot de passe et l'adresse e-mail. Elle inclut des fonctionnalités pour définir l'e-mail, changer le mot de passe et afficher les détails de l'utilisateur.


#pragma once
#include <iostream>
#include <string>
#include <vector> // Non directement utilisé dans cette U.H., mais utile pour l'application cliente

// Alias pour std::string, std::cout, std::endl
using std::string;
using std::cout;
using std::endl;
using std::vector; // Inclus pour être cohérent avec l'original, bien que non strictement nécessaire ici.

// Déclaration de la classe Utilisateur
class Utilisateur {
public:
    // Constructeur: nom, mot de passe (par défaut "123456"), e-mail (par défaut vide)
    Utilisateur(const string& nom_utilisateur, const string& mot_de_passe = "123456", const string& adresse_email = "");
    // Méthode pour définir l'adresse e-mail de l'utilisateur
    void definir_email();
    // Méthode pour changer le mot de passe
    void changer_mot_de_passe();
    // Méthode pour afficher les détails de l'utilisateur
    void afficher_details();
private:
    string nom;
    string motDePasse;
    string email;
};

// Implémentation du constructeur
Utilisateur::Utilisateur(const string& nom_utilisateur, const string& pass_initial, const string& email_initial)
    : nom{nom_utilisateur}, motDePasse{pass_initial}, email{email_initial} {}

// Implémentation de definir_email
void Utilisateur::definir_email() {
    string saisie_email;
    bool email_valide = false;
    cout << "Entrez l'adresse email: ";
    do {
        std::cin >> saisie_email;
        // Vérifie la présence du caractère '@' pour une validation simple
        size_t pos_arobase = saisie_email.find('@');
        if (pos_arobase != string::npos && pos_arobase > 0 && pos_arobase < saisie_email.length() - 1) {
            email_valide = true;
        } else {
            cout << "Email invalide. Veuillez ré-entrer l'email: ";
        }
    } while (!email_valide);
    email = saisie_email;
    cout << "Email défini avec succès." << endl;
}

// Implémentation de changer_mot_de_passe
void Utilisateur::changer_mot_de_passe() {
    string ancien_mot_de_passe;
    string nouveau_mot_de_passe;
    int tentatives = 0;
    bool ancien_correct = false;
    cout << "Entrez l'ancien mot de passe: ";
    do {
        std::cin >> ancien_mot_de_passe;
        if (ancien_mot_de_passe == motDePasse) {
            ancien_correct = true;
        } else {
            tentatives++;
            if (tentatives >= 3) {
                cout << "Trop de tentatives échouées. Veuillez réessayer plus tard." << endl;
                return; // Quitte la fonction après 3 échecs
            }
            cout << "Ancien mot de passe incorrect. Veuillez ré-entrer: ";
        }
    } while (!ancien_correct);

    cout << "Entrez le nouveau mot de passe: ";
    std::cin >> nouveau_mot_de_passe;
    motDePasse = nouveau_mot_de_passe;
    cout << "Nouveau mot de passe défini avec succès." << endl;
}

// Implémentation de afficher_details
void Utilisateur::afficher_details() {
    cout << "Nom: " << nom << endl;
    // Cache le mot de passe avec des astérisques
    string motDePasseCache(motDePasse.length(), '*');
    cout << "Mot de passe: " << motDePasseCache << endl;
    cout << "Email: " << (email.empty() ? "Non défini" : email) << endl;
}

Tests de la Classe Utilisateur

Démonstration de la création et de l'interaction avec des objets Utilisateur.


#include "utilisateur.hpp" // Inclut la définition de Utilisateur
#include <iostream>
#include <vector>
#include <string>

// Alias pour std::cin, std::cout, std::endl, std::vector, std::string
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

void executer_test_utilisateurs() {
    vector<Utilisateur> liste_utilisateurs;

    // Création et ajout d'un premier utilisateur avec tous les détails
    Utilisateur u_alice("Alice", "PassAlice", "alice@example.com");
    liste_utilisateurs.push_back(u_alice);
    cout << endl;

    // Création d'un deuxième utilisateur avec mot de passe par défaut, puis mise à jour
    Utilisateur u_bob("Bob");
    u_bob.definir_email();
    u_bob.changer_mot_de_passe();
    liste_utilisateurs.push_back(u_bob);
    cout << endl;
       
    // Création d'un troisième utilisateur, puis mise à jour
    Utilisateur u_carol("Carol");
    u_carol.definir_email();
    u_carol.changer_mot_de_passe();
    liste_utilisateurs.push_back(u_carol);
    cout << endl;

    cout << "Il y a " << liste_utilisateurs.size() << " utilisateurs enregistrés:\n";
    for(auto &utilisateur_actuel : liste_utilisateurs) {
        utilisateur_actuel.afficher_details();
        cout << endl;
    }
}

int main() {
    executer_test_utilisateurs();
    return 0;
}

Système de Compte Bancaire

Cette section modélise un système de compte d'épargne basique, incluant la gestion des dates, des dépôts, des retraits et du calcul d'intérêts. Elle met en évidence l'utilisation de plusieurs classes interdépendantes et de membres statiques.

Classe Date

La classe Date gère les dates et fournit des utilitaires pour les valider, calculer le nombre de jours entre deux dates et vérifier les années bissextiles.


#pragma once
// Déclaration de la classe Date
class Date {
private:
    int annee;
    int mois;
    int jour;
    int jours_totaux; // Nombre total de jours depuis une référence (ex: 0000-01-01)
public:
    Date(int an, int mo, int jo); // Constructeur
    int obtenir_annee() const { return annee; }
    int obtenir_mois() const { return mois; }
    int obtenir_jour() const { return jour; }
    int obtenir_max_jour() const; // Retourne le nombre maximum de jours pour le mois actuel
    bool est_annee_bissextile() const {
        return (annee % 4 == 0 && annee % 100 != 0) || (annee % 400 == 0);
    }
    void afficher_date() const; // Affiche la date au format AAAA-MM-JJ
    // Calcule la distance en jours entre cette date et une autre date
    int distance_en_jours(const Date& autre_date) const {
        return jours_totaux - autre_date.jours_totaux;
    }
};


#include "date.h" // Inclut la déclaration de la classe Date
#include <iostream>
#include <cstdlib> // Pour exit()

// Alias pour std::cout, std::endl
using std::cout;
using std::endl;

// Tableau des jours cumulés avant chaque mois (pour une année non bissextile)
namespace { // Namespace anonyme pour limiter la visibilité
    const int JOURS_AVANT_MOIS[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
}

// Implémentation du constructeur de Date
Date::Date(int an, int mo, int jo) : annee(an), mois(mo), jour(jo) {
    // Validation de la date
    if (jour <= 0 || jour > obtenir_max_jour()) {
        cout << "Date invalide: ";
        afficher_date();
        cout << endl;
        exit(1); // Arrête le programme en cas de date invalide
    }
    
    // Calcul des jours_totaux (simplifié pour cet exemple)
    int annees_completes = annee - 1;
    jours_totaux = annees_completes * 365 + annees_completes / 4 - annees_completes / 100 + annees_completes / 400;
    jours_totaux += JOURS_AVANT_MOIS[mois - 1] + jour;
    if (est_annee_bissextile() && mois > 2) {
        jours_totaux++;
    }
}

// Implémentation de obtenir_max_jour
int Date::obtenir_max_jour() const {
    if (est_annee_bissextile() && mois == 2)
        return 29;
    else if (mois >= 1 && mois <= 12) // Vérification de la validité du mois
        return JOURS_AVANT_MOIS[mois] - JOURS_AVANT_MOIS[mois - 1];
    else // Mois invalide
        return 0; // Ou lancer une exception
}

// Implémentation de afficher_date
void Date::afficher_date() const {
    cout << obtenir_annee() << "-" << obtenir_mois() << "-" << obtenir_jour();
}

Classe CompteEpargne

La classe CompteEpargne représente un compte bancaire avec un solde, un taux d'intérêt et un historique des transactions. Elle utilise la classe Date pour la gestion temporelle et un membre statique pour suivre le solde total de tous les comptes.


#pragma once
#include "date.h" // Inclut la définition de la classe Date
#include <string>

// Déclaration de la classe CompteEpargne
class CompteEpargne {
private:
    std::string identifiant;
    double solde;
    double taux_interet;
    Date derniere_date; // Dernière date d'opération enregistrée
    double accumulation_interets; // Accumulation pour le calcul des intérêts
    static double solde_total_global; // Solde total de tous les comptes

    // Méthode privée pour enregistrer une transaction
    void enregistrer_transaction(const Date& date_op, double montant, const std::string& description);
    // Méthode privée pour afficher un message d'erreur
    void signaler_erreur(const std::string& message_erreur) const;
    // Calcule l'accumulation des intérêts jusqu'à une certaine date
    double calculer_accumulation(const Date& date_actuelle) const {
        return accumulation_interets + solde * date_actuelle.distance_en_jours(derniere_date);
    }

public:
    // Constructeur
    CompteEpargne(const Date& date_creation, const std::string& id_compte, double taux);
    // Accesseurs constants
    const std::string& obtenir_identifiant() const { return identifiant; }
    double obtenir_solde() const { return solde; }
    double obtenir_taux_interet() const { return taux_interet; }
    // Accesseur statique pour le solde total global
    static double obtenir_solde_total_global() { return solde_total_global; }

    // Méthodes publiques pour les opérations bancaires
    void deposer(const Date& date_op, double montant, const std::string& description);
    void retirer(const Date& date_op, double montant, const std::string& description);
    void regler_interets(const Date& date_op); // Règle les intérêts cumulés
    void afficher_infos() const; // Affiche les informations du compte
};


#include "account.h" // Inclut la déclaration de la classe CompteEpargne
#include <cmath>     // Pour floor
#include <iostream>  // Pour cout

// Alias pour std::cout, std::endl, std::string
using std::cout;
using std::endl;
using std::string;

// Initialisation du membre statique
double CompteEpargne::solde_total_global = 0;

// Implémentation du constructeur de CompteEpargne
CompteEpargne::CompteEpargne(const Date& date_creation, const string& id_compte, double taux)
    : identifiant(id_compte), solde(0), taux_interet(taux), derniere_date(date_creation), accumulation_interets(0) {
    date_creation.afficher_date();
    cout << "\t#" << identifiant << " créé" << endl;
}

// Implémentation de enregistrer_transaction
void CompteEpargne::enregistrer_transaction(const Date& date_op, double montant, const string& description) {
    accumulation_interets = calculer_accumulation(date_op); // Met à jour l'accumulation
    derniere_date = date_op; // Met à jour la dernière date d'opération

    // Arrondit le montant à deux décimales pour la précision monétaire
    montant = floor(montant * 100 + 0.5) / 100;
    solde += montant;
    solde_total_global += montant; // Met à jour le solde total global

    date_op.afficher_date();
    cout << "\t#" << identifiant << "\t" << montant << "\t" << solde << "\t" << description << endl;
}

// Implémentation de signaler_erreur
void CompteEpargne::signaler_erreur(const string& message_erreur) const {
    cout << "Erreur (compte #" << identifiant << "): " << message_erreur << endl;
}

// Implémentation de deposer
void CompteEpargne::deposer(const Date& date_op, double montant, const string& description) {
    if (montant <= 0) {
        signaler_erreur("Le montant du dépôt doit être positif.");
        return;
    }
    enregistrer_transaction(date_op, montant, description);
}

// Implémentation de retirer
void CompteEpargne::retirer(const Date& date_op, double montant, const string& description) {
    if (montant <= 0) {
        signaler_erreur("Le montant du retrait doit être positif.");
        return;
    }
    if (montant > obtenir_solde()) {
        signaler_erreur("Fonds insuffisants.");
    } else {
        enregistrer_transaction(date_op, -montant, description); // Montant négatif pour un retrait
    }
}

// Implémentation de regler_interets
void CompteEpargne::regler_interets(const Date& date_op) {
    // Calcul de l'intérêt annuel basé sur l'accumulation
    // Le diviseur représente le nombre de jours dans une année (simplifié à 365)
    double interet = calculer_accumulation(date_op) * taux_interet / 365.0; // Taux journalier
    
    if (interet != 0) {
        enregistrer_transaction(date_op, interet, "Intérêts");
    }
    accumulation_interets = 0; // Réinitialise l'accumulation après le règlement
}

// Implémentation de afficher_infos
void CompteEpargne::afficher_infos() const {
    cout << "Compte #" << identifiant << "\tSolde: " << solde << "\tTaux: " << taux_interet * 100 << "%";
}

Tests du Système Bancaire

Démonstration des opérations sur les comptes d'épargne.


#include "account.h" // Inclut CompteEpargne et Date
#include <iostream>

// Alias pour std::cout et std::endl
using std::cout;
using std::endl;

int main() {
    Date date_depart(2008, 11, 1);

    // Création de plusieurs comptes d'épargne
    CompteEpargne comptes_epargne[] = {
        CompteEpargne(date_depart, "00123456", 0.015),
        CompteEpargne(date_depart, "00987654", 0.015)
    };
    const int nombre_comptes = sizeof(comptes_eparg) / sizeof(CompteEpargne);

    // Effectue diverses opérations sur les comptes
    comptes_epargne[0].deposer(Date(2008, 11, 5), 5000, "Salaire");
    comptes_epargne[1].deposer(Date(2008, 11, 25), 10000, "Vente d'actions");
    comptes_epargne[0].deposer(Date(2008, 12, 5), 5500, "Salaire");
    comptes_epargne[1].retirer(Date(2008, 12, 20), 4000, "Achat d'ordinateur portable");

    cout << endl;
    // Règle les intérêts et affiche les informations de chaque compte
    for (int i = 0; i < nombre_comptes; i++) {
        comptes_epargne[i].regler_interets(Date(2009, 1, 1));
        comptes_epargne[i].afficher_infos();
        cout << endl;
    }
    cout << "Solde total global de tous les comptes: " << CompteEpargne::obtenir_solde_total_global() << endl;
    return 0;
}

Bonnes Pratiques de Conception de Classes C++

Pour améliorer la robustesse, la lisibilité et la maintenabilité des classes C++, plusieurs principes peuvent être appliqués:

  1. Utilisation du pointeur this: Bien que souvent implicite, l'utilisation explicite de this->membre = valeur; dans les fonctions membres, surtout lorsque des paramètres ont le même nom qu'un membre, améliore la clarté et évite les ambiguïtés. Pour les opérateurs d'affectation, retourner *this est essentiel.
  2. Classes de Valeur Monétaire: Pour les applications financières, il est fortement recommandé d'utiliser une classe dédiée pour les montants monétaires plutôt que des flottants bruts (double). Une telle classe peut encapsuler la logique d'arrondi et prévenir les erreurs de précision inhérentes aux types à virgule flottante, garantissant des calculs financiers exacts.
  3. Fonctions inline appropriées: Les fonctions membres dont l'implémentation est très courte (une ou deux lignes) et qui sont appelées fréquemment sont de bons candidats pour être déclarées inline. Cela peut réduire le surcoût des appels de fonction et potentiellement améliorer les performances, bien que le compilateur ait le dernier mot sur l'inlining. Par exemple, les accesseurs simples (getters) sont souvent de bons candidats.
  4. Const-correctness: Toutes les fonctions membres qui ne modifient pas l'état interne de l'objet doivent être déclarées const (par exemple, obtenir_solde() const). Cela permet aux objets const d'appeler ces fonctions et signale clairement l'intention de la fonction aux autres développeurs et au compilateur, aidant à détecter les erreurs de logique.
  5. Gestion des erreurs: Plutôt que d'utiliser assert pour les erreurs de validation qui peuvent survenir en production (comme un index hors limites pour l'utilisateur), il est préférable de lancer des exceptions (std::out_of_range, std::invalid_argument) ou de retourner des codes d'erreur. Les assertions sont généralement destinées à détecter des bugs de programmation.

Étiquettes: C++ OOP classes Memory Management std::vector

Publié le 31 juillet à 05h43