Aller au contenu

ELCa11 - BE #1 — Découverte du langage C++

Cours associé : Cours 1 — Initiation au C++ (voir le polycopié, section Cours 1).


Objectifs

  • Découvrir la syntaxe du C++ par l’exemple : variables, contrôle de flux, fonctions, conteneurs, pointeurs.
  • Prendre en main l’environnement de développement Qt Creator (voir le tuto Création d’un projet C++/Qt).

Prérequis

  • Bases de programmation impérative (boucles, fonctions, types) — typiquement acquises en Python ou autre langage en première année.
  • Aucune connaissance préalable du C ou du C++ n’est supposée.

Principe

Le BE est organisé en 6 séquences progressives. À partir de la séquence 3, un fil rouge : travailler sur une série de nombres et en calculer des statistiques.

Pour chaque séquence :

  • Notions à maîtriser — ce qu’il faut apprendre ;
  • Exemples — programmes commentés, à comprendre et exécuter ;
  • Points à noter / Questions à se poser — à discuter avec l’enseignant ;
  • Programme à réaliser — exercice à coder.

Séquence 1 — Premier programme C++

Notions à maîtriser

  • Structure minimale d’un programme C++ ;
  • directives #include et espace de noms std:: ;
  • types primitifs : int, double, char, bool, std::string ;
  • affichage avec std::cout ;
  • opérateurs arithmétiques (+, -, *, /, %) et de comparaison (==, <, >, <=, >=, !=) ;
  • instruction conditionnelle if (…) { … } else { … }.

Exemple 1.1 — Moyenne de deux notes

/* ===========================================
   Exemple 1.1 — Calcul d'une moyenne
   =========================================== */
#include <iostream>

int main() {
    int note1 = 12;
    int note2 = 15;

    // Attention : int / int donne un int. Le 2.0 force le calcul en double.
    double moyenne = (note1 + note2) / 2.0;

    std::cout << "Note 1   : " << note1   << std::endl;
    std::cout << "Note 2   : " << note2   << std::endl;
    std::cout << "Moyenne  : " << moyenne << std::endl;

    if (moyenne >= 10.0) {
        std::cout << "Élève reçu." << std::endl;
    } else {
        std::cout << "Élève recalé." << std::endl;
    }

    return 0;
}

Points à noter

  • Tout programme C++ possède une fonction int main() qui retourne un entier (0 = succès).
  • #include <…> charge un en-tête de bibliothèque (ici, les fonctions d’entrée/sortie).
  • std::cout est le flux de sortie standard ; << est l’opérateur d’écriture sur ce flux.
  • std::endl insère un retour à la ligne. Équivalent simple : "\n".

Questions à se poser

  • Que vaut moyenne si on remplace 2.0 par 2 ?
  • Comment afficher les deux notes sur une même ligne ?
  • Comment ajouter un cas “mention bien” pour moyenne >= 14 ?

Exemple 1.2 — Équation du second degré

/* ===========================================
   Exemple 1.2 — Résolution de a·x² + b·x + c = 0
   =========================================== */
#include <iostream>
#include <cmath>      // pour std::sqrt

int main() {
    int a = 1, b = -3, c = 2;
    double delta = b * b - 4.0 * a * c;

    if (delta < 0) {
        std::cout << "Pas de racine réelle." << std::endl;
    } else if (delta > 0) {
        double x1 = (-b + std::sqrt(delta)) / (2.0 * a);
        double x2 = (-b - std::sqrt(delta)) / (2.0 * a);
        std::cout << "x1 = " << x1 << std::endl;
        std::cout << "x2 = " << x2 << std::endl;
    } else {
        double x = -b / (2.0 * a);
        std::cout << "Racine double : x = " << x << std::endl;
    }

    return 0;
}

Points à noter

  • L’en-tête <cmath> rend disponibles les fonctions mathématiques (std::sqrt, std::pow, std::sin, …).
  • else if chaîne les conditions sans imbrication profonde.

Questions à se poser

  • Pourquoi écrit-on 4.0 * a * c plutôt que 4 * a * c ?
  • Que faut-il ajouter pour gérer le cas où a == 0 (l’équation devient linéaire) ?

Programme à réaliser

Soit la série de notes suivante, codée en dur :

int notes[] = { 8, 12, 15, 4, 17, 9, 11, 13 };
int nbNotes = 8;

