Université de Sherbrooke, Structures de données (SDO)

Vous trouverez ici quelques documents et quelques liens pouvant, je l'espère, vous être utiles.

Les documents qui vous sont fournis ici le sont pour vous rendre service.

Je travaille très fort sur chacun de mes cours. Veuillez ne pas vendre (ou donner) les documents que je vous offre ici à qui que ce soit sans mon consentement. Si des abus surviennent, je vais cesser de rendre ce matériel disponible à toutes et à tous.

Si ces documents vous rendent service, faites-le moi savoir. Mon adresse de courriel est disponible à travers la page où on trouve mon horaire.

Vous trouverez sur ce site :

Documents sous forme électronique

Cliquez sur cette cible pour le plan de cours, sous forme électronique.

Contenu des séances

Ce qui suit détaille le contenu des séances du cours.

Index des séances
S00 S01 S02 S03 S04 S05 S06 S07 S08 S09
S10 S11 S12 S13 S14 S15 S16 S17 S18 S19
S20 S21 S22 S23 S24 S25 S26 S27

Notez que pour la session en cours :

Séance Jour Date Contenu

S00

LUN

29 août

Au menu :

  • Présentation du plan de cours
  • Présentation de l'enseignant et de son horaire quelque peu acrobatique
  • Présentation de la forme particulière du cours
  • Présentation du matériel de support
    • Vous devrez aller vous procurer les notes de cours du cours Structures de données – IFT339 à la coopérative où elles vous attendent avec impatience
  • Première approche avec ce mécanisme d'abstraction qu'est la classe (mot-clé class ou mot-clé struct en C++)
    • Nous utilisons les notes de cours en soutien, en travaillant à partir du chapitre 4 (voir les notes sur ce chapitre, plus bas)
    • Nous essaierons, tout au long de notre cheminement, de distinguer les concepts généraux de la POO de leur réification dans le langage C++
    • Sur le plan des modalités, nous avons fait un hybride entre présentation des concepts et leur réification dans ce langage, puisque la plupart d'entre vous ne sont pas familières ou familiers avec C++
    • Nous nous sommes arrêtés au début de la section sur le polymorphisme à la page 33. Nous poursuivrons à la séance S01

S01

MER

31 août

Au menu :

  • Poursuite de l'examen de ce mécanisme d'abstraction qu'est la classe (mot-clé class ou mot-clé struct en C++)
    • Nous utilisons les notes de cours en soutien, en travaillant à partir du chapitre 4 (voir les notes sur ce chapitre, plus bas)
    • Nous essaierons, tout au long de notre cheminement, de distinguer les concepts généraux de la POO de leur réification dans le langage C++
    • Sur le plan des modalités, nous avons fait un hybride entre présentation des concepts et leur réification dans ce langage, puisque la plupart d'entre vous ne sont pas familières ou familiers avec C++
    • Nous reprenons au début de la section sur le polymorphisme à la page 33
      • Nous avons fait plusieurs digressions, entre autres sur les notions de pointeur et de référence qui peuvent vous être étrangères si vous avez utilisé des langages sans accès direct aux objets par le passé
      • Nous avons discuté de la Sainte-Trinité
      • Nous avons discuté de sémantique de passage de paramètres
      • Pour certains aspects, nous avons utilisé le Compiler Explorer, aussi appelé godbolt du nom de son (très sympathique) auteur
    • Nous nous sommes arrêtés au début de la section sur les « fonctions de base du type abstrait » à la page 36. Nous poursuivrons à la séance S02

S02

VEN

2 sept.

