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.

Document source
Programming, Math · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 10 pages · 2010
Afficher l'aperçu du document
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
ptrdea1est initialisé àNULL.
- Affichage :
-
a1.afficher();- Affichage : Le programme tente d'afficher
*ptr. - Mémoire : Violation de mémoire (Erreur de segmentation). Le pointeur
ptrétantNULL, le déréférencer via*ptrprovoque 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.
- Affichage : Le programme tente d'afficher
-
A a2(5);- Affichage :
A::A(5) - Mémoire : Un entier contenant la valeur 5 est alloué dynamiquement.
- Affichage :
-
a1 = a2;- Affichage :
operateur = - Mémoire : L'opérateur d'affectation de la classe
Aalloue un nouvel espace mémoire pour copier la valeur pointée para2.ptr. Commea1.ptrétaitNULL, il n'y a exceptionnellement pas de fuite de mémoire ici. (Cependant, sia1.ptravait pointé vers une zone mémoire valide, cette zone aurait été perdue, créant une fuite, car l'opérateur=omet de faire undelete ptravant de réallouer).
- Affichage :
-
a1.afficher();- Affichage :
A::*ptr=5 - Mémoire : RAS.
- Affichage :
-
A a3 = a2;- Affichage : Aucun affichage.
- Mémoire : C'est une initialisation, le constructeur de copie est appelé. Or, la classe
An'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.ptreta2.ptrpointent 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)puisB::B() - Mémoire : Le constructeur de
Bappelle explicitement le constructeurA(1)pour initialiser l'attributatt.
B b2(2);
- Affichage :
A::A()puisA::A(2)puisoperateur =puisB::B(2) - Mémoire : L'attribut
attdeb2est d'abord construit avec le constructeur par défaut deA(d'oùA::A()). Ensuite, dans le corps du constructeur deB, une instance temporaireA(2)est créée. Cette instance est affectée àattvia l'opérateur=, puis l'instance temporaire est détruite.
b1.afficher();
- Affichage :
B::puisA::*ptr=1 - Mémoire : RAS.
b1.f(a2);
- Affichage :
operateur =puisB::puisA::*ptr=5 - Mémoire : Le passage de
a2à la méthodef(A arg)se fait par valeur. Comme il n'y a pas de constructeur de copie défini,arg.ptrpointe vers la même adresse quea2.ptr. À l'intérieur def, l'instructionatt = arg;appelle l'opérateur=. Fuite de mémoire : l'ancienne valeur allouée pourb1.att.ptr(qui contenait 1) n'est jamais libérée avant de recevoir le nouveau pointeur. À la fin de l'appel def, l'objet localargest 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 dea2.ptr. L'objeta2possède désormais un pointeur "pendant" (dangling pointer).
Fin du bloc (Appel des destructeurs)
- Affichage :
B::~B()(pourb2), puisB::~B()(pourb1), puis aucun affichage pour les destructions deA. - Mémoire : De multiples violations de mémoire (double free) surviennent.
- Lors de la destruction de
b1, l'attributattest détruit. Il partageait potentiellement sa mémoire avec d'autres objets. - Lors de la destruction de
a3, son destructeur tente de fairedelete ptr. Or, cette adresse a déjà été libérée à la fin de la fonctionf(). - Lors de la destruction de
a2, le destructeur essaie de libérer une mémoire déjà libérée parf()et para3.
- Lors de la destruction de
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 à
NULLlors 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
deletepour é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).
Commentaires
Aucun commentaire pour le moment. Posez la première question.