Écrire un programme qui, pour chaque note :

  • affiche la note et indique si elle est en-dessous ou au-dessus de la moyenne (10) ;
  • si elle est au-dessus de 14, ajoute la mention “très bien” ;
  • en fin de programme, affiche le nombre total de notes au-dessus de 10.

Indication : utilisez une boucle. Dans la prochaine séquence, on verra for plus en détail ; pour cet exercice, voici la syntaxe à utiliser :

for (int i = 0; i < nbNotes; ++i) {
    // notes[i]
}

Séquence 2 — Boucles et mise en forme

Notions à maîtriser

  • Boucles : while, do-while, for ;
  • mise en forme de la sortie : <iomanip> (std::setw, std::fixed, std::setprecision) ;
  • déclaration d’une constante avec const.

Exemple 2.1 — Table de conversion Fahrenheit → Celsius

Trois manières équivalentes d’écrire la même boucle.

/* ===========================================
   Exemple 2.1 — Conversion Fahrenheit → Celsius
   de 0 à 100 F par pas de 20 F
   =========================================== */
#include <iostream>

int main() {
    const int inf = 0, sup = 100, pas = 20;

    std::cout << "--- avec while ---" << std::endl;
    double fahr = inf;
    while (fahr <= sup) {
        double celsius = (5.0 / 9.0) * (fahr - 32.0);
        std::cout << fahr << " F  ->  " << celsius << " C" << std::endl;
        fahr += pas;
    }

    std::cout << "--- avec do-while ---" << std::endl;
    fahr = inf;
    do {
        double celsius = (5.0 / 9.0) * (fahr - 32.0);
        std::cout << fahr << " F  ->  " << celsius << " C" << std::endl;
        fahr += pas;
    } while (fahr <= sup);

    std::cout << "--- avec for ---" << std::endl;
    for (double f = inf; f <= sup; f += pas) {
        double celsius = (5.0 / 9.0) * (f - 32.0);
        std::cout << f << " F  ->  " << celsius << " C" << std::endl;
    }

    return 0;
}

Points à noter

  • const int inf = 0; rend la variable non modifiable. À utiliser pour toute valeur qui ne doit pas changer.
  • Les trois boucles font la même chose, mais s’expriment différemment :
    • while : condition testée avant le corps. Le corps peut ne jamais s’exécuter ;
    • do-while : condition testée après. Le corps s’exécute au moins une fois ;
    • for : la plus compacte pour un parcours avec compteur.
  • fahr += pas; est équivalent à fahr = fahr + pas;.

Questions à se poser

  • Si on prend inf = 100 et sup = 0, que fait chaque boucle ?
  • Pourquoi déclarer f à l’intérieur du for est-il préférable à réutiliser fahr ?

Mise en forme avec <iomanip>

L’affichage par défaut n’est pas très lisible. L’en-tête <iomanip> permet de formater les colonnes :

#include <iostream>
#include <iomanip>

int main() {
    const int inf = 0, sup = 100, pas = 20;

    std::cout << std::fixed << std::setprecision(2);   // toujours 2 décimales

    for (double f = inf; f <= sup; f += pas) {
        double c = (5.0 / 9.0) * (f - 32.0);
        std::cout << std::setw(8) << f
                  << "  ->  "
                  << std::setw(8) << c << std::endl;
    }
    return 0;
}

Questions à se poser

  • À quoi sert std::setw(8) ? Son effet est-il permanent ou ponctuel (sur la valeur suivante uniquement) ?
  • À quoi servent std::fixed et std::setprecision(2) ? Combien de temps leur effet dure-t-il ?

Programme à réaliser

Soit la constante const int N = 10;.

Écrire un programme qui affiche, sous forme de tableau aligné, pour les N premiers entiers strictement positifs :

  • la valeur de l’entier ;
  • son carré ;
  • sa racine carrée (avec 3 décimales).

Indications

  • L’en-tête <cmath> donne accès à std::sqrt.
  • Pour le carré d’un entier, i * i est plus simple que std::pow(i, 2).
  • Utilisez <iomanip> pour aligner les colonnes.

Sortie attendue (extrait) :

   i      i²       √i
   1       1    1.000
   2       4    1.414
   3       9    1.732
   ...
  10     100    3.162

Séquence 3 — Conteneurs std::vector et std::string

