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.

INF1010 – Contrôle périodique

Document source

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 :

  1. Il évite la copie coûteuse de l'objet (contrairement au passage par valeur), ce qui est très performant pour les gros objets.
  2. 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.

  1. cout << "debut" << endl; Affiche debut
  2. B b(2);
    • La classe B possède un attribut A att;. L'initialisation des attributs membres se fait avant le corps du constructeur de B. Puisque att n'est pas dans une liste d'initialisation, le constructeur par défaut de A est 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 objet A temporaire. 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()
  3. cout << "ligne 1" << endl; Affiche ligne 1
  4. b.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(). Comme a a été créé avec le constructeur par défaut, son attribut att vaut 0. Affiche A::afficher(0)
    • À la sortie de la méthode, l'objet local a est détruit. Affiche A::~A()
  5. cout << "ligne 2" << endl; Affiche ligne 2
  6. b.get_att().afficher();
    • La méthode get_att() retourne att par valeur. Cela crée une copie temporaire de l'objet att (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 de b.att valait 2 suite à l'instruction att = A(2);. Affiche A::afficher(2)
    • La copie temporaire est ensuite détruite. Affiche A::~A()
  7. cout << "fin" << endl; Affiche fin
  8. Destruction finale à la fin du main :
    • L'objet b est détruit. Le destructeur de la classe conteneur (B) est appelé en premier. Affiche B::~B()
    • Ensuite, les attributs membres de b sont détruits. L'attribut att (de type A) est détruit. Affiche A::~A()

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").

  • Ordinateur hérite de Appareil
  • Portable hérite de Ordinateur
  • Ordinateur possède un Processeur (composition)
  • Portable possède un Ecran (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.) :

  1. Appareil(string nom_, string date_) (classe de base de Ordinateur)
  2. Processeur(unsigned int vitesse_) (attribut membre de la classe parente Ordinateur)
  3. Ordinateur(string nom, string date, unsigned int vitesse) (classe parente de Portable)
  4. Ecran(unsigned int taille_) (attribut membre de la classe Portable)
  5. 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) de Portable 10 fois. Or, la classe Portable n'a aucun constructeur par défaut défini.
  • Ligne 3 : Cette ligne tente d'accéder à ptr->processeur. Or, l'attribut processeur a été défini en accès private à l'intérieur de la classe Ordinateur. 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 est get_vitesse() et non getVitesse().

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++ :

  1. 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.
  2. 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 bloc private (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.
  3. 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++.
  4. 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 type Joueur et qu'il faut utiliser .get_nom() pour obtenir son nom. Toujours utiliser les méthodes d'accès si les attributs sont privés.

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions