Résumé
Un std::array exige de connaître à la compilation combien d’éléments il aura ; la plupart des problèmes, en revanche, ne le disent qu’à l’arrivée des données. Cette leçon présente std::vector, la séquence qui grandit selon les données : comment on la crée, comment on lui ajoute des éléments avec push_back et comment on lit avec elle des séries de longueur inconnue. Suivent les trois parcours canoniques de toute programmation avec des collections (accumuler, filtrer et transformer), avec une boucle et avec les algorithmes de la bibliothèque ; le passage d’un vecteur à une fonction, et pourquoi renvoyer un vecteur nouveau ne coûte presque rien. Elle se termine par l’intérieur du vecteur : sa taille et sa capacité, le bloc qu’on remplace par un autre deux fois plus grand quand il est plein, et la référence à un élément qui, pour cette raison, peut rester pointée vers de la mémoire libérée.
Objectifs d’apprentissage
- Créer un
std::vector, lui ajouter des éléments avecpush_backet le parcourir, en lisant avec lui une série de données de longueur inconnue. - Écrire des fonctions qui reçoivent un vecteur par référence constante pour le lire et par référence pour le modifier, et qui renvoient un vecteur nouveau quand le résultat est une séquence.
- Implémenter des parcours d’accumulation, de filtrage et de transformation, reconnaître dans un problème le schéma qui lui correspond et l’abréger, quand la bibliothèque le permet, avec
std::ranges::count,std::ranges::maxoustd::ranges::fold_left. - Expliquer avec un exemple la différence entre la taille et la capacité d’un vecteur, et pourquoi une référence à un élément peut être invalidée quand on en ajoute un autre.
La séquence qui grandit
Créer et remplir un vecteur
La leçon Tableaux de taille fixe a présenté la séquence indexée : des éléments du même type, l’un après l’autre, numérotés à partir de zéro. Sa forme moderne de taille fixe, std::array<int, 7>, porte la taille dans le type, et le compilateur doit la connaître. Or, la personne qui écrit un programme calculant la moyenne des notes d’un groupe ne sait pas combien d’élèves il comptera. Il s’ensuit qu’il faut une séquence dont la taille soit une donnée de plus, qui change pendant l’exécution : c’est le vecteur, std::vector<T>, de l’en-tête <vector>, où T est le type des éléments.
#include <print>#include <vector>int main(){ std::vector<int> squares{}; std::println("Au début : taille {}, vide : {}", squares.size(), squares.empty()); for (int k{1}; k <= 5; ++k) { squares.push_back(k * k); } std::println("{}", squares); std::println("Taille {}, premier {}, dernier {}", squares.size(), squares[0], squares.back()); squares[1] = 0; squares.pop_back(); std::println("{}", squares);}Au début : taille 0, vide : true[1, 4, 9, 16, 25]Taille 5, premier 1, dernier 25[1, 0, 9, 16]La ligne 6 crée un vecteur vide d’entiers : à la différence du tableau, il n’y a pas à annoncer combien d’éléments il aura. size() renvoie combien il en a, et empty() dit s’il n’en a aucun. La boucle des lignes 8 à 10 ajoute cinq carrés avec push_back, qui place son argument à la fin et augmente la taille de un. La ligne 11 écrit le vecteur entier avec le formatage des ranges que la leçon précédente a utilisé avec les tableaux. L’accès par indice est celui de toujours, et squares.back() abrège squares[squares.size() - 1]. La ligne 13 affecte un élément existant, et la ligne 14 retire le dernier avec pop_back. Les opérations de cette leçon sont, en résumé, les suivantes.
v.push_back(x)ajoutexà la fin ;v.pop_back()retire le dernier (avec le vecteur vide, c’est un comportement indéfini).v.size()est la taille, de typestd::size_t, l’entier non signé des tailles qu’a présenté la leçon Tableaux de taille fixe ;v.empty()équivaut àv.size() == 0.v[i]etv.at(i)donnent l’élément d’indicei, qui doit vérifieri < v.size();v.front()etv.back()donnent le premier et le dernier, et exigent que le vecteur ne soit pas vide.v.resize(n)laisse le vecteur avecnéléments : il retire ceux qui sont en trop à la fin ou ajoute à la fin des éléments initialisés comme avec des accolades vides (des zéros, pour les nombres).
Comme dans les tableaux, v[i] ne garantit aucune vérification, et v.at(i) si : avec un indice 7 et cinq éléments, il arrête le programme avec le message vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), qui nomme l’indice et la taille. Le mécanisme qui produit cet arrêt, une exception, s’étudie dans Programmation en C++ II.
Accolades et parenthèses
Le cours initialise tout avec des accolades, et le vecteur est le premier type du cours où remplacer les accolades par des parenthèses change le résultat. Avec des accolades, les valeurs écrites sont les éléments ; avec des parenthèses, le premier nombre est le nombre d’éléments et le second, s’il y en a un, la valeur de chacun.
#include <print>#include <vector>int main(){ const std::vector<int> a{5}; const std::vector<int> b(5); const std::vector<int> c{3, 7}; const std::vector<int> d(3, 7); std::println("{} {} {} {}", a, b, c, d);}[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]a a un élément, le 5 ; b, cinq zéros ; c, le 3 et le 7 ; d, trois sept. La liste entre accolades dit donc « voici les éléments », et les parenthèses sont réservées pour demander n copies d’une même valeur, comme un vecteur de compteurs à zéro.
Le dernier d’un vecteur vide
Un vecteur vide est un cas légitime (l’entrée sans données, le filtre que personne ne franchit), et chaque formule avec sa taille doit être revue dans ce cas ; la plus traîtresse est v.size() - 1. Comme v.size() est un entier non signé, lui soustraire 1 quand il vaut 0 ne donne pas , mais fait le tour, comme on l’a vu dans la leçon Les entiers et leur représentation, jusqu’à la plus grande valeur de std::size_t.
#include <cstddef>#include <print>#include <vector>int main(){ const std::vector<int> values{}; std::println("values.size() - 1 = {}", values.size() - 1); // Borne erronée : avec le vecteur vide, la condition est vraie. for (std::size_t i{0}; i <= values.size() - 1; ++i) { std::println("{}", values[i]); }}values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.Le système a arrêté le programme avec le signal 6.
La première ligne de la sortie est , c’est-à-dire , le plus grand std::size_t de cette plateforme. Il s’ensuit que la condition de la boucle, 0 <= 18446744073709551615, est vraie, et que la première itération lit values[0] dans un vecteur qui n’a pas d’élément 0 : comportement indéfini. Ce que l’on observe dépend des options, comme dans les tableaux : compilée sans optimisation, la bibliothèque de GCC 15 vérifie l’indice de operator[] et arrête le programme par une assertion qui dit __n < this->size() ; avec optimisation, cette vérification disparaît, et la boucle, qui ne s’arrête pas à temps, tente de lire là où il n’y a aucun élément (vérifié sous Linux avec -O2 : comme le vecteur vide n’a pas de bloc, le programme se termine par une erreur de segmentation, sans message qui en explique la cause). La correction consiste à écrire la borne sans soustraction, i < values.size(), qui avec le vecteur vide est fausse dès le début ; et, là où la soustraction semble nécessaire, à interroger d’abord values.empty().
Chargement du contenu…