Notions à maîtriser

  • std::vector<T> : création, accès, ajout, taille ;
  • std::string : création, concaténation, accès ;
  • parcours avec une boucle for indexée et avec une boucle for range-based ;
  • déduction de type avec auto ;
  • génération de nombres aléatoires avec <random>.

Exemple 3.1 — std::vector<int>

/* ===========================================
   Exemple 3.1 — Le conteneur std::vector
   =========================================== */
#include <iostream>
#include <vector>

int main() {
    // Trois manières de créer un vector
    std::vector<int> v1;                           // vide
    std::vector<int> v2 = { 3, 1, 4, 1, 5, 9 };    // par liste de valeurs
    std::vector<int> v3(10, 0);                    // 10 éléments tous à 0

    // Ajout en fin
    v1.push_back(7);
    v1.push_back(8);
    v1.push_back(9);

    // Taille et accès
    std::cout << "v1 contient " << v1.size() << " éléments." << std::endl;
    std::cout << "v2[2] = " << v2[2] << std::endl;

    // Parcours indexé classique
    std::cout << "v2 (indexé) : ";
    for (std::size_t i = 0; i < v2.size(); ++i) {
        std::cout << v2[i] << " ";
    }
    std::cout << std::endl;

    // Parcours moderne (range-based for) avec déduction de type (auto)
    std::cout << "v2 (range)  : ";
    for (auto x : v2) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    return 0;
}

