Big Tuto : Apprenez le C++

Chapitre 3 : Tableaux, vectors et introduction aux fonctions

Tutoriel présenté par : Robert Gillard (Gondulzak)
Date d'écriture : 10 août 2015
Date de révision : 31 octobre 2015

      1 - Définitions 

   On peut considérer les tableaux et les vectors comme des ensembles de collections d'objets mais différents en ce sens que les premiers doivent être finis et les seconds non.

   Un tableau est un ensemble fini d'éléments d'un type défini et qui peut-être représenté par t[n] tandis qu'un vector est un ensemble de données d'un type défini dont le nombre ne sera pas nécessairement connu à l'avance.

   Nous allons voir cette importante différence dans les paragraphes suivants. wink

   Cette parenthèse étant faite, on en revient à notre tutoriel. smiley

 

         2 – Les tableaux

   Initialiser un tableau

   La quantité de données contenue dans un tableau doit être connue à la compilation d'un programme et donc être une expression constante. Prenons l'exemple d'entiers non signés :

   - unsigned q = 20 n'est pas une expression constante et ne peut donc pas être une longueur de tableau. Dans ce cas, une initialisation telle que unsigned Tab[q] n'est pas valide.

   - const unsigned q = 20 est une expression constante et l'initialisation unsigned Tab[q] sera tout à fait valide.

   Un tableau devant avoir obligatoirement un type dès sa déclaration, le type ''auto'' ne peut pas être utilisé pour déclarer un tableau.


   Initialisation explicite des éléments d'un tableau

   Soit l'expression : const unsigned q = 4;

   unsigned Tab[q] = { 0, 1, 2, 3, }; déclarera un tableau de 4 entiers non signés dont les valeurs connues sont 0, 1, 2, 3.

   En C++ on n'est pas obligé de mettre une valeur à l'intérieur des crochets pour faire une initialisation explicite. Ainsi, l'expression int Tab[] = { -1, 4, 5 } est tout-à-fait valable et déclarera un tableau de 3 entiers signés contenant les valeurs -1, 4, 5. wink


   Si un tableau est explicitement initialisé avec moins d'éléments que la quantité renseignée à l'intérieur des crochets, les éléments suivants prendront les valeurs 0 en ce qui concerne les types arithmétiques ou des chaînes vides dans le cas de chaînes de caractères.

   Par exemple : int Tab[6] = { 8, 3, 5 }; verra ses éléments initialisés avec les valeurs 8, 3, 5, 0, 0, 0.

   D'autre part :

