Devoir Surveillé en Programmation Orientée Objets

Exercice 1 - Analyse de l'exécution et de la mémoire Question unique - Trace d'exécution et gestion de la mémoire Ce code présente plusieurs défauts majeurs de conception (notamment l'absence de constructeur de copie dans la classe A et un opérateur d'affectation qui ne libère pas la mémoire existante), ce qui entraîne des violations d'accès et des fuites de mémoire.

D'après le document Devoir Surveillé en Programmation Orientée Objets

Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Devoir Surveillé en Programmation Orientée Objets

Document source

Devoir Surveillé en Programmation Orientée Objets

Programming, Math · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 10 pages · 2010

Afficher l'aperçu du document

Consulter le document original →

Exercice 1 - Analyse de l'exécution et de la mémoire

Question unique - Trace d'exécution et gestion de la mémoire

Ce code présente plusieurs défauts majeurs de conception (notamment l'absence de constructeur de copie dans la classe A et un opérateur d'affectation qui ne libère pas la mémoire existante), ce qui entraîne des violations d'accès et des fuites de mémoire.

Voici l'analyse instruction par instruction :

  • A a1;

    • Affichage : A::A()
    • Mémoire : Aucune erreur à ce stade. Le pointeur ptr de a1 est initialisé à NULL.
  • a1.afficher();

    • Affichage : Le programme tente d'afficher *ptr.
    • Mémoire : Violation de mémoire (Erreur de segmentation). Le pointeur ptr étant NULL, le déréférencer via *ptr provoque le plantage du programme. Note : Pour la suite de l'analyse, nous supposerons que le programme continue virtuellement afin de pouvoir analyser les autres instructions.
  • A a2(5);

    • Affichage : A::A(5)
    • Mémoire : Un entier contenant la valeur 5 est alloué dynamiquement.
  • a1 = a2;

    • Affichage : operateur =
    • Mémoire : L'opérateur d'affectation de la classe A alloue un nouvel espace mémoire pour copier la valeur pointée par a2.ptr. Comme a1.ptr était NULL, il n'y a exceptionnellement pas de fuite de mémoire ici. (Cependant, si a1.ptr avait pointé vers une zone mémoire valide, cette zone aurait été perdue, créant une fuite, car l'opérateur = omet de faire un delete ptr avant de réallouer).
  • a1.afficher();

    • Affichage : A::*ptr=5
    • Mémoire : RAS.
  • A a3 = a2;

    • Affichage : Aucun affichage.
    • Mémoire : C'est une initialisation, le constructeur de copie est appelé. Or, la classe A n'en définit pas. Le compilateur fournit donc un constructeur de copie par défaut qui effectue une copie de surface (shallow copy). Ainsi, a3.ptr et a2.ptr pointent vers la même adresse mémoire. C'est une erreur grave qui provoquera une double libération (violation) à la destruction.
  • a3.afficher();

    • Affichage : A::*ptr=5
    • Mémoire : RAS à l'exécution de cette ligne.
  • B b1;

    • Affichage : A::A(1) puis B::B()
    • Mémoire : Le constructeur de B appelle explicitement le constructeur A(1) pour initialiser l'attribut att.
  • B b2(2);

    • Affichage : A::A() puis A::A(2) puis operateur = puis B::B(2)
    • Mémoire : L'attribut att de b2 est d'abord construit avec le constructeur par défaut de A (d'où A::A()). Ensuite, dans le corps du constructeur de B, une instance temporaire A(2) est créée. Cette instance est affectée à att via l'opérateur =, puis l'instance temporaire est détruite.
  • b1.afficher();

    • Affichage : B:: puis A::*ptr=1
    • Mémoire : RAS.
  • b1.f(a2);

    • Affichage : operateur = puis B:: puis A::*ptr=5
    • Mémoire : Le passage de a2 à la méthode f(A arg) se fait par valeur. Comme il n'y a pas de constructeur de copie défini, arg.ptr pointe vers la même adresse que a2.ptr. À l'intérieur de f, l'instruction att = arg; appelle l'opérateur =. Fuite de mémoire : l'ancienne valeur allouée pour b1.att.ptr (qui contenait 1) n'est jamais libérée avant de recevoir le nouveau pointeur. À la fin de l'appel de f, l'objet local arg est détruit. Son destructeur libère la mémoire de son pointeur. Violation de mémoire : puisque c'était une copie de surface, cela détruit la mémoire de a2.ptr. L'objet a2 possède désormais un pointeur "pendant" (dangling pointer).
  • Fin du bloc (Appel des destructeurs)

    • Affichage : B::~B() (pour b2), puis B::~B() (pour b1), puis aucun affichage pour les destructions de A.
    • Mémoire : De multiples violations de mémoire (double free) surviennent.
      • Lors de la destruction de b1, l'attribut att est détruit. Il partageait potentiellement sa mémoire avec d'autres objets.
      • Lors de la destruction de a3, son destructeur tente de faire delete ptr. Or, cette adresse a déjà été libérée à la fin de la fonction f().
      • Lors de la destruction de a2, le destructeur essaie de libérer une mémoire déjà libérée par f() et par a3.
  • Exercice 2 - La classe Fraction

    Note de correction : L'énoncé d'origine comporte une erreur de syntaxe dans le constructeur de la classe Fraction : if (denom) == 0 ). Cette erreur a été corrigée en if (denom == 0) dans le code suivant. De plus, pour implémenter les opérateurs demandés, il est nécessaire de déclarer les opérateurs externes comme friend dans la classe, ou d'ajouter des méthodes d'accès (getters). Nous avons choisi d'ajouter les déclarations friend.

    Question 1 - Opérateurs de multiplication et d'addition de deux fractions

    Voici la définition mise à jour de la classe pour supporter les opérateurs, suivie de leur implémentation. On suppose la fonction int pgcd(int a, int b) accessible.

    class Fraction {
    private:
        int numer;  // numérateur
        int denom;  // dénominateur
    
    public:
        Fraction(int num=1, int den=1) : numer(num), denom(den)
        {  
            if (denom == 0) denom = 1; 
            reduire(); // Appel à une méthode utilitaire pour garantir la forme réduite
        }
    
        // Méthode privée ou publique pour réduire la fraction
        void reduire() {
            int p = pgcd(numer, denom);
            numer = numer / p;
            denom = denom / p;
            // Gestion du signe au dénominateur (optionnel mais recommandé)
            if (denom < 0) {
                numer = -numer;
                denom = -denom;
            }
        }
    
        // Déclarations amies pour les opérateurs
        friend Fraction operator*(const Fraction& f1, const Fraction& f2);
        friend Fraction operator+(const Fraction& f1, const Fraction& f2);
        friend Fraction operator*(const Fraction& f, int entier);
        friend Fraction operator*(int entier, const Fraction& f);
        friend bool operator==(const Fraction& f1, const Fraction& f2);
        friend bool operator!=(const Fraction& f1, const Fraction& f2);
    };
    
    // Implémentation de la multiplication de deux fractions
    Fraction operator*(const Fraction& f1, const Fraction& f2) {
        int num = f1.numer * f2.numer;
        int den = f1.denom * f2.denom;
        return Fraction(num, den); // Le constructeur s'occupe de la forme réduite
    }
    
    // Implémentation de l'addition de deux fractions
    Fraction operator+(const Fraction& f1, const Fraction& f2) {
        int num = (f1.numer * f2.denom) + (f2.numer * f1.denom);
        int den = f1.denom * f2.denom;
        return Fraction(num, den);
    }
    

    Question 2 - Multiplication avec un entier

    Nous tenons compte de la commutativité en définissant les deux ordres.

    // Multiplication : Fraction * Entier
    Fraction operator*(const Fraction& f, int entier) {
        int num = f.numer * entier;
        int den = f.denom;
        return Fraction(num, den);
    }
    
    // Multiplication : Entier * Fraction
    Fraction operator*(int entier, const Fraction& f) {
        return f * entier; // Réutilisation de l'opérateur précédent
    }
    

    Question 3 - Opérateurs d'égalité et de non égalité

    Deux fractions sont égales si leurs produits croisés sont égaux, ou, puisqu'elles sont toujours stockées sous forme réduite dans notre conception, si leurs numérateurs et dénominateurs sont strictement identiques. Utilisons la comparaison directe (plus sûre contre les débordements d'entiers).

    bool operator==(const Fraction& f1, const Fraction& f2) {
        return (f1.numer == f2.numer) && (f1.denom == f2.denom);
    }
    
    bool operator!=(const Fraction& f1, const Fraction& f2) {
        return !(f1 == f2);
    }
    

    Question 4 - L'opérateur d'affectation

    Non, il n'est pas nécessaire de redéfinir l'opérateur d'affectation (operator=) pour la classe Fraction. Justification : La classe ne contient que des types primitifs simples (int). Elle ne contient pas de pointeurs ni de ressources allouées dynamiquement. Le comportement par défaut généré par le compilateur C++ (qui effectue une copie bit-à-bit, ou affectation membre à membre) est tout à fait suffisant et correct pour cet usage.

    Question 5 - Programmes d'utilisation de la classe

    a. Calcul de la fraction F3

    #include <iostream>
    using namespace std;
    
    int main() {
        Fraction F1(1, 2);
        Fraction F2(2, 3);
        
        // Calcul de F3 = 2 * F2 + F1
        Fraction F3 = 2 * F2 + F1;
        
        // Vérification de l'égalité
        Fraction reference(5, 6);
        if (F3 == reference) {
            cout << "F3 est egale a la fraction 5/6" << endl;
        } else {
            cout << "F3 n'est pas egale a la fraction 5/6" << endl;
        }
        
        return 0;
    }
    

    b. Calcul de la suite Un

    #include <iostream>
    using namespace std;
    
    int main() {
        // Un = 1 + 1/2 + 1/3 + ... + 1/100
        // On initialise Un à 0/1 pour faciliter la somme
        Fraction Un(0, 1); 
        
        for(int i = 1; i <= 100; i++) {
            Un = Un + Fraction(1, i);
        }
        
        // Le résultat est stocké dans Un sous forme réduite
        return 0;
    }
    

    Exercice 3 - La classe Ensemble et le Crible d’Eratosthène

    Question 1 - Nécessité des éléments du cycle de vie

    Puisque l'on ne manipule qu'une seule instance de la classe Ensemble dans notre programme :

    • Constructeur par défaut : OUI, il est nécessaire. Il faut initialiser le pointeur de début de la liste chaînée à NULL lors de la création de l'objet.
    • Constructeur de copie : NON, il n'est pas strictement nécessaire ici. Puisqu'il n'y a qu'une seule instance et que l'objet ne sera jamais copié ni passé par valeur à une fonction, le constructeur de copie ne sera jamais appelé.
    • Opérateur d'affectation : NON, pour la même raison que le constructeur de copie. Il n'y aura aucune affectation entre différentes instances d'Ensemble.
    • Destructeur : OUI, il est absolument nécessaire. La liste utilise des allocations dynamiques (nœuds). À la fin de vie de l'objet, il faut libérer chaque nœud avec delete pour éviter les fuites de mémoire.

    Question 2 - Interface de la classe Ensemble

    class Ensemble
    {
    private:
        Noeud * tete; // Pointeur vers le premier élément de la liste
    
    public:
        Ensemble();                 // Constructeur par défaut
        ~Ensemble();                // Destructeur
    
        void add(int v);            // Ajouter un élément
        void del(int v);            // Supprimer un élément
        bool estPresent(int v);     // Savoir si un élément est présent
        bool estVide();             // Savoir si l'ensemble est vide
        int plusPetit();            // Obtenir le plus petit élément
    };
    

    Question 3 - Implémentation des méthodes

    #include <iostream>
    using namespace std;
    
    // Constructeur
    Ensemble::Ensemble() {
        tete = NULL;
    }
    
    // Destructeur
    Ensemble::~Ensemble() {
        Noeud * crt = tete;
        while (crt != NULL) {
            Noeud * aSupprimer = crt;
            crt = crt->next;
            delete aSupprimer;
        }
    }
    
    // Savoir si un élément est présent
    bool Ensemble::estPresent(int v) {
        Noeud * crt = tete;
        while (crt != NULL) {
            if (crt->valeur == v) {
                return true;
            }
            crt = crt->next;
        }
        return false;
    }
    
    // Ajouter un élément (en tête) s'il n'est pas déjà présent
    void Ensemble::add(int v) {
        if (!estPresent(v)) {
            Noeud * nouveau = new Noeud;
            nouveau->valeur = v;
            nouveau->next = tete;
            tete = nouveau;
        }
    }
    
    // Supprimer un élément
    void Ensemble::del(int v) {
        Noeud * crt = tete;
        Noeud * prec = NULL;
        
        while (crt != NULL && crt->valeur != v) {
            prec = crt;
            crt = crt->next;
        }
        
        // Si l'élément a été trouvé
        if (crt != NULL) {
            if (prec == NULL) {
                // Suppression en tête
                tete = crt->next;
            } else {
                // Suppression au milieu ou à la fin
                prec->next = crt->next;
            }
            delete crt;
        }
    }
    
    // Savoir si l'ensemble est vide
    bool Ensemble::estVide() {
        return (tete == NULL);
    }
    
    // Obtenir le plus petit élément
    int Ensemble::plusPetit() {
        if (estVide()) {
            // En pratique, on lèverait une exception. 
            // On retourne -1 pour signaler l'erreur selon le type de retour int.
            return -1; 
        }
        
        int min = tete->valeur;
        Noeud * crt = tete->next;
        
        while (crt != NULL) {
            if (crt->valeur < min) {
                min = crt->valeur;
            }
            crt = crt->next;
        }
        return min;
    }
    

    Question 4 - Programme principal (Crible d’Eratosthène)

    void main( )
    {
        Ensemble set;
        int n;
        
        cout << "Saisissez un entier n (>= 2) : ";
        cin >> n;
        
        // 1. Construire un ensemble contenant tous les entiers de 2 à n
        for (int i = 2; i <= n; i++) {
            set.add(i);
        }
        
        // 4. Continuer jusqu'à ce que l'ensemble soit vide
        while (!set.estVide()) {
            // 2. Extraire le plus petit élément et l'afficher
            int ppe = set.plusPetit();
            cout << ppe << endl;
            
            // 3. Enlever tous les multiples de ce nombre premier
            // On incrémente de 'ppe' en 'ppe' pour parcourir directement les multiples
            for (int multiple = ppe; multiple <= n; multiple += ppe) {
                set.del(multiple);
            }
        }
    }
    

    (Note sur void main() : L'énoncé impose la signature void main(), bien que la norme C++ standard exige int main(). Nous avons respecté la signature de l'énoncé.)

    Méthode

    Face à ce type de devoir surveillé, il est crucial de maîtriser le cycle de vie des objets.

    Pour l'exercice de trace (Exercice 1), la difficulté réside dans les mécanismes invisibles du C++ : l'appel du constructeur de copie par défaut (copie de surface) lors d'un passage par valeur ou d'une initialisation, et l'appel des destructeurs à la fin de la portée (bloc). Dessiner schématiquement la mémoire (pointeurs et blocs alloués) sur un brouillon vous évitera de rater les fuites et violations.

    Pour les classes mathématiques ou les structures de données (Exercices 2 et 3), concentrez-vous sur l'encapsulation et la cohérence de l'état. Vérifiez à chaque opération d'affectation dynamique s'il faut libérer de la mémoire (delete), et pensez toujours aux cas particuliers de vos parcours de liste (suppression du premier élément, liste vide).

    Partager

    Commentaires

    Aucun commentaire pour le moment. Posez la première question.

    Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

    ← Toutes les révisions