Le carnet On est
Annale · session normale 2024

Examen C++ 2024 — 2025

Cinq exercices balayant tout le programme : passage de paramètres, classe abstraite avec virtual + const, comptage constructeur/destructeur, héritage Point → PointCol (questions de cours), et un gros final sur l'héritage multiple virtuel + templates STL.

1h30 · ING2 MF 5 exercices · sans document Date : 06/12/2024

1. Le sujet

Le PDF ne s'affiche pas. Télécharger l'énoncé.

📥 Télécharger l'énoncé · ↗ Énoncé en plein écran · ↗ Corrigé officiel

2. Notions mobilisées

ExoSujetNotion cléChapitre
13 versions de ajouterPassage par valeur, par pointeur, par référenceSE1
2Classe Forme abstraitevirtual, const sur méthodes, virtuelle pure privateSE4 §7
3Trois main sur PointCycle de vie : new, delete, scopeSE2 §3
4Héritage Point/PointColMode public, using namespace std, #ifndefSE4 §4 · SE1
Accès aux attributs privésprotected ou gettersSE4
Polymorphisme et typage dynamiqueConstruction via constructeur de la filleSE4 §5
5Hiérarchie complète + diagramme UMLHéritage multiple virtuel — problème du diamantSE7 §10
Template Vector<T> + itérateurConteneur STL, polymorphisme via pointeursSE7

3. Correction — Exercice 1 : passages de paramètres

Le programme principal :

void main() {
   int a = 1;
   int b = -2;
   int c = 0;
   ajouter(a, b, c);
   cout << "apres appel de ajouter" << endl;
   cout << "c = " << c << endl;
}

Cas a — passage par valeur

void ajouter(int a, int b, int c) { c = a + b; }

Sortie : apres appel de ajouter puis c = 0.

L'argument c de la fonction est une copie de la variable du main. La modification reste locale, le c du main n'est pas touché.

Cas b — passage par pointeur

void ajouter(int a, int b, int* c) { *c = a + b; }

Résultat : erreur de compilation. L'appel ajouter(a, b, c) transmet c comme int, alors que la fonction attend un int*. invalid conversion from int to int*.

Pour faire fonctionner ce cas, il aurait fallu écrire ajouter(a, b, &c);.

Cas c — passage par référence

void ajouter(int a, int b, int& c) { c = a + b; }

Sortie : apres appel de ajouter puis c = -1.

L'argument c est un alias sur la variable du main. Toute écriture dans la fonction modifie directement la variable d'origine. c = 1 + (-2) = -1.

Question 2 — nommer les trois mécanismes
  • Pour c : (a) copie de valeur · (b) copie de pointeur · (c) copie de référence
  • Pour a et b : copie de valeur dans les trois cas (la signature ne change pas pour ces paramètres)

4. Correction — Exercice 2 : classe Forme

class Forme {
   public:
      virtual void description(const string s) { cout << "Ceci est une forme."; }
      virtual const double aire() { }
   private:
      virtual double perimetre() const = 0;
};
Q1 — Cette classe ne possède pas de constructeur, pourquoi ?

Parce qu'elle est abstraite : elle contient une fonction virtuelle pure (perimetre() = 0). On ne peut donc pas instancier directement Forme, et il n'y a pas besoin de constructeur public. La classe sert uniquement de contrat pour ses dérivées.

Q2 — Que signifie virtual dans chacune des trois méthodes ?
  • description et aire sont des méthodes virtuelles classiques : elles peuvent être redéfinies dans les classes filles, et l'appel est résolu dynamiquement (polymorphisme).
  • perimetre() = 0 est virtuelle pure : pas d'implémentation par défaut, les classes dérivées doivent la définir.
Q3 — Que signifie const dans chacune des trois méthodes ?
  • description(const string s)l'argument s ne peut pas être modifié dans le corps.
  • const double aire() — le type retourné est constant (on ne peut pas modifier la valeur de retour). Usage rare et discutable.
  • double perimetre() const — la méthode elle-même est const : elle ne peut pas modifier les attributs de l'instance. C'est le const qui suit la parenthèse.
Q4 — Pourquoi ce code ne compile pas ?

La méthode perimetre() est déclarée virtuelle pure ET privée. Or pour qu'une dérivée puisse fournir une implémentation par override, la méthode doit être au moins protected. Une virtuelle pure private n'a pas de sens dans un contrat d'héritage.