Au menu :

  • Poursuite de l'examen de ce mécanisme d'abstraction qu'est la classe (mot-clé class ou mot-clé struct en C++)
    • Nous utilisons les notes de cours en soutien, en travaillant à partir du chapitre 4 (voir les notes sur ce chapitre, plus bas)
    • Nous essaierons, tout au long de notre cheminement, de distinguer les concepts généraux de la POO de leur réification dans le langage C++
    • Sur le plan des modalités, nous avons fait un hybride entre présentation des concepts et leur réification dans ce langage, puisque la plupart d'entre vous ne sont pas familières ou familiers avec C++
    • Nous reprenons au début de la section sur les « fonctions de base du type abstrait » à la page 36
      • Plusieurs des aspects des sections aujourd'hui étaient des rappels d'idées explorées lors des cours précédents
      • Nous avons discuté de surcharge d'opérateurs
      • Nous avons aussi commencé à discuter de programmation générique
    • Nous nous sommes arrêtés à la fin du chapitre 4

Pour quelques exercices, voir Exercices--serie-00.html

S03

MER

7 sept.

Au menu :

s/o

VEN

9 sept.

Pas de cours cette semaine; je serai hors du pays pour donner des cours et une conférence à CppCon. Pour suivre mes péripéties : ../../Sujets/Orthogonal/cppcon2022.html (je suis en déplacement dans cette direction aujourd'hui)

s/o

MER

14 sept.

Pas de cours cette semaine; je serai hors du pays pour donner des cours et une conférence à CppCon. Pour suivre mes péripéties : ../../Sujets/Orthogonal/cppcon2022.html

s/o

VEN

16 sept.

Pas de cours cette semaine; je serai hors du pays pour donner des cours et une conférence à CppCon. Pour suivre mes péripéties : ../../Sujets/Orthogonal/cppcon2022.html

S04

LUN

19 sept.

Au menu :

  • Retour sur la série 00 d'exercices
    • Pas besoin de tout avoir fait, ne vous en faites pas, mais l'idée est de voir avec vous ce qui demande plus d'explications et ce qui se passe bien pour vous
  • Questions sur le devoir 1
  • Idée d'itérateur
  • Écrire un itérateur pour un conteneur
    • Exemple avec une liste simplement chaînée

Le code auquel nous en sommes arrivés fut :

// sliste.h

#ifndef SLISTE_H
#define SLISTE_H

#include <cstddef> // std::size_t
#include <utility> // std::swap
#include <iostream>

template <class T>
   class sliste {
      // p-ê mouvement (ctor, affectation)
      struct noeud {
         T valeur;
         noeud *prochain = nullptr;
         noeud(const T &valeur) : valeur{ valeur } {
         }
      };
      noeud *tete = nullptr;
      noeud *queue = nullptr;
      std::size_t nelems = 0;
   public:
      class iterator {
         noeud *cur = nullptr;
      public:
         iterator() = default;
         iterator(noeud *p) : cur{ p } {
         }
         T &operator*() {
            return cur->valeur;
         }
         const T &operator*() const {
            return cur->valeur;
         }
         iterator &operator++() { // version préfixe : ++i
            cur = cur->prochain;
            return *this;
         }
         iterator operator++(int) { // version suffixe : i++
            iterator temp = *this;
            operator++();
            return temp;
         }
         bool operator==(const iterator &autre) const {
            return cur == autre.cur;
         }
         bool operator!=(const iterator &autre) const {
            return !(*this == autre);
         }
      };
      // complexité : O(1) donc constant
      bool empty() const {
         return tete == nullptr;
      }
      sliste() = default;
      // complexité : O(n)
      sliste(const sliste &autre) {
         //for (noeud *p = autre.tete; p != nullptr; p = p->prochain)
         //   push_back(p->valeur);
         for (const T &val : autre)
            push_back(val);
      }
      // complexité : O(1) donc constant
      void swap(sliste &autre) {
         using std::swap;
         swap(tete, autre.tete);
         swap(queue, autre.queue);
         swap(nelems, autre.nelems);
      }
      // complexité : O(ctor de copie)... donc linéaire
      sliste& operator=(const sliste &autre) {
         sliste{ autre }.swap(*this);
         return *this;
      }
      // complexité : O(n) donc linéaire
      void clear() {
         while (!empty())
            pop_front();
      }
      // complexité : O(n) donc linéaire
      ~sliste() {
         clear();
      }
      // précondition : !empty()
      // complexité : O(1)
      void pop_front() {
         noeud *p = tete->prochain;
         delete tete;
         --nelems;
         tete = p;
         if (tete == nullptr)
            queue = nullptr;
      }
      // complexité : O(1) donc constant
      void push_front(const T &val) {
         noeud *p = new noeud{ val };
         p->prochain = tete;
         tete = p;
         ++nelems;
         if (queue == nullptr)
            queue = p;
      }
      // complexité : O(1) donc linéaire
      void push_back(const T &val) {
         if (empty())
            push_front(val);
         else {
            noeud *p = new noeud{ val };
            queue->prochain = p;
            queue = p;
            ++nelems;
         }
      }
      // précondition : !empty()
      // complexité : O(1) donc constant
      T& front() {
         return tete->valeur;
      }
      // précondition : !empty()
      // complexité : O(1) donc constant
      const T& front() const {
         return tete->valeur;
      }
      // précondition : !empty()
      // complexité : O(1) donc constant
      T &back() {
         return queue->valeur;
      }
      // précondition : !empty()
      // complexité : O(1) donc constant
      const T &back() const {
         return queue->valeur;
      }
      // complexité : O(1)
      std::size_t size() const {
         return nelems;
      }
      //// NOTE : ceci est une patch temporaire, le temps qu'on
      //// ait une meilleure interface... avec des itérateurs
      //void afficher() {
      //   for (noeud *p = tete; p != nullptr; p = p->prochain)
      //      std::cout << p->valeur << ' ';
      //}
      // 
      iterator begin() {
         return { tete };
      }
      iterator end() {
         return {};
      }
      bool operator==(const sliste &autre) const {
         if (size() != autre.size()) return false;
         for (iterator p = begin(), q = autre.begin();
            p != end(); ++p, ++q)
            if (*p != *q)
               return false;
         return true;
      }
   };

#endif

// principal.cpp

#include <string>

int main() {
   using namespace std;
   cout << "Taille en bytes d'un sliste<int> : " << sizeof(sliste<int>) << endl;
   int tab[]{ 2,3,5,7,11 };
   sliste<int> lst;
   for (int n : tab)
      lst.push_back(n);
   // lst.afficher();
   for (int n : lst)
      cout << n << ' ';
   std::cout << '\n';
   sliste<int> ze_copie = lst;
   ze_copie.pop_front();
   for (int n : lst)
      cout << n << ' ';
   std::cout << '\n';
   for (int n : ze_copie)
      cout << n << ' ';
   std::cout << '\n';
   sliste<string> slst;
   slst.push_front("prof");
   slst.push_front("mon");
   slst.push_front("j'aime");
   for (const string &s : slst)
      cout << s << ' ';
   cout << '\n';
}

S05

MER

21 sept.

Au menu :

  • Survol des conteneurs standards de C++
    • Complexité algorithmique des opérations clés
    • Faire un choix éclairé
  • S'il reste du temps :
    • Implémenter un équivalent (naïf; pour un véritable équivalent, faudra venir me voir en IFT611 🙂) de std::vector<T>
      • Attention : std::vector<T> et le Array<T,N> de votre devoir 1 sont des classes semblables, mais différentes à plusieurs égards (en particuler, un Array<T,N> a une capacité fixe alors que la capacité d'un std::vector<T> est dynamique)

S06

VEN

23 sept.

Au menu :

  • Journée atypique car plusieurs d'entre vous ont fait le choix (légitime) de participer aux manifestations entourant les événements pour la justice climatique
  • Les cours n'étant pas suspendus, et notre groupe étant incomplet, j'ai préféré offrir une séance informelle de soutient au travail pratique... ce qui fut, je pense, utile
  • Au passage, j'ai glissé quelques bribes d'information pour divertir les gens présents. Entre autres, j'ai fait remarquer que ceci :
int main() {
   enum { N = 10'000 };
   int tab[N * N]{}; // initialisé avec des zéros... oups!
}
  • ... ne fonctionnera pas en pratique même si la taille du tableau tab est connue à la compilation, du fait que N*N*sizeof(int) est approximativement 4 Mo alors que la pile d'exécution d'un programme est typiquement de 1 Mo ou de 2 Mo (ça varie selon les systèmes d'exploitation), du moins pas défaut.
  • Cela explique d'ailleurs l'intérêt d'une classe comme le Array<T,N> de votre devoir our prendre en charge l'allocation et la libération dynamique des ressources. En effet, si nous le faisions « manuellement », il nous faudrait écrire quelque chose comme :
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros...
   // ICI : ... le programme s'exécute et il se passe "des choses" ...
   delete [] tab;
}
  • ... or ce serait une approche fragilisante, car si une instruction dans le code remplacé ici par le commentaire débutant par ICI devait lever une exception, nous risquerions de laisser fuir les ressources associées à tab
  • Aussi, j'ai laissé planer que le code suivant :
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros
   // remplir le tableau avec des valeurs impaires
   // question : que pensez-vous de ce code?
   for (int i = 0; i != N * N; ++i)
      tab[i] = 2 * i + 1;
   // ...
   delete []tab;
}
  • ... bien que fonctionnel, manque un peu d'élégance. J'ai alors soulevé que si nous remplacions le code de génération de valeurs par un appel de fonction...
int generer_impair(int n) {
   return 2 * n + 1;
}
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros
   // question : que pensez-vous de ce code?
   for (int i = 0; i != N * N; ++i)
      tab[i] = generer_impair(i);
   // ...
   delete []tab;
}
  • ... pour clarifier le propos, nous gagnerions beaucoup au change. Ensuite, si nous essayons d'élever le niveau d'abstraction de notre boucle pour remplacer le recours à un tableau et un indice par le recours à un simple pointeur, nous arriverions à une boucle moins élégante, mais plus féconde...
int generer_impair(int n) {
   return 2 * n + 1;
}
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros
   // question : que pensez-vous de ce code?
   int i = 0; // le i est un artéfact du code pour générer des impairs, et n'est
              // pas essentiel au propos ici
   for (int *p = tab; p != tab + N * N; ++p)
      *p = generer_impair(i++);
   // ...
   delete []tab;
}
  • ... car elle serait exprimée en termes d'une entité qui s'apparente à un itérateur. Sachant cela, nous pourrions abstraire le concept que modélise cette boucle (remplir une séquence de valeurs à partir d'expressions dépendant de la position des valeurs dans la séquence) en des termes plus généraux (mais avec du code qui ne compilera pas pour le moment) :
int generer_impair(int n) {
   return 2 * n + 1;
}
void remplir(debut, fin, f) {
   int n = 0;
   for (; debut != fin; ++debut)
      *debut = f(n++);
}
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros
   // question : que pensez-vous de ce code?
   remplir(tab, tab + N, generer_impair);
   // ...
   delete []tab;
}
  • ... ce qui devient, si nous utilisons la syntaxe du code générique de C++, ceci (qui compile et fonctionne!) :
