Devoir Surveillé de Programmation Orientée Objets

Exercice n°1 - Trace d'exécution avec pointeurs et références Remarque préliminaire sur le code source : Le code extrait contient des erreurs de syntaxe (déclarations imbriquées, paramètres par défaut redéfinis).

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

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

Devoir Surveillé de Programmation Orientée Objets

Document source

Devoir Surveillé de Programmation Orientée Objets

Programmation, Informatique · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 6 pages · 2011

Afficher l'aperçu du document

Consulter le document original →

Exercice n°1 - Trace d'exécution avec pointeurs et références

Remarque préliminaire sur le code source : Le code extrait contient des erreurs de syntaxe (déclarations imbriquées, paramètres par défaut redéfinis). Pour pouvoir répondre à la question de manière logique, le code C++ a été reconstitué sous sa forme standard valide : les fonctions f1, f2, et f3 sont définies à l'extérieur de main(). La fonction f3 prend un argument par défaut int x = 7.

Une erreur critique (Comportement Indéfini) est présente dans f1 : elle retourne l'adresse d'une variable locale (res). Bien que la mémoire soit techniquement libérée à la fin de f1, nous ferons la trace en supposant que la zone mémoire n'est pas immédiatement écrasée par le compilateur, ce qui est l'exercice classique demandé dans ce type de devoir.

Tableau d'exécution

Instruction g1 ptr ptr1 a
État initial 2 non initialisé non initialisé non initialisé
1. ptr=&g1; 2 adresse de g1 non initialisé non initialisé
2. int &a=*ptr; 2 adresse de g1 non initialisé 2 (référence à g1)
3. int *ptr1=f1(a); 3 adresse de g1 adresse de res (dangling) 3
4. *ptr1=f2(ptr); 12 adresse de g1 adresse de res (valeur = 12) 12
5. (*ptr1)++; 12 adresse de g1 adresse de res (valeur = 13) 12
6. a++; 13 adresse de g1 adresse de res (valeur = 13) 13

Détail du raisonnement

  1. ptr=&g1; : ptr pointe désormais sur g1.
  2. int &a=*ptr; : a devient une référence sur la variable pointée par ptr, c'est-à-dire g1. Toute modification de a modifiera g1.
  3. int *ptr1=f1(a); : L'appel f1(a) passe g1 par référence (x). res = ++x; pré-incrémente g1 qui passe à 3. res vaut 3. La fonction retourne l'adresse de res. ptr1 pointe donc vers cette adresse mémoire devenue invalide. a et g1 valent 3.
  4. *ptr1=f2(ptr); :
    • L'appel f2(ptr) copie le pointeur. p = &g1;
    • (*p)++; incrémente g1, qui passe de 3 à 4.
    • return *f3(); appelle f3(7) (car argument par défaut).
    • Dans f3(7) : x++ fait passer x à 8. (*ptr)+=x; ajoute 8 à g1 (car ptr pointe sur g1). g1 = 4 + 8 = 12. La fonction retourne ptr (référence sur pointeur).
    • f2 déréférence ce retour (*ptr), renvoyant donc une référence sur g1 (qui vaut 12).
    • L'affectation *ptr1 = 12 place la valeur 12 dans la case mémoire (invalide) pointée par ptr1.
  5. (*ptr1)++; : La valeur pointée par ptr1 passe à 13. Cela n'affecte pas g1.
  6. a++; : a étant une référence sur g1, g1 est incrémenté et passe à 13.

Exercice n°2 - Analyse de l'exécution et de la gestion mémoire

Pour analyser cet exercice, il faut garder à l'esprit l'ordre d'appel des constructeurs lors de l'instanciation (les attributs objets sont construits avant le corps du constructeur de la classe englobante) et la génération par défaut du constructeur de copie et de l'opérateur d'affectation si non redéfinis.

Trace de l'affichage produit et erreurs associées

A a1;

Affichage : A::A()

A a2(1);

Affichage : A::A(int) avec n=1