Pour corriger : déplacer perimetre() en public ou protected.

5. Correction — Exercice 3 : compteur d'appels

class Point {
   private: short x, y;
   public:
      Point()  { cout << " ++ normal \n"; }
      ~Point() { cout << " -- normal \n"; }
};

Cas a — variable locale

void main() { Point p; }

Sortie :

++ normal
-- normal

Le constructeur est appelé à la création. Le destructeur est appelé à la fin du scope (fin de main), juste avant le retour du programme. Tout est affiché.

Cas b — allocation dynamique sans delete

void main() { Point* p = new Point(); }

Sortie :

++ normal

Le constructeur est appelé. Mais aucun delete n'est appelé : l'objet alloué sur le tas n'est jamais libéré. Le destructeur n'est jamais déclenché → memory leak et pas d'affichage de -- normal.

(Quand le programme se termine, l'OS récupère la mémoire mais les destructeurs des objets non libérés ne sont pas appelés.)

Cas c — allocation et libération explicite

void main() { Point* p = new Point(); delete p; }

Sortie :

++ normal
-- normal

Le constructeur s'exécute, puis delete p appelle explicitement le destructeur avant de libérer la mémoire. Comportement attendu et propre.

📌 Règle d'or
À chaque new doit correspondre un delete. À chaque new[] doit correspondre un delete[]. Sans ça : fuite mémoire et destructeur silencieusement omis.

6. Correction — Exercice 4 : Point / PointCol

Énoncé long avec 10 questions de cours. Réponses synthétiques :

Q1 — Comment se fait l'initialisation dans le constructeur ? Pourquoi ?

À la C++ via la liste d'initialisation (: x(abs), y(ord) {}) et non par affectation dans le corps. Avantage : contrôle de type à la compilation et plus efficace (pas de double initialisation).

Q2 — Pourquoi short dans Point et unsigned int dans PointCol ?
  • short = entier signé sur peu d'octets (typiquement 2). Convient à des coordonnées modestes.
  • unsigned int = entier non signé, valeurs positives uniquement (« valeurs naturelles »). Couleur RGB ou index de palette → naturellement positif, donc unsigned.
Q3 — Cette classe Point est-elle canonique ?

Non. Une classe canonique au sens C++ devrait définir : constructeur par défaut, constructeur de copie, opérateur d'affectation et destructeur. Ici manquent le constructeur de copie et la surcharge de l'opérateur d'affectation (cf. règle de trois, CM2).

Q4 — À quoi sert using namespace std; ? Sans cette directive ?

Un namespace regroupe des noms (variables, fonctions, classes). La directive using namespace std; permet d'écrire cout, endl, string sans le préfixe std::. Si on la supprime, il faut préfixer : std::cout, std::endl

Q5 — Expliquer #ifndef POINT_H_ / #define POINT_H_ / #endif

C'est le include guard. Évite que le contenu d'un header soit chargé plusieurs fois lors de la compilation (problème de double définition). Vérifie si POINT_H_ n'est pas défini, le définit, et inclut tout le contenu uniquement à la première rencontre.

Q6 — Que signifie public devant Point dans class PointCol : public Point ?

C'est le mode d'héritage. Avec public, les membres de Point conservent leur statut d'origine dans PointCol : les public restent public, les protected restent protected. C'est le mode qui correspond à la relation « est un ».

Q7 — Problème d'accès dans PointCol ?

Les attributs x et y de Point sont private. PointCol ne peut pas y accéder directement (l'encapsulation s'applique aussi aux dérivées).

Deux corrections possibles :

  1. Implémenter des getters getX() et getY() dans Point.
  2. Déclarer x et y en protected dans Point (accessibles aux dérivées sans casser l'encapsulation publique).
Q8 — Dans le main, comment appelle-t-on ce typage ?

Typage dynamique (ou polymorphisme) : le type de l'objet réellement créé est choisi à l'exécution. Par exemple, Point *p2 = new PointCol(8, 6, 5); : le pointeur est déclaré comme Point* mais l'objet créé est en réalité un PointCol.

Q9 — Quelle ligne est fausse ?

La 4e ligne :

PointCol *pc2 = new Point(7, 6);   // faux

Un Point n'est pas un PointCol. On ne peut pas affecter un pointeur sur une classe de base à un pointeur sur une dérivée sans cast explicite. La relation « est un » est unidirectionnelle (PointCol est un Point, mais pas l'inverse).

Q10 — Si on supprime cette ligne, quelle est la sortie ?
je suis un point
mes coordonnees sont : 3 5
je suis un point colore
mes coordonnees sont : 8 6
ma couleur est : 2
je suis un point colore
mes coordonnees sont : 8 6
ma couleur est : 5

Sortie cohérente avec le polymorphisme : p2->decrire() sur un PointCol caché derrière un Point* appelle bien la version PointCol::decrire grâce à virtual.

7. Correction — Exercice 5 : diamant + templates

L'exo ajoute deux classes à la hiérarchie : PointForme (caractère pour la forme) et PointFormeCol (à la fois forme et couleur).

Q1 — Diagramme UML

La hiérarchie forme un diamant dont la pointe basse est PointFormeCol :

Point x, y · decrire() PointCol + color PointForme + forme PointFormeCol

Q2 — Les .h des deux nouvelles classes

// pointforme.h
#ifndef POINTFORME_H_
#define POINTFORME_H_
#include "point.h"

class PointForme : virtual public Point {
   protected:
      char forme;
   public:
      PointForme(short abs, short ord, char f)
         : Point(abs, ord), forme{f} { }
      void decrire() override {
         cout << "je suis un point avec une forme\n";
         cout << "mes coordonnees : " << x << " " << y
              << " et ma forme : " << forme << "\n";
      }
};
#endif
// pointformecol.h
#ifndef POINTFORMECOL_H_
#define POINTFORMECOL_H_
#include "pointcol.h"
#include "pointforme.h"

class PointFormeCol : virtual public Point,
                      public PointForme,
                      public PointCol {
   public:
      PointFormeCol(short abs, short ord, char f, unsigned int cl)
         : Point(abs, ord),
           PointForme(abs, ord, f),
           PointCol(abs, ord, cl) { }
      void decrire() override {
         cout << "je suis un point colore avec une forme\n";
         cout << "mes coordonnees : " << x << " " << y << ", "
              << "ma couleur : " << color << ", "
              << "ma forme : " << forme << "\n";
      }
};
#endif
⚠ Le mot-clé virtual dans la dérivation est crucial Sans lui, PointFormeCol contiendrait deux copies de Point (une via PointCol, une via PointForme). Les accès à x, y deviendraient ambigus. C'est le « problème du diamant », résolu par l'héritage virtuel (SE7 §10).

Q3 — main avec vector<Point*> et itérateur

#include "pointformecol.h"
#include <vector>

int main() {
   vector<Point*> LPs;
   vector<Point*>::iterator it;

   Point* p   = new Point(3, 5);
   Point* pc  = new PointCol(3, 5, 3);
   Point* pf  = new PointForme(5, 6, 'o');
   Point* pfc = new PointFormeCol(5, 6, 'x', 9);

   LPs.push_back(p);
   LPs.push_back(pc);
   LPs.push_back(pf);
   LPs.push_back(pfc);

   for (it = LPs.begin(); it != LPs.end(); ++it)
      (*it)->decrire();

   return 0;
}

Chaque objet appelle sa propre version de decrire() grâce au polymorphisme dynamique (les decrire() sont virtuelles dans Point). Le vector<Point*> conserve uniquement le type statique de base, mais le dispatch est résolu au runtime.

📌 N'oublie pas delete
Bonne pratique idéale : itérer sur LPs à la fin pour appeler delete sur chaque pointeur, puis LPs.clear(). Ou mieux : utiliser vector<unique_ptr<Point>> qui s'en occupe tout seul (C++11+).

Stratégie générale pour ce format

  • Lire d'abord le code avant de répondre aux questions. Souvent les questions explicitent ce qui ne va pas dans le code.
  • Identifier les pièges classiques :
    • fonction virtuelle pure en private
    • delete oublié après new
    • cast implicite parent → enfant (interdit)
    • accès aux attributs privés depuis une fille
    • héritage multiple sans virtual → diamant
  • Justifier chaque réponse. Une bonne réponse sans explication vaut moins qu'une réponse partielle bien justifiée.
  • Connaître la règle de trois, la syntaxe des templates, les modes d'héritage : ça revient à chaque exam.
  • Schéma UML : même approximatif, il aide à structurer la réponse sur les hiérarchies.