string Tab[4] = { ''Epee longue du Pourfendeur'',
''Lame aceree de la Sorciere des Landes'',
''Fleche de venin mortel de l'Araignee de Rochebrune'' };

   verra son 4ème élément initialisé avec une chaîne vide soit ''Epee..., ''Lame...'', ''Fleche...'', '' ''.
   Nous reparlerons des chaînes de caractères dans un autre chapitre. wink

 

   Accéder aux éléments d'un tableau :

   L'indice du premier élément d'un tableau est toujours 0.

   Donc, si un tableau d'entiers est défini par int Tab[4] = { 7, 9, 2, 5 },

Tab[0] sera le premier élément du tableau et aura la valeur 7,
Tab[1] sera le second élément du tableau et aura la valeur 9,
Tab[2] sera la troisième élément du tableau et aura la valeur 2,
Tab[3] sera le quatrième élément du tableau et aura la valeur 5.

   Plus généralement, ceci veut dire qu'un tableau contenant n éléments aura ses indices compris entre 0 et n-1smiley

   Voyons un petit exemple qui utilise un tableau d'entiers (Projet 014Tableaux).  

 

Retrouvez les projets complets de ce chapitre :

 

         Projet014 : Tableaux

//projet 014Tableaux
#include<iostream>
 
using namespace std;
 
 
int main()
{
 
const int q = 10;
 
int Tab1[q];
int Tab2[] = { 1, 2, 3 };
int Tab3[5] = { 8, 2, 6 };
 
// 1 - Traitement de Tab1
//On remplit les 10 (q) éléments du tableau avec la valeur de l'indice
for (int i = 0; i < q; ++i)
Tab1[i] = i;
 
//Et on affiche
 
for (int i = 0; i < q; ++i)
cout << Tab1[i] << " ";
cout << endl << endl;
 
// 2 - Traitement de Tab2
 
//On affiche les 3 éléments de Tab2
for (int i = 0; i < 3; ++i)
cout << Tab2[i] << " ";
cout << endl << endl;
 
// 3 - Traitement de Tab3
//On affiche les 5 éléments de Tab3
for (int i = 0; i < 5; ++i)
cout << Tab3[i] << " ";
cout << endl;
 
cin.get();
return 0;
 
} 

 

   Et le résultat dans la console :

 

   ...montre bien qu'en ligne 1 nous avons 10 entiers dont les valeurs sont comprises de 0 à 9.

   En ligne 2 , les valeurs 1, 2, 3 de Tab2[] sont affichées et dans la ligne 3, les éléments 4 et 5 de Tab3[5] sont initialisés à 0.

Remarque :
    Si pour Tab1 nous avions voulu afficher 10 éléments de 1 à 10, il aurait fallu écrire Tab1[i] = i + 1; dans la première boucle.

   Il est toujours préférable d'initialiser une boucle for avec la valeur 0 plutôt qu'avec la valeur 1, cela pour éviter des dépassements de limite de tableau qui pourraient être catastrophiques pour le déroulement du programme.

   Pas plus que C, C++ ne détecte une telle erreur à la compilation. C'est au programmeur de prendre les précautions nécessaires pour que cela ne puisse arriver, nous allons bientôt voir comment. wink

   Dépassement de limite d'un tableau :


   Toutes les données d'un tableau sont écrites séquentiellement dans la mémoire, ce qui veut dire par exemple qu'un tableau de 5 entiers va réserver 20 bytes en mémoire vive.

   Supposons par exemple qu'un tel tableau commence à l'adresse 0. Le premier entier utilisera les adresses mémoire de 0 à 3 et le 5eme entier les adresses 16 à 19. Les adresses 20 et au delà sont bien entendu occupées pour la gestion d'autres fonctions du programme ou par le système d'exploitation lui-même.

   Je vous laisse donc deviner le désastre que pourrait occasionner l'ajout d'un élément en dehors du tableau (un plantage dans le meilleur des cas indecision). Vous voulez essayer ? surprise Ne venez pas dire ensuite ''C'est Gondulzak qui nous y a obligé !...'' laugh

   A l'inverse du langage C, C++ a prévu des ''garde-fou'' pour se prémunir de multiples possibles erreurs qui pourraient survenir lors du déroulement d'un programme. Il s'agit des ''Exceptions''. Et bien qu'il puisse sembler peu orthodoxe d'en parler à cet endroit d'un cours ou d'un tutoriel C++, c'est cependant ce que je vais faire ici car on peut, bien entendu, ''lever'' (c'est le terme adéquat), une ''exception'' pour créer un tableau. wink


   Lever une exception :

   Les exceptions sont très importantes en C++ et sont très utiles pour de gros programmes. La recherche d'une ou plusieurs erreurs dans un projet important peut coûter énormément en terme de temps et d'argent. Il vaut donc mieux prévoir des exceptions tout au long du développement d'un gros projet plutôt que de voir apparaître un ou plusieurs bugs une fois le projet pratiquement terminé, surtout si ces bugs ne se sont pas identifiés lors de la compilation... frown

   Nous reparlerons plus en détail des exceptions lors d'un chapitre traitant de la SL (Standard Library) mais en attendant, voyons comment lever une exception sur un simple programme.

 

   Une exception se compose d'un bloc ''try'', dans lequel sont écrites toutes les instructions pouvant poser problème.

try
{

//code pouvant générer un problème

}

 

   Dès qu'une exception est lancée, elle est transmise à un bloc catch (...) qui contient ce que l'on nomme une ''ellipse'' en paramètre (...) et qui désigne n'importe quel type d'exception.

Catch (...)
{

//Erreur détectée

}

 

   On écrira par exemple : //Erreur – Division par zéro (dans le cas effectif où nous aurions tenté cette division interdite ! indecision). On évitera ainsi un plantage de programme qui pourrait être douloureux ! cheeky

   Un programmeur peut créer ses propres exceptions et nous allons le voir dans notre prochain exemple. Le but est de provoquer une réaction au bloc ''try'' à l'aide du mot-clé throw (lancer).

   Ce type d'exception ainsi lancé par ''throw'' sera récupéré et traité dans le bloc ''catch''.


   Mais voyons tout ceci dans le programme suivant (projet 015Tableaux2) : 

 
//projet 015Tableaux2
#include <iostream>
 
using namespace std;
 
 
int main()
{
int Tab[10];
 
//Traitement du tableau
//On remplit les 10 éléments du tableau
 
//Bloc try
try
{
//On ouvre une boucle pour entrer dix entiers dans le tableau
for (int i = 0; i < 10; i++)
{
/* On lève une exception pour ne pas écrire en dehors des limites du
tableau */
if (i > 9)
{
throw "*** DEPASSEMENT DE LIMITE DE TABLEAU ! ***";
}
 
Tab[i] = i;
cout << Tab[i] << " ";
}
cout << endl;
}
 
//Bloc catch
catch (const char *except)
{
cout << endl << except << endl;
}
 
cin.get();
return 0;
} 

 

   Dans cet exemple nous créons notre exception de dépassement de limite de tableau avec ''throw''.
   La chaîne est capturée dans le bloc ''catch'' à l'aide d'un pointeur sur un caractère constant (Ne vous inquiétez pas, nous parlerons bientôt des pointeurs wink).

   Comme nous n'avons pas dépassé les limites du tableau, notre programme se déroule normalement et affiche dans la console :

 

   Supposons maintenant que nous nous soyons trompés et ayons écrit 20 à la place de 10 dans la boucle for (faites-le et voyez le résultat affiché dans la console indecision) :


   Nous avons bien rempli notre tableau de 10 entiers mais lorsque l'indice i est passé de 9 à 10, l'exception a été lancée et notre programme s'est terminé normalement sans plantage ! surprise

   Voilà en ce qui concerne une première approche des tableaux. Nous en reparlerons lors de prochains chapitres sur les chaînes de caractères ou des pointeurs. Je n'aborderai pas ici les tableaux multi-dimensionnels, je vous renvoie à ce sujet à un cours ou à un tutoriel C. wink

 

         3 – Les vectors

   Au même titre que les tableaux, les vectors sont utilisés pour stocker des collections d'objets ayant tous le même type mais à la différence des premiers, les vectors peuvent être déclarés sans connaître au départ la quantité d'objets qu'ils contiendront. Dans cette collection d'objets, chaque objet est associé à un index qui donnera accès à cet objet.

   Pour pouvoir utiliser des vectors, il faut inclure le fichier d'en-tête ''vector'' au début d'un programme :

#include <vector>


   Attention : vector n'est pas un type, on le référence comme un ''container'' (qui contient des objets) de la Standard Library que nous verrons plus tard.

   Par exemple, la declaration suivante :

   vector<int> nb_Entiers;

   veut dire que nous déclarons un vecteur (vide) nommé nb_Entiers et qui pourra contenir un nombre indéterminé d'entiers signés. De même vector<string> armes; déclarera un vecteur nommé armes et qui pourra contenir des chaînes de caractères.

   vector<int>(10, 5); initialisera un vecteur de 10 entiers ayant chacun la valeur 5.

 

   On peut copier tous les éléments d'un vecteur dans un autre à condition que tous les éléments soient de même type. Ainsi :

vector<int> nombres; //vector vide au départ
vector<int> nombres2 (nombres); //copie les éléments du vector nombres dans nombres2
vector<int> nombres3 = nombres; //copie les éléments du vector nombres dans nombres3
vector<string> armes (nombres); // Erreur ! Types différents.

 

   Initialiser une liste :

 

vector<string> armes { ''Epee longue du Pourfendeur'',
''Lame aceree de la Sorciere des Landes'',
''Fleche de venin mortel de l'Araignee de Rochebrune'' };

 

   Comme pour l'initialisation explicite d'un tableau, les objets ''string'' doivent se trouver à l'intérieur d'accolades avec ou sans signe = devant la parenthèse ouvrante.

vector<string> Enemy {10}; initialisera un vecteur de 10 chaînes vides.
vector<string> Enemy (10); idem
vector<int> nombres (10); initialisera un vecteur de 10 entiers ayant chacun la valeur 0, donc identique à vector<int> nombres(10, 0);
vector<int> nombres {10}; initialisera un vecteur de 1 entier ayant 10 pour valeur.

 

   Vous voyez qu'il faut faire très attention pour initialiser une liste dans le cas des vectors et qu'il n'est pas très difficile de confondre parenthèses et accolades. indecision


   Opérations sur les vectors :

   Les principales opérations que l'on peut effectuer sur des vectors sont :

push_back() ajoute un élément à la fin d'un vector,
pop_back() supprime le dernier élément d'un vector,
front() accède au premier élément d'un vector,
back() accède au dernier élément d'un vector,
size() renvoie le nombre d'éléments d'un vector,
empty() renvoie ''true'' si le vector est vide, ''false'' dans le cas contraire.
 

   Nous pouvons maintenant voir ce que nous avons appris sur les vectors à l'aide d'un exemple (projet 016vector1):  

//Projet 016vector1
#include <iostream>
#include <vector>
#include <string>
using namespace std;
 
 
int main()
{
vector<int> nombres(10, 5); //Un vector contenant 10 entiers dont la valeur est 5
vector<int> nombres2; //Un vector d'entiers mais vide
vector<int> nombres3; //idem
vector<string> armes; //un vector chaîne vide
vector<string> Enemy{ 3 }; //un vector contenant 3 chaînes vides
vector<int> nombres4{ 10 }; //un vector contenant 1 entier de valeur 10
 
// On affiche le vector nombres
cout << "vector nombres : ";
for (unsigned i(0); i < nombres.size(); ++i)
//On utilise size() pour connaître le nombre d'éléments dans le vector
cout << nombres[i] << " ";
 
//On utilise les crochets [] pour accéder aux éléments du vector
cout << endl << endl;
 
// On copie nombres dans nombre2 et on affiche nombres2
nombres2 = nombres;
cout << "vector nombres2 : ";
 
for (unsigned i(0); i < nombres2.size(); ++i)
cout << nombres2[i] << " ";
cout << endl << endl;
 
//On vérifie que le vector nombres3 est bien vide
if (nombres3.empty())
cout << "nombre3 est un vecteur vide";
else
cout << "nombre3 n'est pas un vecteur vide";
cout << endl << endl;
 
// On remplit le vector armes et on l'affiche
armes.push_back("Epee longue du Pourfendeur");
armes.push_back("Lame aceree de la Sorciere des Landes");
armes.push_back("Fleche de venin mortel de l'Araignee de Rochebrune");
 
for (unsigned i(0); i < armes.size(); ++i)
cout << armes[i] << endl;
cout << endl << endl;
 
// On remplit les 3 chaines vides du vector Enemy
Enemy[0] = "Gobelin";
Enemy[1] = "Orc";
Enemy[2] = "Troll";
// Et on affiche
for (unsigned i(0); i < Enemy.size(); ++i)
cout << Enemy[i] << endl;
cout << endl << endl;
 
//On affiche le vector nombres4
for (unsigned i(0); i < nombres4.size(); ++i)
cout << nombres4[i] << endl;
cout << endl << endl;
 
//On supprime les 3 derniers éléments du vector nombres
for (unsigned i(0); i < 3; ++i)
nombres.pop_back();
//Et on affiche
cout << "vector nombres : ";
 
for (unsigned i(0); i < nombres.size(); ++i)
cout << nombres[i] << " ";
cout << endl << endl;
 
//On ajoute un élément au vector nombres
nombres.push_back(7);
 
//Et on affiche
cout << "vector nombres : ";
 
for (unsigned i(0); i < nombres.size(); ++i)
cout << nombres[i] << " ";
cout << endl;
 
cin.get();
return 0;
}

 

   Avec comme résultat dans la console :

 

   Vous remarquerez que j'ai dû changer le type int par unsigned dans toutes les boucles for du programme et ceci pour supprimer les warnings suivants à la compilation :

   warning : signed / unsigned type mismatch

 

   Autres opérations sur les vectors

   La nouvelle norme C++ 11 permet des manières spéciales mais intéressantes d'initialiser ou de lire un vecteur. Nous allons voir deux exemples utilisant les spécificateurs de types ''auto'' et ''decltype''.

   a) Avec ''auto'' (projet 017vector2) 

//Projet 017vector2
#include <iostream>
#include <vector>
#include <string>
using namespace std;
 
 
int main()
{
vector<string> armes { "Epee longue du Pourfendeur",
"Lame aceree de la Sorciere des Landes",
"Fleche de venin mortel de l'Araignee de Rochebrune" };
 
//On imprime les chaînes
for (auto &i : armes) // i est une référence à une chaîne du vector armes
cout << i << endl;
 
cin.get();
return 0;
}

 

   Ce qui donne dans la console :

 

   i est une référence à une chaîne du vector armes.
 
   for (auto &i : armes ) peut donc être traduit comme ceci : Pour chaque i faisant référence à une chaîne du vector armes, .... (ici, afficher la chaîne).
   Que cet exemple ne vous perturbe pas, prenez-le tel quel pour ce chapitre. wink Le prochain chapitre sera consacré aux pointeurs et aux références. smiley

   b) Avec ''decltype'' (projet 018vector3) 
//Projet 018vector3
#include <iostream>
#include <vector>
using namespace std;
 
 
int main()
{
//On déclare un vector d'entiers vide
vector<int> nombres;
 
//Pour ajouter une quantité quelconque (ici 10) d'entiers au vector
//On doit utiliser push_back()
for (decltype (nombres.size()) i = 0; i < 10; ++i)
nombres.push_back(i);
 
//Et on peut relire le vector avec "auto"
for (auto &i : nombres)
cout << i << " ";
cout << endl;
 
cin.get();
return 0;
}

 

   Ce qui donne dans la console :

 

   Pourquoi utiliser nombres.push_back(i) et pas nombre[i] = i frown
   Le vecteur étant vide, on ne peut pas appliquer un indice entre crochets ! surprise Voici les hurlements émis par le compilateur... angry

   Debug assertion failed ! Expression : vector subscript out of range


   Et il n'y a rien d'autre à redire ici : decltype déduit le type des éléments à entrer dans le vectorwink


   Tableaux ou vectors ?

   C'est très simple, si vous ne connaissez pas le nombre d'éléments que contiendra une liste, créez un vector sinon utilisez les tableaux. wink

   Un exemple d'utilisation de vector pourrait être la gestion d'un inventaire de personnage dans un Rpg. wink En effet on pourrait créer un vector pour les armes, d'autres pour les boucliers, armures, et différents accessoires. Mieux encore serait d'utiliser un Template gérant plusieurs types. Mais nous n'en sommes pas encore là, nous verrons les templates à la suite de la POO et en attendant nous avons encore beaucoup à (re)decouvrir. angel

 

   4 – Introduction aux fonctions

   Au premier chapitre de ce tutoriel nous avons vu que main() était la fonction principale d'un programme C++. wink

   Afin de rendre les programmes plus lisibles, on préconise de diviser les différentes tâches enfonctions. Celà permet d'aérer le code et de rendre plus explicite l'écriture du programme.

   Une fonction doit avoir un type de retour, un nom, une liste de paramètres (quelquefois vide) et un corps, logé entre une accolade ouvrante et une accolade fermante. Sa représentation est :

Type nom_Fonction (paramètres)
{
// Corps de la fonction
}

 

   Pour cette introduction, nous verrons simplement deux exemples, une fonction ne retournant aucun type et l'autre une fonction retournant un entier. cool

   Une fonction qui ne retourne rien vers la fonction principale possède le type ''void'' et contrairement au langage C qui permettait d'écrire ''void'' dans la liste de paramètres si ceux-ci n'existaient pas, le C++ s'en passe parfaitement. Nous écrirons donc :

void nom_fonction()
{
 
}


   Voyons cela avec deux exemples.

   a) Une fonction qui ne retourne rien (projet 019Fonction1)       

