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.
1. Le sujet
📥 Télécharger l'énoncé · ↗ Énoncé en plein écran · ↗ Corrigé officiel
2. Notions mobilisées
| Exo | Sujet | Notion clé | Chapitre |
|---|---|---|---|
| 1 | 3 versions de ajouter | Passage par valeur, par pointeur, par référence | SE1 |
| 2 | Classe Forme abstraite | virtual, const sur méthodes, virtuelle pure private | SE4 §7 |
| 3 | Trois main sur Point | Cycle de vie : new, delete, scope | SE2 §3 |
| 4 | Héritage Point/PointCol | Mode public, using namespace std, #ifndef | SE4 §4 · SE1 |
| Accès aux attributs privés | protected ou getters | SE4 | |
| Polymorphisme et typage dynamique | Construction via constructeur de la fille | SE4 §5 | |
| 5 | Hiérarchie complète + diagramme UML | Héritage multiple virtuel — problème du diamant | SE7 §10 |
Template Vector<T> + itérateur | Conteneur STL, polymorphisme via pointeurs | SE7 |
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.
- 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 ?
descriptionetairesont des méthodes virtuelles classiques : elles peuvent être redéfinies dans les classes filles, et l'appel est résolu dynamiquement (polymorphisme).perimetre() = 0est 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'argumentsne 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 leconstqui 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.
À 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 :
- Implémenter des getters
getX()etgetY()dans Point. - Déclarer
xetyenprotecteddans 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 :
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
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.
deleteBonne 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
deleteoublié aprèsnew- 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.