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.

INF1010 – Contrôle périodique

Document source

INF1010 – Contrôle périodique

Programming, Math, etc. · PDF · 10 pages · 2006

Afficher l'aperçu du document

Consulter le document original →

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 utilise if (this != &autre_objet) pour éviter qu'un objet ne se détruise lui-même lors d'une affectation comme a = 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->nom permet 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 :

  1. 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.
  2. Sécurité : Le mot-clé const garantit 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 :

  1. cout << "debut" << endl; affiche debut
  2. B b(2); : Création de l'objet b.
    • Avant d'exécuter B::B(int a), le compilateur initialise l'attribut att de type A avec 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 temporaire A est 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()
  3. cout << "ligne 1" << endl; affiche ligne 1
  4. b.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'attribut att local 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()
  5. cout << "ligne 2" << endl; affiche ligne 2
  6. b.get_att().afficher(); :
    • get_att() retourne l'objet att par valeur. Une copie temporaire de l'objet A est donc créée (le constructeur de copie par défaut ne fait aucun affichage). L'attribut att de 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()
  7. cout << "fin" << endl; affiche fin
  8. Fin du programme. L'objet b est détruit.
    • Appel du destructeur de B => affiche B::~B()
    • Destruction automatique de l'attribut membre att => affiche A::~A()

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

  1. Appareil(string nom_, string date_); (Héritage parent de Ordinateur)
  2. Processeur(unsigned int vitesse_); (Objet membre contenu dans Ordinateur)
  3. Ordinateur(string nom, string date, unsigned int vitesse); (Héritage parent de Portable)
  4. Ecran(unsigned int taille_); (Objet membre contenu dans Portable)
  5. 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 classe Portable (ni pour ses classes parentes). Le compilateur bloquera donc ici.
  • Instruction 3 invalide : cout << ptr->processeur.getVitesse() << endl;
    • Raison : L'attribut processeur a été déclaré dans la section private: de la classe Ordinateur. 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 est get_vitesse() et non getVitesse().

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.

  1. a = a + b => a contient maintenant (A + B).
  2. b = a - b => b devient (A + B) - B = A. (b possède maintenant la valeur initiale de a).
  3. a = a - b => a devient (A + B) - A = B. (a possède maintenant la valeur initiale de b). 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 :

  1. 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.
  2. Gestion de la mémoire : Dès que vous lisez ou écrivez le mot new, écrivez mentalement ou sur votre copie le delete correspondant. En C++, la fuite de mémoire est une erreur grave d'implémentation.
  3. Encapsulation stricte : Portez une attention particulière à private, protected et const. 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.

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