Points à noter

  • std::vector<T> est un tableau dynamique : il peut grandir avec push_back.
  • v.size() retourne le nombre d’éléments (type std::size_t, non signé).
  • L’accès par v[i] ne vérifie pas les bornes ; pour un accès vérifié, utiliser v.at(i) (lance une exception en cas de dépassement — on y reviendra dans le BE #4).
  • La boucle range-based for (auto x : v) est plus lisible que la version indexée et évite les erreurs d’indice. Elle fonctionne avec tout conteneur standard.

Questions à se poser

  • Quelle est la différence de comportement entre std::vector<int> v(10); et std::vector<int> v; ?
  • Que se passe-t-il si on écrit v[100] alors que le vecteur a 10 éléments ?

Exemple 3.2 — std::string

/* ===========================================
   Exemple 3.2 — Le type std::string
   =========================================== */
#include <iostream>
#include <string>

int main() {
    std::string nom    = "Centrale";
    std::string ville  = "Lyon";

    // Concaténation avec +
    std::string complet = nom + " " + ville;
    std::cout << complet << std::endl;

    // Taille et accès caractère par caractère
    std::cout << "Longueur : " << complet.length() << std::endl;
    std::cout << "Premier  : " << complet[0] << std::endl;

    // Une chaîne se parcourt comme un vector
    for (char c : complet) {
        std::cout << c << "-";
    }
    std::cout << std::endl;

    return 0;
}

Points à noter

  • std::string se manipule comme un type primitif : affectation, comparaison (==, !=), concaténation (+).
  • En interne, c’est essentiellement un std::vector<char> : on peut accéder à chaque caractère par [i] et le parcourir avec une boucle range-based.

Exemple 3.3 — Génération aléatoire avec <random>

Pour éviter d’utiliser std::cin, on remplit nos séries avec des valeurs tirées au hasard.

/* ===========================================
   Exemple 3.3 — Génération de valeurs aléatoires
   =========================================== */
#include <iostream>
#include <vector>
#include <random>

int main() {
    // Générateur pseudo-aléatoire avec graine fixe (résultats reproductibles)
    std::mt19937 gen(42);
    // Distribution uniforme : entiers entre 0 et 100 inclus
    std::uniform_int_distribution<int> dist(0, 100);

    // Remplir un vector de 10 entiers aléatoires
    std::vector<int> serie;
    for (int i = 0; i < 10; ++i) {
        serie.push_back(dist(gen));
    }

    // Affichage
    std::cout << "Série : ";
    for (auto x : serie) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    return 0;
}

Points à noter

  • std::mt19937 est un générateur pseudo-aléatoire de bonne qualité. La graine 42 rend les résultats reproductibles d’une exécution à l’autre — pratique pour déboguer.
  • Pour des tirages vraiment aléatoires, on peut remplacer std::mt19937 gen(42); par std::mt19937 gen(std::random_device{}());.
  • std::uniform_int_distribution<int>(a, b) produit des entiers entre a et b inclus.

Questions à se poser

  • Que se passe-t-il si on exécute deux fois le programme avec la graine 42 ? Avec std::random_device{}() ?
  • Comment changer le programme pour tirer des nombres entre -50 et +50 ?

Programme à réaliser

Soit const int N = 20;.

Écrire un programme qui :

  1. Génère un vecteur de N entiers aléatoires entre 0 et 100 (graine fixe 42 pour avoir des résultats reproductibles).
  2. Affiche la série.
  3. Calcule et affiche le minimum, le maximum et la moyenne de la série.

Indications

  • Initialisez min à un grand entier (par exemple, le premier élément du vector) et max à un petit entier (idem) ; mettez-les à jour au fil du parcours.
  • La moyenne est de type double. Forcez la division en flottants pour ne pas perdre la partie décimale.

Sortie attendue (avec graine 42, valeurs indicatives — la sortie exacte dépend de l’implémentation de <random> et peut varier selon le compilateur) :

Série : 13 92 35 82 14 70 ...
Min     : 6
Max     : 99
Moyenne : 51.40

Séquence 4 — Fonctions

Notions à maîtriser

  • Définition et appel d’une fonction ;
  • passage des paramètres par valeur, par référence (&) et par adresse (*, pointeur) ;
  • paramètre const T& pour passer un objet sans le copier ni le modifier ;
  • fonction void (qui ne retourne rien) et fonction qui retourne une valeur.

Exemple 4.1 — Passage par valeur

/* ===========================================
   Exemple 4.1 — Passage par valeur (copie)
   =========================================== */
#include <iostream>

int somme(int a, int b) {
    return a + b;
}

void incrementer(int x) {     // x est une COPIE locale
    x = x + 1;
    std::cout << "Dans la fonction, x = " << x << std::endl;
}

int main() {
    int s = somme(3, 4);
    std::cout << "somme(3, 4) = " << s << std::endl;

    int n = 10;
    incrementer(n);
    std::cout << "Après appel, n = " << n << std::endl;   // n vaut toujours 10
    return 0;
}

Points à noter

  • Par défaut, les arguments d’une fonction sont passés par valeur : la fonction reçoit une copie de la variable de l’appelant. Modifier cette copie ne change pas l’original.
  • Le type de retour précède le nom : int somme(...), void incrementer(...). Le mot-clé void signifie “ne retourne rien”.

Exemple 4.2 — Passage par référence et par adresse

/* ===========================================
   Exemple 4.2 — Permutation par référence et par pointeur
   =========================================== */
#include <iostream>

void permuter_ref(int& a, int& b) {     // référence : a et b désignent les variables de l'appelant
    int tmp = a;
    a = b;
    b = tmp;
}

void permuter_ptr(int* a, int* b) {     // pointeur : *a et *b accèdent aux valeurs pointées
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {
    int x = 1, y = 2;

    permuter_ref(x, y);
    std::cout << "Après ref : x = " << x << ", y = " << y << std::endl;

    permuter_ptr(&x, &y);
    std::cout << "Après ptr : x = " << x << ", y = " << y << std::endl;
    return 0;
}

Points à noter

  • Un paramètre référence int& a signifie : a est un alias de la variable passée en argument. Toute modification de a modifie la variable de l’appelant.
  • Un paramètre pointeur int* a reçoit l’adresse d’une variable. On y accède en déréférençant : *a. À l’appel, on prend l’adresse avec &x.
  • Référence et pointeur ont le même effet, mais la syntaxe est plus simple côté appelant avec les références.

Questions à se poser

  • Peut-on appeler permuter_ref(3, 4) ? Pourquoi ?
  • Peut-on appeler permuter_ptr(nullptr, &y) ? Que se passe-t-il alors ?

Exemple 4.3 — Calcul de statistiques par une fonction

/* ===========================================
   Exemple 4.3 — Fonction qui retourne plusieurs valeurs
   via des paramètres de sortie (références)
   =========================================== */
#include <iostream>
#include <vector>

// Le vector est passé par const& : pas de copie, lecture seule.
// min, max, moyenne sont passés par référence : ce sont les sorties.
void stats(const std::vector<int>& v, int& min, int& max, double& moyenne) {
    min = v[0];
    max = v[0];
    long somme = 0;
    for (auto x : v) {
        if (x < min) min = x;
        if (x > max) max = x;
        somme += x;
    }
    moyenne = (double)somme / v.size();
}

int main() {
    std::vector<int> serie = { 13, 92, 35, 82, 14, 70 };

    int   minVal, maxVal;
    double moy;
    stats(serie, minVal, maxVal, moy);

    std::cout << "Min : " << minVal << std::endl;
    std::cout << "Max : " << maxVal << std::endl;
    std::cout << "Moy : " << moy    << std::endl;
    return 0;
}

Points à noter

  • const std::vector<int>& v signifie : on passe le vecteur sans le copier (& = référence) et on s’engage à ne pas le modifier (const). C’est la manière standard de transmettre un gros objet à une fonction qui ne le modifie pas.
  • (double)somme / v.size() force la division en flottants. Sans le cast, somme / v.size() ferait une division entière.

Programme à réaliser

Reprendre la série aléatoire de la séquence 3 (graine 42, N = 20 entiers entre 0 et 100).

Écrire deux variantes équivalentes d’une fonction qui calcule la moyenne et la variance d’une série :

  • stats_ref(const std::vector<int>& v, double& moyenne, double& variance) — passage par référence ;
  • stats_ptr(const std::vector<int>& v, double* moyenne, double* variance) — passage par adresse.

Rappel : la variance se calcule en deux passes — on calcule d’abord la moyenne, puis on calcule la moyenne des carrés des écarts à cette moyenne.

\[\text{variance} = \frac{1}{N}\sum_{i=0}^{N-1} (x_i - \text{moyenne})^2\]

Vérifier que les deux fonctions donnent le même résultat.

Sortie attendue (valeurs indicatives) :

Série : 13 92 35 82 14 70 ...
[stats_ref] moyenne = 51.40, variance = 942.84
[stats_ptr] moyenne = 51.40, variance = 942.84

Séquence 5 — Pointeurs et allocation dynamique

Notions à maîtriser

  • Variables pointeurs : déclaration, opérateurs & (adresse) et * (déréférencement) ;
  • valeur nulle d’un pointeur : nullptr ;
  • allocation dynamique d’un tableau 1D : new T[n] / delete[] ;
  • bonne pratique : libérer toute mémoire allouée et remettre le pointeur à nullptr.

Exemple 5.1 — Variables pointeurs

/* ===========================================
   Exemple 5.1 — Adresse, pointeur, déréférencement
   =========================================== */
#include <iostream>

int main() {
    int x  = 42;
    int* p = &x;       // p contient l'adresse de x

    std::cout << "x   = " << x  << std::endl;
    std::cout << "&x  = " << &x << std::endl;     // l'adresse de x
    std::cout << "p   = " << p  << std::endl;     // contient la même adresse
    std::cout << "*p  = " << *p << std::endl;     // la valeur pointée par p

    *p = 100;          // modifie x à travers p
    std::cout << "Après *p = 100, x vaut " << x << std::endl;

    int* q = nullptr;  // pointeur nul : ne pointe sur rien
    if (q == nullptr) {
        std::cout << "q ne pointe sur rien." << std::endl;
    }
    return 0;
}

Points à noter

  • int* p déclare un pointeur sur un int. À ce stade, p n’est pas initialisé : il vaut une adresse quelconque. Toujours initialiser un pointeur (avec une vraie adresse ou avec nullptr).
  • L’opérateur & (adresse) appliqué à une variable retourne son adresse mémoire.
  • L’opérateur * (déréférencement) appliqué à un pointeur accède à la valeur pointée.
  • nullptr (C++11) est la valeur conventionnelle pour “pointeur invalide”. Préférable à l’ancien NULL ou 0.

Questions à se poser

  • Que se passe-t-il si l’on écrit int* p; *p = 5; (pointeur non initialisé) ?
  • Que se passe-t-il si l’on écrit int* p = nullptr; *p = 5; ?

Exemple 5.2 — Allocation dynamique d’un tableau 1D

/* ===========================================
   Exemple 5.2 — Tableau 1D alloué dynamiquement
   =========================================== */
#include <iostream>

int main() {
    int n = 5;
    int* tab = new int[n];        // alloue n entiers sur le tas

    for (int i = 0; i < n; ++i) {
        tab[i] = i * i;
    }
    for (int i = 0; i < n; ++i) {
        std::cout << "tab[" << i << "] = " << tab[i] << std::endl;
    }

    delete[] tab;                  // libère la mémoire (impératif !)
    tab = nullptr;                 // bonne pratique : invalide le pointeur

    return 0;
}

Points à noter

  • new T[n] réserve sur le tas (mémoire dynamique) un tableau de n éléments de type T et retourne un pointeur sur le premier élément.
  • delete[] tab libère la mémoire correspondante. Toute mémoire allouée par new[] doit être libérée par delete[] — sinon, fuite mémoire.
  • Le crochet est essentiel : delete tab (sans []) sur un tableau cause un comportement indéfini.

Programme à réaliser

Reprendre la série aléatoire de la séquence 3, mais cette fois sans utiliser std::vector : les N = 20 entiers seront stockés dans un tableau 1D alloué dynamiquement.

  1. Allouer dynamiquement un tableau de N entiers.
  2. Le remplir avec des entiers aléatoires entre 0 et 100 (graine 42).
  3. Écrire une fonction void stats(const int* tab, int N, double& moyenne, double& variance) qui calcule la moyenne et la variance.
  4. Afficher la série, la moyenne et la variance.
  5. Libérer la mémoire.

Indications

  • Le pointeur seul ne connaît pas la taille du tableau qu’il pointe : il faut systématiquement passer la taille N en paramètre des fonctions.
  • N’oubliez pas le delete[] à la fin du main(). Sinon : fuite mémoire.

Sortie attendue (valeurs indicatives, identiques à celles de la séquence 4) :

Série : 13 92 35 82 14 70 ...
moyenne = 51.40
variance = 942.84

Note : en C++ moderne, std::vector<int> (vu en séquence 3) est presque toujours préférable à new int[N] / delete[] pour des raisons de sûreté et de simplicité (pas de risque d’oublier le delete, taille connue, redimensionnement automatique). On utilise les pointeurs nus principalement pour comprendre les mécanismes sous-jacents et pour interfacer avec du code existant. L’allocation dynamique en 2 dimensions sera abordée au BE #2, dans le contexte d’une classe Damier.


Séquence 6 — Synthèse

Cette dernière séquence n’introduit pas de nouvelle notion. Objectif : combiner tout ce que vous avez vu dans un programme complet.

Programme à réaliser

Simulation de 3 dés différents lancés chacun N = 1000 fois :

  • un dé classique à 6 faces (D6, valeurs entre 1 et 6) ;
  • un dé à 10 faces (D10, valeurs entre 1 et 10) ;
  • un dé à 20 faces (D20, valeurs entre 1 et 20).
  1. Pour chaque dé, générer la série des N lancers (graines respectives 42, 2024, 7).
  2. Stocker chaque série dans un std::vector<int> (ou un tableau alloué dynamiquement, au choix).
  3. Pour chaque dé, calculer min, max, moyenne et variance à l’aide d’une fonction stats(...).
  4. Afficher un tableau récapitulatif aligné de la forme :
Dé      |  Min  |  Max  | Moyenne | Variance
--------+-------+-------+---------+---------
D6      |    1  |    6  |    3.51 |     2.91
D10     |    1  |   10  |    5.43 |     8.32
D20     |    1  |   20  |   10.62 |    33.15
  1. Indiquer en fin de programme le dé qui présente la plus grande variance observée. Comparer ensuite aux valeurs théoriques pour un tirage uniforme entre 1 et n :
Moyenne théorique Variance théorique
D6 3.5 2.917
D10 5.5 8.250
D20 10.5 33.250

Rappel : pour une variable uniforme discrète entre 1 et n, la moyenne vaut (n+1)/2 et la variance vaut (n²−1)/12.

Bonus : afficher un histogramme ASCII des résultats du D6, montrant la fréquence d’apparition de chaque face de 1 à 6.

1 : ##############
2 : ##############
3 : ###############
4 : #################
5 : ##############
6 : #############

Aller plus loin — vers les classes

Vous remarquez probablement que vous traitez ces 3 séries de la même manière, en répétant les mêmes appels de fonctions sur des std::vector différents. Cela suggère qu’il serait pratique de regrouper les données (le vecteur des lancers) et les opérations (stats, affichage) dans une seule entité. C’est précisément l’idée des classes, que nous découvrirons au BE #2.