Tableaux (introduction)

Résumé des épisodes précédents

Pour le moment nous avons vu:

  • Expressions: 3 * (4+5) 1 < x and x < 5 or y == 3

  • Variables, types, affectation: variable = expression

  • Instruction conditionnelle: if

  • Instructions itératives: while, do ... while, for

  • Fonctions, procédures

Pourquoi aller plus loin?

Pour passer à l’échelle!

Manipulation de collections de données

Les tableaux

Exemple: Mini annuaire

On souhaite implanter un mini annuaire. Pour cela, on va stocker, pour chaque personne, un nom et un numéro de téléphone. Nous utiliserons pour chacun d’entre eux une chaîne de caractères (string). Noter que le numéro n’est pas un nombre: il peut contenir des espaces, etc.

Commençons par les incantations magiques usuelles:

#include <iostream>
using namespace std;

Dans un premier temps, notre annuaire aura trois personnes (les noms et numéros sont factices!):

string nom1 = "Jeanne";
string telephone1 = "04 23 23 54 56";
string nom2 = "Franck";
string telephone2 = "03 23 42 34 26";
string nom3 = "Marie";
string telephone3 = "06 52 95 56 06";

Écrivons un petit programme pour afficher le contenu de l’annuaire:

cout << nom1 << " " << telephone1 << endl;
cout << nom2 << " " << telephone2 << endl;
cout << nom3 << " " << telephone3 << endl;

Est-ce satisfaisant comme manière de procéder?

Non: ce code est très redondant. Imaginez s’il y avait 100 personnes dans l’annuaire. La ligne d’affichage serait répétée 100 fois.

Répéter 3 fois (quasiment) la même ligne cela s’appelle une boucle for. Essayons:

for (int i=1; i <= 3; i++) {
    cout << nom1 << " " << telephone1 << endl;
}

Ce n’est pas encore ce que l’on veut. En effet, pour \(i=1\), on voudrait utiliser la variable nom1, pour \(i=2\) la variable nom2, etc. Mais ce sont des variables distinctes!

La solution va être de regrouper les trois variables nom1, nom2, nom3 contenant chacune un nom en une seule variable contenant les trois noms. Pour cela, on va utiliser un tableau. En C++, on utilisera le type vector<string> pour représenter un tableau de chaînes de caractères. Il faut au préalable charger la bibliothèque vector:

#include <vector>
using namespace std;

On construit un tableau pour les trois noms et un pour les trois numéros de téléphone:

vector<string> noms;
noms = vector<string>(3);

vector<string> telephones;
telephones = vector<string>(3);