int generer_impair(int n) {
   return 2 * n + 1;
}
template <class It, class F> // It pour itérateur, F pour fonction
   void remplir(debut, fin, f) {
      int n = 0;
      for (; debut != fin; ++debut)
         *debut = f(n++);
   }
int main() {
   enum { N = 10'000 };
   int * tab = new int[N * N]{}; // initialisé avec des zéros
   // question : que pensez-vous de ce code?
   remplir(tab, tab + N, generer_impair);
   // ...
   delete []tab;
}

Que pensez-vous du code client (de l'appel de la fonction remplir dans le programme principal)? Il me semble que, sur le plan de l'utilisabilité, nous avons fait un gain net... Notez aussi que la fonction remplir est exprimée non pas en termes de pointeurs mais bien en termes des opérations d'un itérateur (comme vector<T>::iterator ou... Array<T,N>::iterator par exemple), et serait donc applicable à tout conteneur, pas seulement à un tableau brut.

N'oubliez pas de remettre le devoir 1 au plus tard ce soir à 23 h 59 sur https://turnin.dinf.usherbrooke.ca

S07

LUN

26 sept.

N'oubliez pas que, de manière exceptionnelle, notre séance d'aujourd'hui se tiendra une heure plus tard qu'à l'habitude, soit de 14 h 30 à 16 h 20 car votre chic prof a des obligations administratives...

Au menu :

  • Implémenter un équivalent (naïf; pour un véritable équivalent, faudra venir me voir en IFT611 🙂) de std::vector<T>
    • Attention : std::vector<T> et le Array<T,N> de votre devoir 1 sont des classes semblables, mais différentes à plusieurs égards (en particuler, un Array<T,N> a une capacité fixe alors que la capacité d'un std::vector<T> est dynamique)
  • Modéliser un tableau 2D, 3D, ..., ND
  • Examen sommaire de l'équivalent (contextuel) des itérateurs dans d'autres langages
    • Interfaces IEnumerable<T> et IEnumerator<T> de C#
    • Interfacesset Iterator<T> de Java
    • Intervalles à demi ouverts selon les langages ([debut,fin) en C++, (debut,fin] en Java et en C#)
    • Réécriture des boucles sur des intervalles dans ces langages
  • En fonction du temps qu'il reste :
    • Implémenter un équivalent (naïf) de std::deque<T>

Le code vu en classe était (note : il y a beaucoup d'allocations là-dedans pour le pauvre petit coeur de votre chic prof...). Je l'ai présenté en classe par étapes, et j'ai essayé de respecter la même forme ici :

#include <cstddef> // std::size_t
#include <utility> // std::swap

template <class T>
   class deque {
      T **debut{};
      size_t nelems{}, cap{}, zero{};
      // ...

 

      // ...
      bool full() const {
         return size() == capacity();
      }
   public:
      std::size_t size() const {
         return nelems;
      }
      std::size_t capacity() const {
         return cap;
      }
      bool empty() const {
         return !size();
      }
      // ...

 

      // ...
      class iterator {
         std::size_t cur;
         deque &source;
      public:
         iterator(deque &source, std::size_t pos)
            : source{ source }, cur{ pos } {
         }
         iterator &operator++() {
            ++cur;
            return *this;
         }
         iterator operator++(int) {
            auto temp = *this;
            operator++();
            return temp;
         }
         T &operator*() {
            return source[cur];
         }
         const T &operator*() const {
            return source[cur];
         }
         bool operator==(const iterator &autre) const {
            return cur == autre.cur;
         }
         bool operator!=(const iterator &autre) const {
            return !(*this == autre);
         }
      };
      // ...

 

      // ...
      class const_iterator {
         std::size_t cur;
         const deque &source;
      public:
         const_iterator(const deque &source, std::size_t pos)
            : source{ source }, cur{ pos } {
         }
         const_iterator &operator++() {
            ++cur;
            return *this;
         }
         const_iterator operator++(int) {
            auto temp = *this;
            operator++();
            return temp;
         }
         const T &operator*() const {
            return source[cur];
         }
         bool operator==(const iterator &autre) const {
            return cur == autre.cur;
         }
         bool operator!=(const iterator &autre) const {
            return !(*this == autre);
         }
      };
      // ...

 

      // ...
      iterator begin() {
         return { *this, 0 };
      }
      iterator end() {
         return { *this, size() };
      }
      const_iterator begin() const {
         return { *this, 0 };
      }
      const_iterator end() const {
         return { *this, size() };
      }
      // ...

 

      // ...
      deque() = default;
      deque(std::size_t dim)
         : nelems{ dim }, cap{ dim }, zero{} {
         if (dim > 0)
            debut = new T *[capacity];
         else
            debut = nullptr;
         for (std::size_t i = 0; i < size(); ++i)
            debut[i] = new T{};
      }
      // ...

 

      // ...
      ~deque() {
         clear();
      }
      // ...

 

      // ...
      deque(const deque &autre) : deque{ autre.size() } {
         for (std::size_t i = 0; i != size(); ++i)
            *debut[i] = *autre.debut[(i + autre.zero) % autre.cap];
      }
      // ...

 

      // ...
      void swap(deque &autre) {
         using std::swap;
         swap(debut, autre.debut);
         swap(cap, autre.cap);
         swap(nelems, autre.nelems);
         swap(zero, autre.zero);
      }
      deque &operator=(const deque &autre) {
         deque{ autre }.swap(*this);
         return *this;
      }
      // ...

 

      // ...
      void resize(std::size_t nouv_nelems) {
         while (size() > nouv_nelems)
         {
            std::size_t pos = (zero + size() - 1) % capacity();
            delete debut[pos];
            debut[pos] = nullptr;
            --nelems;
         }
         // valider pour être sûr
         reserve(nouv_nelems);
         while (size() < nouv_nelems)
         {
            std::size_t pos = (zero + size()) % capacity();
            debut[pos] = new T{};
            ++nelems;
         }
      }
      // ...

 

      // ...
      void reserve(std::size_t nouv_cap) {
         if (nouv_cap <= capacity()) return;
         T **vieux_debut = debut;
         debut = new T * [nouv_cap];
         for (std::size_t i = 0; i != size(); ++i)
            debut[i] = vieux_debut[(i + zero) % cap];
         for (std::size_t i = size(); i != nouv_cap; ++i)
            debut[i] = nullptr;
         zero = 0;
         cap = nouv_cap;
         delete[] vieux_debut;
      }
      // ...

 

      // ...
      void clear() {
         for (std::size_t i = 0; i != size(); ++i)
            delete debut[(i + zero) % capacity()];
         delete[] debut;
         debut = nullptr;
         zero = 0;
         cap = nelems = 0;
      }
      // ...

 

      // ...
      T &operator[](std::size_t n) {
         return *debut[(n + zero) % capacity()];
      }
      const T &operator[](std::size_t n) const {
         return *debut[(n + zero) % capacity()];
      }
      // ...

 

      // ...
      void push_back(const T &val) {
         // ...
      }
      void pop_back() {
         // ...
      }
      void push_front(const T &val) {
         // ...
      }
      void pop_front() {
         // ...
      }
   };
   // ...

 

// ...
#include <string>
#include <iostream>

int main() {
   using namespace std;
   deque<int> v;
   for (int i : { 2, 3, 5, 7, 11 })
      v.push_back(i);
   for (int n : v)
      cout << n << ' ';
   cout << endl;
   while (!v.empty()) {
      v.pop_front();
      for (int n : v)
         cout << n << ' ';
      cout << endl;
   }
}

S08

MER

28 sept.

Au menu :

  • On joue avec les listes
    • Listes simplement chaînées (on en a déjà fait une à la séance S04)
    • Liste doublement chaînée
      • Sans noeuds sentinelles
        • Impact sur les opérations
      • Avec noeuds sentinelles
        • Impact sur les opérations
      • Inversion de l'ordre des éléments
      • Splicing
      • Insertion
      • Suppression

s/o

VEN

30 sept.

Journée nationale de la vérité et de la réconciliation (levée des cours)

S09

MER

5 oct.

Au menu :

  • À venir

S10

VEN

7 oct.

Au menu :

  • À venir

N'oubliez pas de remettre le devoir 2 au plus tard ce soir à 23 h 59 sur https://turnin.dinf.usherbrooke.ca

S11

MER

12 oct.

Au menu :

  • À venir

S12

VEN

14 oct.

Au menu :

  • À venir

S13

??

??

Examens périodiques (levée des cours)

s/o

MER

26 oct.

Relâche (levée des cours)

s/o

VEN

28 oct.

Relâche (levée des cours)

S14

LUN

31 oct.

Au menu :

  • À venir

S15

MER

2 nov.

Au menu :

  • À venir

S16

VEN

4 nov.

Au menu :

  • À venir

S17

MER

9 nov.

Au menu :

  • À venir

S18

VEN

11 nov.

Au menu :

  • À venir

S19

MER

16 nov.

Au menu :

  • À venir

S20

VEN

18 nov.

Au menu :

  • À venir

S21

MER

23 nov.

Au menu :

  • À venir

S22

VEN

25 nov.

Au menu :

  • À venir

S23

MER

30 nov.

Au menu :

  • À venir

S24

VEN

2 déc.

Au menu :

  • À venir

S25

MER

7 déc.

Au menu :

  • À venir

S26

VEN

9 déc.

Au menu :

  • À venir

S27

??

??

Chic examen final

Consignes des devoirs et laboratoires

Les consignes des devoirs et des laboratoires suivront.

Notes à propos des notes de cours

Quelques notes pour accompagner votre lecture des notes de cours.

Chapitre 1

À venir

 

Chapitre 2

À venir

 

Chapitre 3

À venir

 

Chapitre 4

La page 33 donne un exemple qui présente une classe entierRelatif comme un cas pertinent de dérivé d'une classe entier, mais notez qu'il y a des nuances à apporter ici (en particulier, si entierRelatif est un dérivé public de la classe entier, il y a alors bris de ce qu'on nomme le principe de Liskov, sur lequel nous reviendrons). Le texte est bon en général, mais cet exemple précis mérite qu'on le revisite éventuellement

La note en bas de page numéro 23, page 33, parle d'encapsulation en termes de regroupement; le regroupement est effectivement une caractéristique importante de l'approche OO, mais l'encapsulation trouve sa réelle importance dans la responsabilisation de l'objet quant à la garantie du respect de ses invariants. Nous y reviendrons, car cet aspect a une incidence importante sur plusieurs choix de représentation des objets

À la page 34, le littéral "Bonjour" n'est pas techniquement un const char* mais est par contre implicitement convertible en un const char* (il y a une subtilité ici). Ceci revient aussi ailleurs dans le texte

La page 35 et plusieurs autres utilisent des majuscules pour des noms d'attributs (en termes du langage C++, on utilisera aussi « variables membres » car le mot anglais attribute a un autre sens technique), mais cet usage privilégié dans le document n'est pas recommandé (en pratique, il causerait beaucoup de dégâts car les majuscules, en C++ comme en C, indiquent normalement des macros) alors nous l'éviterons dans notre cours

Le discours au bas de la page 35 demande quelques nuances, que nous ferons en classe

Le texte utilise copieur pour le constructeur de copie et affectateur pour l'opérateur d'affectation. Ce sont des choix raisonnables

La page 36 affirme « Si [une] classe a absolument besoin d'un destructeur, alors on doit absolument aussi coder le copieur et l’affectateur », mais je vous propose de nuancer sous cet angle : « Si [une] classe a absolument besoin d'un destructeur, alors mieux vaut considérer coder le copieur et l’affectateur ». Le mot « absolument » est trop fort ici, mais le signal est qu'il faut une bonne raison pour ne pas compléter ce trio d'opérations. Notez aussi que l'enjeu n'est pas limité à l'allocation dynamique (de mémoire), mais bien de manière plus large à la gestion des ressources sous la responsabilité de l'objet. Pour en savoir plus, voir ../../Sujets/Divers--cplusplus/Sainte-Trinite.html

La page 37 contient un passage qui demande un peu de nuances et qui indique « Pour les objets définis par des classes, le constructeur sans paramètre est appelé. Pour les objets primitifs, il n'y a pas de tel appel ». En réalité, la réalité décrite à cet endroit s'applique aussi à plusieurs types plus complexes que des primitifs, et ces types (les agrégats) sont très utiles

L'exemple de la page 37 pour présenter un constructeur de copie est correct, mais dans le cas de la classe point le constructeur de copie implicitement généré par le compilateur aurait été préférable

À la page 38, le passage suivant est incorrect : « On ne pourrait pas non plus déclarer de vector<point> car ce conteneur a besoin d’initialiser des points avec ce constructeur », car vector<T> pour un certain type T ne créera pas de T par défaut si on ne le lui demande pas. Démonstration : https://wandbox.org/permlink/Y5GEcdBdG1R7GuI7

À la page 38, notez que dans plusieurs cas, retourner un objet par valeur d'une fonction ne provoquera pas une construction par copie (en fait, cela en provoquera rarement). Il y a des subtilités ici...

À la page 39, on aurait pu remplacer ceci :

flot>>i>>j;
if(flot)... // si vrai, il y a des données

... par cela :

if (flot>>i>>j)... // si les lectures de i et de j ont réussi

La page 40 (une page parmi plusieurs) utilise le vocable « référence constante » pour l'idée de « référence-vers-const » mais notez que ce n'est pas vraiment la référence qui est const, c'est le référé

Lisez la section sur les mutateurs (pages 40 et 41) avec attention : fuyez les automatismes! Cette section est excellente. Les mutateurs à la « set » devraient être rares dans du code sérieux, et l'argumentaire à ce sujet dans le texte est limpide

Page 43, notez qu'une fonction friend participe à l'interface du type dont elle est amie. Notez que le premier exemple de la page est légèrement trompeur (c'est subtil) dans son recours aux parenthèses, car l'ordre d'évaluation des sous-expressions

Page 43, les opérateurs d'entrée / sortie sur des flux sont perfectibles. Plutôt que ceci :

istream& operator>>(istream &entree,point &p){
   if(!entree)return entree;
   char skip;
   double X,Y;
   entree >> skip >> X >> skip >> Y >> skip;
   p=point(X,Y);
   return entree;
}
ostream& operator<<(ostream &sortie,const point &p){
   sortie << "(" << p.abscisse() << "," << p.ordonnee() << ")";
   return sortie;
}

... qui néglige entre autre de valider que les lectures aient été réussies, préférez :

istream& operator>>(istream &entree,point &p){
   if(!entree)return entree;
   char skip;
   double x, y;
   if (entree >> skip >> x >> skip >>  y>> skip)
      p = { x,y };
   return entree;
}
ostream& operator<<(ostream &sortie,const point &p){
   return sortie << '(' << p.abscisse() << ',' << p.ordonnee() << ')';
}

Page 44, le passage « Ces objets sont tous initialisés au départ du programme, avant même le début de l’exécution de la fonction main() » demande un peu de nuances. Notez aussi que int2 n'est pas initialisé deux fois en C++ (il existe des langages qui font une double initialisation, mais C++ n'en est pas), du moins si l'initialisation de ses attributs est faite de manière idiomatique

Page 45, section « L'agrégation », deux remarques : (a) l'agrégation au sens OO n'est pas exactement ce qu'indique cette section (on parle plus de composition, règle générale, avec ce descriptif), et (b) le texte est un peu trompeur en escamotant l'enjeu contemporain de la sémantique de mouvement, dont nous discuterons

Page 45, section « La surdéfinition », notez que C++ n'est pas pleinement compatible avec C (il existe des programmes C qui ne sont pas des programmes C++ corrects). Notez aussi que map<double,double>, bien que légal, est un type périlleux à utiliser... Voyez-vous pourquoi? Démonstration : https://wandbox.org/permlink/iC4c6SBYSkFX9oiw

Page 47, le passage « Cela allège beaucoup le code, mais dans ce cas, il faut faire très attention à l’ordre des définitions des fonctions dans la classe, car celles-ci ne peuvent utiliser une fonction qui n’a pas encore été déclarée » est incorrect (c'est tout à fait légal en C++, et ce depuis des décennies). Par contre, il faut que les types aient été déclarés avant d'être utilisés. Démonstration : https://wandbox.org/permlink/3lfKZpYqbJOzIdwi (si vous commentez Y::f() et commentez l'autre version, définie après l'introduction du type Y::type, tout fonctionne : https://wandbox.org/permlink/YhydoA64nhiHzQGM pour le voir)

Chapitre 5

À venir

 

 


Valid XHTML 1.0 Transitional

CSS Valide !