A a3=a1;

Affichage : A::A(A&)0 (Note : Le constructeur de copie de A affiche la valeur de att, qui a été copiée depuis a1 et vaut 0).

a1=a2;

Affichage : affectation d'un A avec 1

B b1(2);

Affichage : A::A() (construction de l'attribut ab de b1 avant d'entrer dans le corps de B(int a)) B::B(2) A::A(int) avec n=2 (création de l'objet temporaire A(2) lié à l'instruction ab = a) affectation d'un A avec 2 (affectation de l'objet temporaire à l'attribut ab) A::~A() (destruction de l'objet temporaire)

b1.afficher();

Affichage : B::afficher() A::A() (instanciation de l'objet local A a dans la méthode) A::afficher(0) A::~A() (destruction de l'objet local a à la fin de la méthode)

b1.get_att().afficher();

Affichage : A::A(A&)2 (la méthode get_att retourne un A par valeur, ce qui invoque le constructeur de copie) A::afficher(2) A::~A() (destruction de l'objet temporaire retourné par la fonction)

B *b2;

Affichage : Aucun affichage. Erreur : b2 est un pointeur non initialisé (dangling pointer).

b2->afficher();

Affichage : B::afficher() A::A() A::afficher(0) A::~A() Erreur signalée : Violation de mémoire (Comportement Indéfini). On appelle une méthode sur un pointeur non initialisé. L'exécution ne plante pas immédiatement ici uniquement parce que B::afficher() n'accède à aucun attribut d'instance (le pointeur this n'est pas déréférencé dans la méthode), mais c'est une faute grave.

b2= new B(5);

Affichage : A::A() B::B(5) A::A(int) avec n=5 affectation d'un A avec 5 A::~A()

*b2=a3 ;