noms et telephones contiennent chacun trois chaînes vide (ne vous préocuppez pas du type affiché en deuxième ligne; c’est essentiellement vector<string>.

noms
telephones

Commençons à remplir le tableau. Noter que, pour accéder au i-ème nom, on utilise la syntaxe nom[i].

noms[1] = "Jeanne";
telephones[1] = "04 23 23 54 56";
noms[2] = "Franck";
telephones[2] = "03 23 42 34 26";

Voyons ce que cela donne:

noms

Oups! Que s’est-il passé????

Explication: en C++, comme dans la plupart des langages, les tableaux commencent à 0. Ainsi, le premier nom est noms[0], le deuxième noms[1], etc.

Reprennons:

noms[0] = "Jeanne";
telephones[0] = "04 23 23 54 56";

noms[1] = "Franck";
telephones[1] = "03 23 42 34 26";

noms[2] = "Marie";
telephones[2] = "06 52 95 56 06";

Maintenant, nos tableaux sont remplis correctement:

noms
telephones

Nous pouvons enfin écrire notre boucle for:

#include <iostream>
for (int i=0; i < 3; i++) {
    cout << noms[i] << " " << telephones[i] << endl;
}

Notez bien que nous avons fait varier i de 0 à 2!

Si vous êtes aventureux, regardez ce qui se passe si on fait varier i de 1 à 3. Soyez prêt à redémarer votre noyau!

Nommage: telephone ou telephones?

Il pourrait être tentant de nommer notre variable telephone, pour que telephone[i] ressemble à telephonei. Mais ce n’est pas la bonne convention. Le nom d’une variable doit refléter ce qu’elle contient. Ici c’est plusieurs numéro de téléphones. Donc telephones.

Un annuaire avec seulement trois personnes, c’est un peu triste. Ajoutons une quatrième personne:

noms[3] = "Joël";
telephones[3] = "07 23 63 92 38"
noms

Oups! Que s’est-il passé? On a essayé de rajouter un quatrième nom dans un tableau prévu pour trois noms, et ça a tout planté.

Redémarez votre noyau.

Reprennons à zéro: incantations magiques, création de tableau de taille 4, remplissage:

#include<vector>
#include<iostream>
using namespace std;

vector<string> noms;
noms = vector<string>(4);
vector<string> telephones;
telephones = vector<string>(4);

noms[0] = "Jeanne";
telephones[0] = "04 23 23 54 56";
noms[1] = "Franck";
telephones[1] = "03 23 42 34 26";
noms[2] = "Marie";
telephones[2] = "06 52 95 56 06";
noms[3] = "Joël";
telephones[3] = "07 23 63 92 38";

Réutilisons notre programme pour afficher l’annuaire:

for (int i=0; i < 3; i++) {
    cout << noms[i] << " " << telephones[i] << endl;
}

Oups! Quel est le problème?

Forcément, on ne peut pas réutiliser exactement le même programme. Il faut changer le 3 en 4:

for (int i=0; i < 4; i++) {
    cout << noms[i] << " " << telephones[i] << endl;
}

Ce n’est pas très pratique de devoir à chaque fois ajuster notre programme. Imaginez le vendeur de téléphone s’il devait changer son programme chaque fois qu’il rajoute un client!

Pour éviter cela, nous allons exploiter le fait qu’un tableau connaît sa taille:

noms.size()

Nous pouvons maintenant écrire notre boucle de sorte qu’elle fonctionne pour un annuaire de taille quelconque:

for (int i=0; i < noms.size(); i++) {
    cout << noms[i] << " " << telephones[i] << endl;
}

Ce n’est pas encore satisfaisant de devoir reconstruire l’annuaire chaque fois que l’on veut rajouter une personne. Imaginez si vous deviez retaper tous vos contacts sur votre téléphone chaque fois que vous souhaitez en rajouter un.

Pour pallier cela, nous allons voir une dernière opération sur les tableaux: push_back; littéralement, rajouter à la fin:

noms.push_back("Zoé");
telephones.push_back("04 12 43 93 27");
noms

Observez ce qui se passe si vous exécutez à nouveau les deux cellules précédentes.

Tout ceci n’est pas encore parfait: on mélange encore beaucoup données et code. Imaginez si chaque propriétaire de téléphone portable devait réécrire le code pour afficher le contenu de son annuaire.

Un peu plus tard aujourd’hui, nous verrons comment écrire le code une bonne fois pour toutes dans une fonction réutilisable. Dans deux semaines, nous verrons comment regrouper toute l’information dans un tableau unique, où le nom et le numéro de téléphone sont proches l’un de l’autre. Plus tard dans le semestre, nous verrons comment utiliser les fichiers pour stocker les données complètement séparément.

Définition

À retenir:

  • Un tableau est une valeur composite homogène
    C’est-à-dire qu’elle est formée de plusieurs valeurs du même type

  • Une valeur (ou élément) d’un tableau \(t\) est désignée par son indice \(i\) dans le tableau
    on la note \(t[i]\)

  • En C++: cet indice est un entier entre \(0\) et \(\ell-1\),
    \(\ell\) est le nombre d’éléments du tableau

Exemple:

  • Voici un tableau de six entiers: \(\begin{array}{|c|c|c|c|c|c|c|c|c|c|} \hline 1 & 4 & 1 & 5 & 9 & 2\\\hline \end{array}\)

  • Avec cet exemple, t[0] vaut 1, t[1] vaut 4, t[2] vaut 1, …

  • Notez que l’ordre et les répétitions sont importants!

Les tableaux en C++

Exemple:

Au préalable:

#include <vector>
using namespace std;
vector<int> t;
t = vector<int>(6);
t[0] = 1;
t[1] = 4;
t[2] = 1;
t[3] = 5;
t[4] = 9;
t[5] = 2;
t[1] + 2 * t[3]

Construction des tableaux

Déclaration d’un tableau d’entiers:

vector<int> t;
  • Pour un tableau de nombres réels: vector<double>, etc.

  • vector est un template

Allocation d’un tableau de six entiers:

(on réserve de l’espace en mémoire pour ces six entiers)

t = vector<int>(6);

Initialisation du tableau:

t[0] = 1;
t[1] = 4;
t[2] = 1;

Les trois étapes de la construction d’un tableau

À retenir:

  • Une variable de type tableau se construit en trois étapes:

    1. Déclaration

    2. Allocation
      Sans elle: faute de segmentation (au mieux!)

    3. Initialisation
      Sans elle: même problème qu’avec les variables usuelles

Raccourci: Déclaration, allocation et initialisation en un coup:

      vector<int> t = { 1, 4, 1, 5, 9, 2 };

Introduit par la norme C++ de 2011

Utilisation des tableaux

Syntaxe et sémantique

  • t[i] s’utilise comme une variable usuelle:

        // Exemple d'accès en lecture
        x = t[2] + 3 * t[5];
        y = sin( t[3]*3.14 );
    
        // Exemple d'accès en écriture
        t[4] = 2 + 3*x;
    
  • En C++ les indices ne sont pas vérifiés!

  • Le comportement de t[i] n’est pas spécifié en cas de débordement

  • Source no 1 des trous de sécurité!!!

  • Accès avec vérifications: t.at(i) au lieu de t[i]

Quelques autres opérations sur les tableaux

      t.size();       // Taille du tableau `t`
      t.push_back(3); // Ajout d'un élément à la fin de `t`

La syntaxe de ces opérations peut vous paraître suprenante: que vient le . faire là? Pourquoi n’écrit-on pas plutôt size(t), push_back(t,3)?

En fait, nous sommes en train d’utiliser de la programmation objet sans le dire. Vous n’avez pas besoin de connaître les détails pour le moment; juste de mémoriser la syntaxe un peu particulière.

Fonctions et tableaux

Nous avons dit qu’un tableau était une valeur. Il est donc tout à fait possible d’écrire une fonction qui prend un ou des tableaux en paramètres et/ou qui renvoie des tableaux.

Voici par exemple une fonction qui affiche le contenu de notre annuaire:

#include <iostream>
#include <vector>
using namespace std;
void affiche_annuaire(vector<string> noms, vector<string> telephones) {
   for ( int i = 0; i < noms.size(); i++ ) {
      cout << noms[i] << " " << telephones[i] << endl;
   }   
}
vector<string> noms = {"Jeanne", "Franck", "Marie", "Joël"};
vector<string> telephones = {"04 23 23 54 56", "03 23 42 34 26", "06 52 95 56 06", "07 23 63 92 38"}
affiche_annuaire(noms, telephones)

En voici une autre qui calcule la somme des éléments d’un tableau:

#include<vector>
using namespace std;
int somme(vector<int> v) {
    int s = 0;
    for ( int i = 0; i < v.size(); i++) {
        s = s + v[i];
    }
    return s;
}
vector<int> v = { 1, 2, 3 };
somme(v)

Voir la fiche d’exercices sur les tableaux et fonctions.

Résumé: les tableaux

  • Motivation: manipulation de collections de données
    Exemple: un annuaire

  • Un tableau est une valeur composite formée de plusieurs valeurs du même type

  • Un tableau se construit en trois étapes:

    • Déclaration: vector<int> t;

    • Allocation: t = vector<int>(3);

    • Initialisation: t[0] = 3;  t[1] = 0; ...

  • Utilisation: t[i] = t[i]+1, t.size(), t.push_back(3)

  • Un tableau est une valeur comme les autres

  • Il peut être passé en paramètre à ou renvoyé par une fonction