//Projet 019Fonctions1
#include <iostream>
 
using namespace std;
 
 
void Bonjour();
 
 
int main()
{
// On appelle la fonction
Bonjour();
cout << endl;
 
cin.get();
return 0;
}
 
 
// Une fonction qui ne retourne rien (void)
void Bonjour()
{
cout << "BONJOUR MERUVIA";
}

 

   Dis bonjour à Meruvia... laugh

 

   Comme dans le langage C, si la fonction est placée après la fonction main(), elle ne sera pas reconnue par celle-ci, si elle n'est pas déclarée avant. wink

   Une fois le programme lancé, nous entrons dans la fonction main(). Celle-ci ne contient qu'une instruction, c'est l'appel à la fonction Bonjour(). La fonction Bonjour() affiche ''BONJOUR MERUVIA'' et rend aussitôt la main à la fonction main() qui retourne la valeur 0 indiquant que le programme s'est bien déroulé. wink


   b) Une fonction qui retourne un entier (projet 020Fonctions2)

//Projet 020Fonctions2
#include <iostream>
 
using namespace std;
 
 
// Une fonction qui retourne un entier
int Addition(int x, int y)
{
int resultat(0);
resultat = x + y;
 
return resultat;
}
 
 
int main()
{
int a(10), b(18), somme(0);
 
// On appelle la fonction
somme = Addition(a, b);
 
// Et on affiche
cout << "la somme de " << a << " + " << b << " est " << somme;
cout << endl;
 
cin.get();
return 0;
}

 

   Résultat dans la console :

 

 

   Cette fois, nous plaçons notre fonction avant main() et nous n'avons donc pas besoin de la déclarer. cool
   La fonction (dont le type de retour est un entier) prend 2 paramètres qui sont additionnés et dont le résultat est retourné à la fonction main() par la variable ''résultat''.

   Dans la fonction main(), on initialise deux entiers a et b avec des valeurs respectives de 10 et 18.

   On appelle ensuite la fonction Addition avec les variables a et b passées en paramètres et on affiche ensuite la valeur de la variable somme.

   Je ne l'ai pas fait ici mais j'aurais pu lever une exception sur la valeur des entiers. Notre but n'est cependant pas de lever des exceptions partout dans les exemples, retenez simplement que vous pouvez le faire. indecision


   Voilà, nous allons clôturer ce chapitre par cette petite introduction aux fonctions. Nous aurons encore beaucoup à dire au sujet des fonctions mais ce sera pour un prochain chapitre car je ne voudrais pas étendre les chapitres à l'infini mais les laisser de taille raisonnable. wink

   Dans l'immédiat, nous allons nous concentrer sur les pointeurs et les références car nous devrons bientôt y recourir fréquemment.

   Je vous souhaite une bonne lecture. smiley

     @ bientôt pour le chapitre 4 : Pointeurs et références ! angel

Gondulzak.
 

 

Connexion

CoalaWeb Traffic

Today210
Yesterday297
This week1007
This month4681
Total1743888

26/04/24