INF1010 – Contrôle périodique
1. Questions théoriques Cette section vérifie votre compréhension des concepts fondamentaux du C++ liés à l'orientation objet et à la gestion de la mémoire. Question 1.1 - Le mot-clé const Énoncé : Que signifie le mot const dans void Polygone::afficher(ostream &out) const; ? Le mot-clé const placé à la fin de la signature d'une méthode indique qu'il s'agit d'une méthode constante.
D'après le document INF1010 – Contrôle périodique
Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Document source
Programming, Math, etc. · PDF · 10 pages · 2006
Afficher l'aperçu du document
1. Questions théoriques
Cette section vérifie votre compréhension des concepts fondamentaux du C++ liés à l'orientation objet et à la gestion de la mémoire.
Question 1.1 - Le mot-clé const
Énoncé : Que signifie le mot const dans void Polygone::afficher(ostream &out) const; ?
Le mot-clé const placé à la fin de la signature d'une méthode indique qu'il s'agit d'une méthode constante. Cela signifie qu'elle garantit de ne modifier aucun attribut de l'objet (l'instance de Polygone) sur lequel elle est appelée.
De plus, le compilateur appliquera cette restriction strictement : à l'intérieur de cette méthode, il est interdit d'appeler d'autres méthodes de la classe qui ne sont pas elles-mêmes déclarées const. C'est une excellente pratique pour les méthodes d'affichage ou d'accès (getters).
Question 1.2 - L'utilité des membres protégés
Énoncé : Quelle est l'utilité des membres protégés (protected) dans une classe ?
L'encapsulation protected offre un niveau d'accès intermédiaire entre private (accessible uniquement par la classe elle-même) et public (accessible par tous).
Les membres protégés restent invisibles et inaccessibles pour le code extérieur (comme le programme principal), mais ils sont accessibles aux classes dérivées (les classes enfants dans une hiérarchie d'héritage) et aux classes/fonctions amies (friend). Cela permet d'exposer une interface d'implémentation spécifique pour faciliter la construction des sous-classes tout en maintenant la sécurité vis-à-vis de l'extérieur.
Question 1.3 - Applications du pointeur this
Énoncé : Donnez une application du pointeur this.
Le pointeur this est un pointeur implicite passé à toutes les méthodes non statiques, qui pointe vers l'objet courant. Voici les applications principales :
- Les appels en cascade : En retournant
*this(souvent par référence) à la fin d'une méthode, on peut enchaîner les appels (ex:objet.methode1().methode2();). - Vérification de l'auto-affectation : Dans la surcharge de l'opérateur
=, on utiliseif (this != &autre_objet)pour éviter qu'un objet ne se détruise lui-même lors d'une affectation commea = a;. - Résolution d'ambiguïté (masquage) : Lorsqu'un paramètre ou une variable locale porte le même nom qu'un attribut de la classe,
this->nompermet de préciser que l'on parle de l'attribut.
Question 1.4 - Liste vs Vecteur
Énoncé : Donnez un avantage et un inconvénient de l'utilisation d'une liste vis-à-vis d'un vecteur.
- Avantage de la liste (std::list) : L'ajout ou le retrait d'un élément à n'importe quelle position (une fois l'endroit ciblé) s'effectue en temps constant, car il suffit de réassigner quelques pointeurs. Le coût ne dépend pas du nombre d'éléments, contrairement au vecteur qui pourrait nécessiter de décaler tous les éléments suivants.
- Inconvénient de la liste : L'accès aléatoire est impossible (l'opérateur
[]n'existe pas). Pour accéder au 10ème élément, il faut obligatoirement parcourir les 9 précédents de manière séquentielle.
Question 1.5 - Argument générique non type
Énoncé : Qu'est-ce qu'un argument générique non type ?
Dans les patrons (templates) en C++, un argument générique est généralement un type (ex: template <typename T>). Cependant, un argument non type est une valeur constante connue à la compilation, dont le type est fixé, mais la valeur est générique.
Par exemple : template <int ROWS> permet de créer des classes ou des fonctions paramétrées par un nombre entier (utile pour fixer la taille statique d'un tableau interne par exemple).
Question 1.6 - Passage par référence constante
Énoncé : À quoi sert le passage par référence constante ?
Il combine deux avantages :
- Performance : Le passage par référence (
&) évite la copie complète de l'objet, ce qui économise du temps et de la mémoire, surtout pour les gros objets. - Sécurité : Le mot-clé
constgarantit que la fonction appelée ne modifiera pas l'objet original. Cela simule donc la sécurité d'un passage par valeur, mais avec l'efficacité d'un passage par référence.
2. Composition d'objets
Énoncé : Donnez l'affichage que produit le code fourni.
Pour trouver la solution sans se tromper, il faut suivre pas à pas l'exécution de la fonction main() et se rappeler que lorsqu'une classe contient un objet (composition), le constructeur de cet objet membre est appelé avant le corps du constructeur de la classe hôte.
Voici l'analyse détaillée qui mène au résultat :
cout << "debut" << endl;affiche debutB b(2);: Création de l'objetb.- Avant d'exécuter
B::B(int a), le compilateur initialise l'attributattde typeAavec son constructeur par défaut. - Appel de
A::A()=> affiche A::A() - Entrée dans le corps de
B::B(int a)=> affiche B::B(int 2) - Exécution de
att = A(2);: Un objet temporaireAest créé. - Appel de
A::A(int a)=> affiche A::A(int 2) - L'affectation se fait, puis l'objet temporaire est détruit.
- Appel de
A::~A()=> affiche A::~A()
- Avant d'exécuter
cout << "ligne 1" << endl;affiche ligne 1b.afficher();:- Début de la méthode => affiche B::afficher()
- Création de la variable locale
A a;=> affiche A::A() - Appel de
a.afficher()(l'attributattlocal est à 0 car initialisé par le constructeur par défaut) => affiche A::afficher(0) - Fin de la méthode, destruction de l'objet local
a=> affiche A::~A()
cout << "ligne 2" << endl;affiche ligne 2b.get_att().afficher();:get_att()retourne l'objetattpar valeur. Une copie temporaire de l'objetAest donc créée (le constructeur de copie par défaut ne fait aucun affichage). L'attributattde cette copie vaut 2.- Appel de
afficher()sur cette copie => affiche A::afficher(2) - L'instruction se termine, la copie temporaire est détruite => affiche A::~A()
cout << "fin" << endl;affiche fin- Fin du programme. L'objet
best détruit.- Appel du destructeur de
B=> affiche B::~B() - Destruction automatique de l'attribut membre
att=> affiche A::~A()
- Appel du destructeur de
Résultat final de l'affichage :
debut
A::A()
B::B(int 2)
A::A(int 2)
A::~A()
ligne 1
B::afficher()
A::A()
A::afficher(0)
A::~A()
ligne 2
A::afficher(2)
A::~A()
fin
B::~B()
A::~A()
3. Implémentation de classes et vecteur
Question 3.a - Classe Joueur
Énoncé : Donnez la définition et l'implémentation de la classe Joueur.
Voici le code. J'utilise la liste d'initialisation dans le constructeur, ce qui est la méthode recommandée en C++ pour initialiser les attributs.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Joueur
{
public:
Joueur(const string& n, int num);
string get_nom() const;
int get_numero() const;
bool operator<(const Joueur& j) const; // Bonne pratique: la méthode doit être const
private:
string nom;
int numero;
};
// Implémentation
Joueur::Joueur(const string& n, int num)
: nom(n), numero(num)
{
}
string Joueur::get_nom() const
{
return nom;
}
int Joueur::get_numero() const
{
return numero;
}
Note pédagogique : Bien que le solutionnaire d'origine ne mette pas de const à la fin de la déclaration de operator<, c'est une erreur conceptuelle commune. L'opérateur de comparaison ne modifie pas l'objet courant, il doit donc être déclaré constant.
Question 3.b - Surcharge de l'opérateur inférieur
Énoncé : Implémentez la méthode qui surcharge l'opérateur < en comparant les numéros.
bool Joueur::operator<(const Joueur& j) const
{
return numero < j.numero;
}
Question 3.c - Classe Equipe
Énoncé : Implémentez l'interface de la classe Equipe.
Il faut faire attention à la méthode de retrait. Puisque l'ordre n'est pas important (comme spécifié dans l'énoncé), la technique la plus rapide pour retirer un élément d'un vector sans décaler toute la suite est de copier le dernier élément à la place de l'élément à supprimer, puis de retirer le dernier élément (pop_back).
Correction par rapport au solutionnaire source : Le code fourni dans le document original utilisait une variable taille non déclarée dans la méthode retirer_joueur. J'ai corrigé ce bug d'implémentation en utilisant joueurs.size().
class Equipe
{
public:
Equipe(const string& nom_, Joueur& capitaine_);
void ajouter_joueur(const Joueur& joueur);
void retirer_joueur(const string& nom);
void afficher() const;
private:
string nom;
Joueur& capitaine;
vector<Joueur> joueurs;
};
// Implémentation
Equipe::Equipe(const string& n, Joueur& cap)
: nom(n), capitaine(cap)
{
}
void Equipe::ajouter_joueur(const Joueur& j)
{
joueurs.push_back(j);
}
void Equipe::retirer_joueur(const string& nom_cherche)
{
for (size_t i = 0; i < joueurs.size(); ++i) {
if (joueurs[i].get_nom() == nom_cherche) {
// Astuce O(1) pour supprimer: on remplace par le dernier élément
joueurs[i] = joueurs[joueurs.size() - 1];
// On retire le dernier élément qui est maintenant en double
joueurs.pop_back();
return; // Le joueur est retiré, on quitte la fonction
}
}
}
void Equipe::afficher() const
{
cout << "Equipe " << nom << endl;
cout << "capitaine: " << capitaine.get_nom() << endl;
for (size_t i = 0; i < joueurs.size(); ++i) {
cout << joueurs[i].get_nom() << " " << joueurs[i].get_numero() << endl;
}
}
4. Allocation dynamique
Question 4.a - Programme principal
Énoncé : Implémentez le main en allouant dynamiquement les objets pour obtenir l'arbre généalogique décrit.
Tout objet instancié dynamiquement (avec le mot-clé new) doit obligatoirement être détruit manuellement à la fin du programme (avec delete) pour éviter les fuites de mémoire. Les parents inexistants doivent être initialisés avec le pointeur nul (0 ou NULL en C++03).
int main()
{
// Création des individus dynamiquement
Individu* emile = new Individu("Emile", 54, 0, 0);
Individu* berthe = new Individu("Berthe", 50, 0, 0);
Individu* eric = new Individu("Eric", 33, emile, berthe);
Individu* anne = new Individu("Anne", 34, emile, berthe);
Individu* max = new Individu("Maxime", 12, eric, 0);
// Création de la famille dynamiquement
Famille* girard = new Famille("Girard");
// Ajout des membres (on passe les pointeurs)
girard->ajouter_membre(emile);
girard->ajouter_membre(berthe);
girard->ajouter_membre(eric);
girard->ajouter_membre(anne);
girard->ajouter_membre(max);
// Affichage
girard->afficher();
// Libération de la mémoire dans l'ordre inverse des dépendances conceptuelles
delete girard;
delete max;
delete anne;
delete eric;
delete berthe;
delete emile;
return 0;
}
Question 4.b - Gestion de la destruction
Énoncé : Expliquez pourquoi ce n'est pas la classe Famille qui est responsable de détruire les individus.
Il s'agit de différencier une agrégation d'une composition.
La relation entre la classe Famille et la classe Individu est une relation d'agrégation (la famille connaît des individus), et non une relation de composition (la famille ne possède pas exclusivement les individus).
Les objets Individu sont créés à l'extérieur de la classe Famille (dans le main). Ils pourraient très bien appartenir à plusieurs familles différentes ou survivre à la destruction de l'objet Famille. En C++, c'est l'entité qui s'est chargée d'appeler le new qui doit être responsable d'appeler le delete. Ici, c'est donc le rôle de la fonction main().
5. Héritage
Question 5.1 - Diagramme de classes
Énoncé : Dessinez le diagramme de classes correspondant.
L'énoncé montre des liens d'héritage (public Appareil, public Ordinateur) et des liens de composition (la présence d'attributs de type classe).
Voici la structure du diagramme :
- La classe de base au sommet est
Appareil. Ordinateurhérite deAppareil.Portablehérite deOrdinateur.Ordinateurpossède une relation de composition versProcesseur(il en contient un).Portablepossède une relation de composition versEcran(il en contient un).
(En UML formel, les héritages sont des flèches avec des triangles vides pointant vers le parent, et les compositions sont des losanges pleins du côté du contenant).
Question 5.2 - Constructeurs des classes
Énoncé : Implémentez tous les constructeurs en utilisant les listes d'initialisation.
Correction par rapport au solutionnaire source : Le solutionnaire d'origine contenait une erreur syntaxique sur la classe Ordinateur. Il tentait d'initialiser vitesse(vitesse_) alors que Ordinateur ne possède aucun attribut nommé vitesse (il possède un attribut processeur de type Processeur). Le code ci-dessous répare cette erreur pour assurer que le code compile et s'exécute correctement.
Processeur::Processeur(unsigned int vitesse_)
: vitesse(vitesse_)
{
}
Ecran::Ecran(unsigned int taille_)
: taille(taille_)
{
}
Appareil::Appareil(string nom_, string date_)
: nom(nom_), date(date_)
{
}
Ordinateur::Ordinateur(string nom, string date, unsigned int vitesse_)
: Appareil(nom, date), processeur(vitesse_) // Correction ici !
{
}
Portable::Portable(string nom, string date, unsigned int vitesse, unsigned int taille_ecran)
: Ordinateur(nom, date, vitesse), ecran(taille_ecran)
{
}
Question 5.3 - Ordre d'invocation des constructeurs
Énoncé : Quels sont les constructeurs invoqués (dans l'ordre) lors de Portable portable("A inc.", "05/09/06", 3100, 17); ?
Règle d'or en C++ : la construction se fait toujours de bas en haut (des parents les plus éloignés jusqu'à l'objet lui-même), en construisant les objets membres avant d'entrer dans le corps du constructeur courant.
L'ordre exact des appels est le suivant :
Appareil(string nom_, string date_);(Héritage parent de Ordinateur)Processeur(unsigned int vitesse_);(Objet membre contenu dans Ordinateur)Ordinateur(string nom, string date, unsigned int vitesse);(Héritage parent de Portable)Ecran(unsigned int taille_);(Objet membre contenu dans Portable)Portable(string nom, string date, unsigned int vitesse, unsigned int taille_ecran);(L'objet courant ciblé)
Question 5.4 - Instructions invalides
Énoncé : Précisez quelle(s) instruction(s) est(sont) invalide(s) et pourquoi.
Le programme principal proposé comporte quatre instructions.
- Instruction 1 invalide :
Portable lesPortables[10];- Raison : Cette ligne demande de créer un tableau de 10 objets
Portable. Pour cela, le compilateur a besoin d'invoquer un constructeur sans paramètre (le constructeur par défaut). Or, aucun constructeur par défaut n'a été défini pour la classePortable(ni pour ses classes parentes). Le compilateur bloquera donc ici.
- Raison : Cette ligne demande de créer un tableau de 10 objets
- Instruction 3 invalide :
cout << ptr->processeur.getVitesse() << endl;- Raison : L'attribut
processeura été déclaré dans la sectionprivate:de la classeOrdinateur. Le programme principal (main) ne fait pas partie de la classe et ne peut donc pas accéder directement à cet attribut privé. De plus, la méthode pour lire la vitesse dans le code défini estget_vitesse()et nongetVitesse().
- Raison : L'attribut
Les instructions 2 (new Ordinateur(...)) et 4 (delete ptr;) sont tout à fait valides.
6. Programmation générique et conteneurs
Question 6.1 - Fonction afficherVector
Énoncé : Écrivez la fonction générique afficherVector.
On utilise un paramètre template T pour que la fonction accepte un vecteur contenant n'importe quel type de donnée.
template <typename T>
void afficherVector(const vector<T>& vecteur)
{
// Parcours et affichage classique d'un vecteur
for (size_t i = 0; i < vecteur.size(); ++i) {
cout << vecteur[i] << endl;
}
}
Question 6.2 - Contraintes sur les types
Énoncé : Peut-on passer à cette fonction un vecteur de n'importe quel type ? Justifiez.
Non, le patron de fonction (template) n'acceptera pas littéralement n'importe quel type.
Pour que la compilation réussisse, il faut que le type T supporte toutes les opérations appliquées sur lui à l'intérieur de la fonction générique. Ici, la ligne cout << vecteur[i] << endl; exige que le type T possède une surcharge de l'opérateur de flux de sortie (operator<<).
Si vous créez une classe maison sans surcharger cet opérateur, le compilateur renverra une erreur lors de l'instanciation du template.
7. Question bonus
Énoncé : Que fait la fonction mystère suivante ?
void Fonction_Mystere(int& a, int& b)
{
a = a + b;
b = a - b;
a = a - b;
}
Cette fonction échange (swap) la valeur des deux variables entières, et ce, sans utiliser de troisième variable temporaire.
Preuve mathématique du comportement :
Si les valeurs initiales sont A et B.
a = a + b=>acontient maintenant (A + B).b = a - b=>bdevient (A + B) - B = A. (bpossède maintenant la valeur initiale dea).a = a - b=>adevient (A + B) - A = B. (apossède maintenant la valeur initiale deb). L'échange est parfait.
Méthode
Face à ce type de contrôle périodique, la rigueur dans le suivi des exécutions est la compétence la plus testée :
- Suivi de code manuel (Tracing) : Pour les questions de composition (Section 2) ou d'héritage, tracez au brouillon chaque appel de constructeur et destructeur. N'oubliez pas les constructeurs par défaut (souvent silencieux mais essentiels) et la règle : les membres d'une classe et ses parents sont toujours instanciés avant le constructeur de la classe elle-même.
- Gestion de la mémoire : Dès que vous lisez ou écrivez le mot
new, écrivez mentalement ou sur votre copie ledeletecorrespondant. En C++, la fuite de mémoire est une erreur grave d'implémentation. - Encapsulation stricte : Portez une attention particulière à
private,protectedetconst. Si une question demande de trouver des erreurs de code (Section 5.4), cherchez en priorité des violations de visibilité d'attributs ou des appels de méthodes non-const sur des objets constants.
Commentaires
Aucun commentaire pour le moment. Posez la première question.