INF1010 – Contrôle périodique
Section 1 - Questions théoriques Question 1.1 - Signification du mot-clé const Dans la signature void Polygone::afficher(ostream &out) const; , le mot-clé const placé à la fin de la déclaration 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, C++ · DOCX · 2006
Section 1 - Questions théoriques
Question 1.1 - Signification du mot-clé const
Dans la signature void Polygone::afficher(ostream &out) const;, le mot-clé const placé à la fin de la déclaration indique qu'il s'agit d'une méthode constante.
Cela signifie que l'appel de cette méthode ne modifiera aucun des attributs de l'objet courant (l'instance de Polygone sur laquelle la méthode est appelée). En conséquence, le compilateur interdira à cette méthode de modifier les attributs ou d'appeler d'autres méthodes de la classe qui ne sont pas elles-mêmes déclarées const.
Question 1.2 - Utilité des membres protégés (protected)
L'accès protected sert de compromis entre private (accessible uniquement par la classe elle-même) et public (accessible par tous).
Les membres protégés restent invisibles et inaccessibles de l'extérieur de la classe, garantissant ainsi l'encapsulation. Toutefois, ils sont accessibles directement par les classes dérivées (l'héritage). Cela permet de concevoir une interface interne spécifiquement destinée aux classes enfants, sans l'exposer au reste du programme.
Question 1.3 - Application du pointeur this
Le pointeur this pointe vers l'objet courant. Ses applications principales incluent :
- Différencier les noms : Accéder à un attribut de la classe lorsqu'un paramètre de la méthode possède exactement le même nom (ex:
this->nom = nom;). - Appels en cascade : Renvoyer une référence à l'objet courant (via
return *this;) pour enchaîner les appels de méthodes (ex:obj.setA().setB();). - Auto-affectation : Vérifier qu'un objet n'est pas affecté à lui-même lors de la surcharge de l'opérateur
=(ex:if (this == &autre_objet) return *this;).
Question 1.4 - Comparaison entre liste et vecteur
- Avantage de la liste (list) : L'insertion et la suppression d'éléments (si on possède déjà un itérateur vers la position) s'effectuent en temps constant, peu importe le nombre d'éléments dans la liste, car il suffit de rediriger les pointeurs.
- Inconvénient de la liste : L'accès aléatoire est impossible (pas d'opérateur
[]). Pour accéder au N-ième élément, il faut obligatoirement parcourir la liste séquentiellement depuis le début ou la fin, ce qui est coûteux. Le vecteur, à l'inverse, offre un accès aléatoire instantané.
Question 1.5 - Argument générique non-type
Dans la programmation par patrons (templates), un argument générique non-type est un paramètre de template qui attend une valeur fixe (comme un entier) plutôt qu'un type (comme int ou une classe).
Par exemple, dans template <int LIGNES>, LIGNES est un argument non-type. Il permet de générer des versions de la classe ou de la fonction avec cette valeur constante définie à la compilation.
Question 1.6 - Passage par référence constante
Le passage par référence constante (const Type& paramètre) offre le meilleur des deux mondes entre le passage par valeur et le passage par référence :
- Il évite la copie coûteuse de l'objet (contrairement au passage par valeur), ce qui est très performant pour les gros objets.
- Il garantit que la fonction appelée ne pourra pas modifier l'objet d'origine (grâce au
const), assurant la même sécurité qu'un passage par valeur.
Section 2 - Composition d'objets
Analyse de l'exécution
Ce programme illustre l'ordre d'appel des constructeurs et destructeurs lors de la création d'objets contenant d'autres objets, ainsi que la gestion des objets temporaires. Voici la trace détaillée.
cout << "debut" << endl;Affiche debutB b(2);- La classe
Bpossède un attributA att;. L'initialisation des attributs membres se fait avant le corps du constructeur deB. Puisqueattn'est pas dans une liste d'initialisation, le constructeur par défaut deAest appelé. Affiche A::A() - Ensuite, on entre dans le corps du constructeur
B::B(int a). Affiche B::B(int 2) - On exécute
att = A(2);. Cela crée d'abord un objetAtemporaire. Affiche A::A(int 2) - Cet objet temporaire est affecté à
att, puis il est immédiatement détruit à la fin de l'instruction. Affiche A::~A()
- La classe
cout << "ligne 1" << endl;Affiche ligne 1b.afficher();- On entre dans
B::afficher(). Affiche B::afficher() - La méthode crée un objet local
A a;. Affiche A::A() - Appel de
a.afficher(). Commeaa été créé avec le constructeur par défaut, son attributattvaut 0. Affiche A::afficher(0) - À la sortie de la méthode, l'objet local
aest détruit. Affiche A::~A()
- On entre dans
cout << "ligne 2" << endl;Affiche ligne 2b.get_att().afficher();- La méthode
get_att()retourneattpar valeur. Cela crée une copie temporaire de l'objetatt(via le constructeur de copie généré par défaut par le compilateur, qui n'affiche rien). - On appelle
afficher()sur cette copie. L'attribut deb.attvalait 2 suite à l'instructionatt = A(2);. Affiche A::afficher(2) - La copie temporaire est ensuite détruite. Affiche A::~A()
- La méthode
cout << "fin" << endl;Affiche fin- Destruction finale à la fin du
main:- L'objet
best détruit. Le destructeur de la classe conteneur (B) est appelé en premier. Affiche B::~B() - Ensuite, les attributs membres de
bsont détruits. L'attributatt(de typeA) est détruit. Affiche A::~A()
- L'objet
Affichage complet attendu :
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()
Section 3 - Implémentation de classes et vecteur
Question 3.a - Implémentation de la classe Joueur
Voici la définition et l'implémentation de la classe Joueur. On utilise des chaînes de caractères standards de la bibliothèque <string>.
#include <string>
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; // Ajout du const car la méthode ne modifie pas l'objet
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 sur la correction : Bien que le corrigé original omette le const à la fin de la déclaration de operator<, il est fortement recommandé (et plus exact en C++) de le déclarer const puisqu'une comparaison ne doit pas modifier l'objet de gauche.
Question 3.b - Surcharge de l'opérateur <
Cette méthode permet de comparer deux joueurs selon leur numéro pour déterminer le "plus petit".
bool Joueur::operator<(const Joueur& j) const {
return numero < j.numero;
}
Question 3.c - Interface et implémentation de la classe Equipe
L'équipe maintient une référence vers un capitaine et un vecteur de joueurs.
Correction importante par rapport au solutionnaire : Le code du corrigé de l'examen pour retirer_joueur contient une erreur de compilation (joueurs[i] = joueurs[taille-1]; où taille n'a pas été défini dans la méthode). Je corrige cette erreur ci-dessous en utilisant joueurs.back(), qui récupère directement le dernier élément. Cette méthode écrase l'élément à retirer par le dernier élément du vecteur, puis supprime le dernier élément, réalisant une suppression en O(1) puisque l'ordre importe peu.
#include <vector>
#include <iostream>
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& nom_, Joueur& capitaine_)
: nom(nom_), capitaine(capitaine_) {
}
void Equipe::ajouter_joueur(const Joueur& joueur) {
joueurs.push_back(joueur);
}
void Equipe::retirer_joueur(const string& nom_recherche) {
for (size_t i = 0; i < joueurs.size(); ++i) {
if (joueurs[i].get_nom() == nom_recherche) {
// L'ordre n'important pas, on remplace l'élément supprimé par le dernier
// Cela corrige le bogue d'usage de la variable non déclarée 'taille'
joueurs[i] = joueurs.back();
joueurs.pop_back();
return;
}
}
}
void Equipe::afficher() const {
cout << "Equipe " << nom << endl;
cout << "capitaine: " << capitaine.get_nom() << endl;
size_t taille = joueurs.size();
for (size_t i = 0; i < taille; ++i) {
cout << joueurs[i].get_nom() << " " << joueurs[i].get_numero() << endl;
}
}
Section 4 - Allocation dynamique
Question 4.a - Écriture de la fonction main
Puisque le programme exige une allocation dynamique, nous devons utiliser new pour instancier nos objets, et surtout nettoyer la mémoire à la fin de notre programme avec delete. En C++ (avant C++11), les pointeurs nuls s'indiquent classiquement avec NULL ou 0.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// (On suppose que les classes Individu et Famille sont déclarées ici)
int main() {
// Allocation dynamique des individus
// On passe NULL (ou 0) pour les parents inconnus
Individu* emile = new Individu("Emile", 54, NULL, NULL);
Individu* berthe = new Individu("Berthe", 50, NULL, NULL);
Individu* eric = new Individu("Eric", 33, emile, berthe);
Individu* anne = new Individu("Anne", 34, emile, berthe);
Individu* max = new Individu("Maxime", 12, eric, NULL);
// Allocation dynamique de la famille
Famille* girard = new Famille("Girard");
// Ajout des membres à la famille
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 obligatoire de la mémoire (delete)
delete emile;
delete berthe;
delete eric;
delete anne;
delete max;
delete girard;
return 0;
}
Question 4.b - Raison de la création / destruction externe des Individus
La relation entre Famille et Individu est une relation d'agrégation et non de composition.
Une famille ne "possède" pas l'existence propre d'un individu : un individu (comme Émile) existe indépendamment de la structure familiale et pourrait appartenir à plusieurs autres entités ou arbres (par exemple, appartenir également à la famille de sa propre mère). Étant donné que Famille n'est pas le propriétaire exclusif de l'individu, ce n'est pas à la famille de le créer ou de détruire l'objet en mémoire. L'instanciation et la destruction doivent donc être gérées par l'entité qui orchestre le système (ici, le main).
Section 5 - Héritage
Question 5.1 - Diagramme de classes
Le diagramme correspond à une modélisation utilisant à la fois l'héritage (flèches "est un") et la composition (flèches "possède un").
Ordinateurhérite deAppareilPortablehérite deOrdinateurOrdinateurpossède unProcesseur(composition)Portablepossède unEcran(composition)
| Classe de base | Relation | Classe / Composant |
|---|---|---|
| Appareil | est hérité par | Ordinateur |
| Ordinateur | possède un | Processeur (attribut privé) |
| Ordinateur | est hérité par | Portable |
| Portable | possède un | Ecran (attribut privé) |
Question 5.2 - Implémentation des constructeurs
L'ordre et la syntaxe de l'initialisation sont cruciaux. Il faut appeler le constructeur de la classe parente et des objets membres dans la liste d'initialisation.
Correction par rapport au solutionnaire source : Le solutionnaire proposait Ordinateur :: Ordinateur(...) : Appareil(nom, date), vitesse(vitesse_) { }. C'est une erreur de code invalidante, car Ordinateur ne possède pas d'attribut vitesse, il possède un membre processeur de type Processeur. Il faut donc initialiser l'objet membre processeur !
Processeur::Processeur(unsigned int vitesse_)
: vitesse(vitesse_) {
}
Ecran::Ecran(unsigned int taille_)
: taille(taille_) {
}
Appareil::Appareil(string nom_, string date_)
: nom(nom_), date(date_) {
}
// CORRECTION : On initialise 'processeur', pas 'vitesse'
Ordinateur::Ordinateur(string nom, string date, unsigned int vitesse_)
: Appareil(nom, date), processeur(vitesse_) {
}
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
Lors de l'instanciation de Portable portable(...), la construction se fait "de l'intérieur vers l'extérieur", selon l'ordre strict suivant (classe de base, attributs de classe de base, classe enfant, attributs classe enfant, etc.) :
Appareil(string nom_, string date_)(classe de base de Ordinateur)Processeur(unsigned int vitesse_)(attribut membre de la classe parente Ordinateur)Ordinateur(string nom, string date, unsigned int vitesse)(classe parente de Portable)Ecran(unsigned int taille_)(attribut membre de la classe Portable)Portable(string nom, string date, unsigned int vitesse, unsigned int taille_ecran)(la classe finale elle-même)
Question 5.4 - Instructions invalides dans le programme
1 Portable lesPortables[10];
2 Ordinateur* ptr = new Ordinateur("ABC", "10/10/06", 2333);
3 cout << ptr->processeur.getVitesse() << endl;
4 delete ptr;
Il y a deux lignes invalides :
- Ligne 1 : Cette ligne tente de créer un tableau de 10 objets
Portable. Pour y parvenir, le compilateur doit appeler le constructeur par défaut (sans paramètres) dePortable10 fois. Or, la classePortablen'a aucun constructeur par défaut défini. - Ligne 3 : Cette ligne tente d'accéder à
ptr->processeur. Or, l'attributprocesseura été défini en accèsprivateà l'intérieur de la classeOrdinateur. Il est donc impossible d'y accéder depuis l'extérieur (le programme principal). De plus, le nom correct de la méthode d'accès défini dans la classe processeur estget_vitesse()et nongetVitesse().
Section 6 - Programmation générique et conteneurs
Question 6.1 - Écriture de la fonction afficherVector
Cette fonction modèle (template) permet de recevoir un vecteur contenant n'importe quel type de donnée T et d'en afficher le contenu.
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
void afficherVector(const vector<T>& vecteur) {
// On parcourt tout le vecteur
for (size_t i = 0; i < vecteur.size(); i++) {
cout << vecteur[i] << endl;
}
}
Question 6.2 - Validation des types passés au vecteur
On ne peut pas y passer n'importe quel type de vecteur sans condition.
Pour que cette fonction compile, il faut que l'instruction cout << vecteur[i] soit valide. Par conséquent, le type de donnée T stocké dans le vecteur doit obligatoirement définir une surcharge de l'opérateur d'insertion de flux (operator<<) pour le flux de sortie (ostream). Si T est une classe personnalisée et que operator<< n'a pas été surchargé, le compilateur rejettera l'appel à la fonction.
Section 7 - Question bonus
Fonctionnalité de la fonction Mystère
void Fonction_Mystere(int& a, int& b) {
a = a + b;
b = a - b;
a = a - b;
}
Cette fonction permet d'échanger le contenu de deux variables entières (a et b) sans nécessiter l'utilisation d'une troisième variable temporaire en mémoire. L'échange se base sur des opérations d'addition et de soustraction mathématiques.
(Attention en pratique : ce type d'échange peut mener à des dépassements de capacité / "overflow" sur de très grands nombres, un aspect dangereux évité par la fonction standard std::swap).
Méthode
Pour réussir ce type d'examen portant sur la programmation orientée objet en C++ :
- Exécution pas à pas : Face à un code à tracer (comme la Section 2), munissez-vous d'un brouillon. Notez l'état de chaque variable, mais surtout, soyez impitoyable avec la mécanique du C++ : l'initialisation des attributs est une étape qui s'exécute avant l'ouverture de l'accolade
{d'un constructeur. Les objets temporaires sont souvent invisibles, mais ils existent. - Analysez bien la définition des classes : Ne supposez pas qu'un attribut s'appelle "vitesse" juste parce que le paramètre du constructeur s'appelle
vitesse_. Regardez dans le blocprivate(Section 5.2). Le correcteur intègre volontairement de tels pièges pour s'assurer que vous regardez la structure de classe en détail. - Apprenez le cycle de vie : Savoir l'ordre exact de création dans un contexte d'héritage (parent d'abord, attributs ensuite, objet enfin) et l'ordre de destruction (exactement l'inverse) est fondamental. C'est l'un des principes directeurs du C++.
- Lisez l'ensemble du fichier de classes fourni : Dans l'exercice 3.c, l'accès à un joueur nécessitait de comprendre que
joueurs[i]est de typeJoueuret qu'il faut utiliser.get_nom()pour obtenir son nom. Toujours utiliser les méthodes d'accès si les attributs sont privés.
Commentaires
Aucun commentaire pour le moment. Posez la première question.