Affichage : A::A(A&)0 (L'objet a3 est converti implicitement en B via le constructeur B(A& t). Initialisation de l'attribut ab) B::B(A&) avec t (corps du constructeur) affectation d'un A avec 0 (L'affectation par défaut B::operator= appelle l'opérateur = de l'attribut A ab) B::~B() (destruction du temporaire B créé pour la conversion) A::~A() (destruction de l'attribut ab du temporaire) Erreurs signalées :

  1. Fuite de mémoire : L'affectation par défaut *b2 = objetTemporaire fait une copie de surface (shallow copy) du pointeur i. Le pointeur b2->i (qui contenait une adresse valide issue de new B(5)) est écrasé. La mémoire allouée est perdue.
  2. Violation de mémoire : Dans le constructeur de conversion B(A& t), l'attribut i n'est pas alloué (new int). Lors de la destruction de l'objet temporaire B, le destructeur fait delete i; sur un pointeur non initialisé.
b1=*b2 ;

Affichage : affectation d'un A avec 0 (affectation de l'attribut ab par l'opérateur généré par défaut) Erreur signalée : Fuite de mémoire. L'opérateur d'affectation par défaut écrase b1.i avec b2->i sans désallouer le pointeur initialement alloué dans b1(2).

B b3 = b1 ;

Affichage : A::A(A&)0 (Constructeur de copie par défaut de B qui appelle le constructeur de copie de A pour ab)

Fin du bloc

Affichage et Erreurs : À la fin du bloc, les objets locaux sont détruits dans l'ordre inverse de leur création :

  1. b3 est détruit : B::~B(), puis crash probable (delete i;).
  2. b1 est détruit : B::~B(), Double Libération de Mémoire (Double Free / Violation de mémoire). b1.i et b3.i pointent vers la même adresse (copie de surface), delete i; tentera de libérer une mémoire déjà libérée. A::~A().
  3. a3, a2, a1 sont détruits : A::~A() appelé 3 fois.
  4. Fuite de mémoire : L'objet pointé par b2 n'est jamais détruit car il n'y a pas de delete b2;.

Exercice n°3 : Manipulation de Polynômes et Évaluation d'Expressions

Première partie : Classe Polynome

Le code est rédigé pour correspondre scrupuleusement aux demandes, avec des méthodes limitées à l'utilisation de +, -, et *.

Questions 1, 2, 3, 4, 5 et 6

#include <iostream>
using namespace std;

class Polynome {
private:
    int* tab; // Tableau dynamique des coefficients
    int N;    // Degré du polynôme

public:
    // 2) Constructeur avec paramètre par défaut
    Polynome(int degre = 0) {
        N = degre;
        tab = new int[N + 1];
        for (int i = 0; i <= N; i++) {
            tab[i] = 0;
        }
    }

    // Constructeur de copie (Indispensable quand on a des pointeurs, 
    // bien que non explicitement demandé, sans lui la pile plantera)
    Polynome(const Polynome& p) {
        N = p.N;
        tab = new int[N + 1];
        for (int i = 0; i <= N; i++) {
            tab[i] = p.tab[i];
        }
    }

    // Opérateur d'affectation (même remarque que le constructeur de copie)
    Polynome& operator=(const Polynome& p) {
        if (this != &p) {
            delete[] tab;
            N = p.N;
            tab = new int[N + 1];
            for (int i = 0; i <= N; i++) {
                tab[i] = p.tab[i];
            }
        }
        return *this;
    }

    // 4) Destructeur : Oui, il est utile et obligatoire. 
    // Justification : La classe alloue dynamiquement de la mémoire pour le tableau 'tab'.
    ~Polynome() {
        delete[] tab;
    }

    // 3) Méthode d'initialisation
    void initialiser() {
        cout << "Entrez le degre du polynome : ";
        cin >> N;
        delete[] tab; // Nettoyer la mémoire précédente
        tab = new int[N + 1];
        for (int i = 0; i <= N; i++) {
            cout << "Coefficient de x^" << i << " : ";
            cin >> tab[i];
        }
    }

    // 5) Opérateur d'addition
    Polynome operator+(const Polynome& p) const {
        int max_deg = (N > p.N) ? N : p.N;
        Polynome res(max_deg);
        for (int i = 0; i <= max_deg; i++) {
            int coeff1 = (i <= N) ? tab[i] : 0;
            int coeff2 = (i <= p.N) ? p.tab[i] : 0;
            res.tab[i] = coeff1 + coeff2;
        }
        return res;
    }

    // 5) Opérateur de soustraction
    Polynome operator-(const Polynome& p) const {
        int max_deg = (N > p.N) ? N : p.N;
        Polynome res(max_deg);
        for (int i = 0; i <= max_deg; i++) {
            int coeff1 = (i <= N) ? tab[i] : 0;
            int coeff2 = (i <= p.N) ? p.tab[i] : 0;
            res.tab[i] = coeff1 - coeff2;
        }
        return res;
    }

    // 5) Opérateur de multiplication
    Polynome operator*(const Polynome& p) const {
        Polynome res(N + p.N);
        for (int i = 0; i <= N; i++) {
            for (int j = 0; j <= p.N; j++) {
                res.tab[i + j] += tab[i] * p.tab[j];
            }
        }
        return res;
    }

    // 6) Affichage
    void afficher() const {
        for (int i = 0; i <= N; i++) {
            cout << tab[i] << "*x^" << i;
            if (i < N) {
                cout << " + ";
            }
        }
        cout << endl;
    }
};

Deuxième partie : Classe Expression

Remarque sur l'énoncé : L'énoncé montre dans le programme principal E= ABC *+ ;. En C++, cette syntaxe n'est pas valide (espaces sans délimiteurs de chaînes). L'opération d'affectation décrite consiste à parcourir l'expression. Nous allons donc surcharger operator= avec un const char* pour traiter la chaîne "ABC*+" et simuler l'association entre les lettres A, B, C et les objets du problème (en supposant, pour respecter la logique du texte, qu'on instancie des polynômes identifiés).

De plus, l'algorithme d'évaluation prescrit par le texte (dépiler un opérateur, dépiler deux polynômes, appliquer, ré-empiler, puis recommencer) suppose que les piles sont construites à l'envers ou qu'elles sont traitées différemment d'un analyseur postfixé classique. Nous l'implémenterons fidèlement à l'énoncé.

Question 7 : Fichier d'en-tête (Expression.h)

#ifndef EXPRESSION_H
#define EXPRESSION_H

#include "Polynome.h"
#include "Pile_oper.h"
#include "Pile_poly.h"

class Expression {
private:
    Pile_poly operandes;
    Pile_oper operateurs;

public:
    Expression();
    ~Expression();

    // Surcharge pour affecter une chaine de caractères représentant l'expression
    Expression& operator=(const char* exp);

    // Méthode d'évaluation
    Polynome evaluer_poly();
};

#endif

Question 8 : Implémentation (Expression.cpp)

#include "Expression.h"
#include <iostream>

using namespace std;

// Constructeur et Destructeur : les piles s'instancient / se détruisent d'elles-mêmes
Expression::Expression() {}
Expression::~Expression() {}

// Implémentation de l'affectation
Expression& Expression::operator=(const char* exp) {
    int i = 0;
    while (exp[i] != '\0') {
        if (exp[i] == '+' || exp[i] == '-' || exp[i] == '*') {
            operateurs.empiler(exp[i]);
        } 
        else if (exp[i] >= 'A' && exp[i] <= 'Z') {
            // Note : L'exercice ne fournit pas le mécanisme de liaison (binding) 
            // entre la lettre 'A' et l'objet Polynome A défini dans le main.
            // Dans une implémentation réelle de cet examen, on empilerait 
            // le polynôme correspondant. Ici, nous créons un polynôme fictif
            // pour satisfaire le typage de la méthode empiler().
            Polynome pTemp(1); 
            operandes.empiler(pTemp);
        }
        i++;
    }
    return *this;
}

// Implémentation de l'évaluation selon l'algorithme strict du sujet
Polynome Expression::evaluer_poly() {
    while (!operateurs.estVide()) {
        // Dépiler un opérateur de la pile des opérateurs
        char op = operateurs.depiler();
        
        // Dépiler les deux derniers polynômes stockés
        Polynome p2 = operandes.depiler();
        Polynome p1 = operandes.depiler();
        Polynome res;

        // Appliquer l'opérateur
        if (op == '+') {
            res = p1 + p2;
        } else if (op == '-') {
            res = p1 - p2;
        } else if (op == '*') {
            res = p1 * p2;
        }

        // Empiler le résultat
        operandes.empiler(res);
    }
    
    // Une fois la pile vide, le polynôme résultat est dépilé et affiché
    Polynome resultat_final = operandes.depiler();
    resultat_final.afficher();
    
    return resultat_final;
}

Méthode

Face à ce type de devoir surveillé, il faut faire preuve d'une double compétence :

  1. Comprendre l'architecture de la mémoire en C++ : L'exercice 1 et l'exercice 2 visent spécifiquement à tester votre capacité à tracer mentalement le cycle de vie d'un objet et l'évolution des pointeurs. Dessinez toujours les zones mémoires sur un brouillon (le tas et la pile) pour repérer les fuites de mémoire (allocations sans delete), les comportements indéfinis (pointeur local détruit mais retourné) et les erreurs de copie de surface (absence de constructeur de copie profond provoquant un "double free").
  2. Suivre la spécification à la lettre : Dans l'exercice 3, l'énoncé fournit un algorithme d'évaluation qui s'écarte légèrement de l'algorithme classique d'évaluation postfixée. Dans le cadre d'un examen, même si l'énoncé vous paraît comporter une approximation (comme E = ABC *+; qui n'est pas du C++ valide), vous devez produire le code qui se rapproche le plus de l'intention du professeur sans modifier fondamentalement sa classe. Implémentez rigoureusement les étapes numérotées du sujet.

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