Resumo
Um std::array exige conhecer na compilação quantos elementos terá; a maioria dos problemas, ao contrário, não o diz até que os dados cheguem. Esta aula apresenta std::vector, a sequência que cresce conforme os dados: como se cria, como se acrescentam elementos a ela com push_back e como se leem com ela séries de comprimento desconhecido. Seguem os três percursos canônicos de toda programação com coleções (acumular, filtrar e transformar), com um laço e com os algoritmos da biblioteca; a passagem de um vetor a uma função, e por que devolver um vetor novo é barato. Fecha com o interior do vetor: o seu tamanho e a sua capacidade, o bloco que é substituído por outro do dobro quando se enche, e a referência a um elemento que, por isso, pode ficar apontando para memória liberada.
Objetivos de aprendizagem
- Criar um
std::vector, acrescentar-lhe elementos compush_backe percorrê-lo, lendo com ele uma série de dados de comprimento desconhecido. - Escrever funções que recebam um vetor por referência constante para lê-lo e por referência para modificá-lo, e que devolvam um vetor novo quando o resultado é uma sequência.
- Implementar percursos de acumulação, de filtragem e de transformação, reconhecer em um problema o padrão que lhe corresponde e abreviá-lo, quando a biblioteca o permite, com
std::ranges::count,std::ranges::maxoustd::ranges::fold_left. - Explicar com um exemplo a diferença entre o tamanho e a capacidade de um vetor, e por que uma referência a um elemento pode ser invalidada ao acrescentar outro.
A sequência que cresce
Criar e preencher um vetor
A aula Arranjos apresentou a sequência indexada: elementos do mesmo tipo, um após o outro, numerados a partir de zero. A sua forma moderna de tamanho fixo, std::array<int, 7>, leva o tamanho no tipo, e o compilador deve conhecê-lo. Ora, quem escreve um programa que calcula a média das notas de uma turma não sabe quantos estudantes ela terá. Disso se segue a necessidade de uma sequência cujo tamanho seja um dado a mais, que mude durante a execução: é o vetor, std::vector<T>, do cabeçalho <vector>, onde T é o tipo dos elementos.
#include <print>#include <vector>int main(){ std::vector<int> squares{}; std::println("No início: tamanho {}, vazio: {}", squares.size(), squares.empty()); for (int k{1}; k <= 5; ++k) { squares.push_back(k * k); } std::println("{}", squares); std::println("Tamanho {}, primeiro {}, último {}", squares.size(), squares[0], squares.back()); squares[1] = 0; squares.pop_back(); std::println("{}", squares);}No início: tamanho 0, vazio: true[1, 4, 9, 16, 25]Tamanho 5, primeiro 1, último 25[1, 0, 9, 16]A linha 6 cria um vetor vazio de inteiros: diferentemente do arranjo, não é preciso anunciar quantos elementos terá. size() devolve quantos tem, e empty() diz se não tem nenhum. O laço das linhas 8 a 10 acrescenta cinco quadrados com push_back, que coloca o seu argumento no final e aumenta o tamanho em um. A linha 11 escreve o vetor inteiro com a formatação de intervalos que a aula anterior usou com os arranjos. O acesso por índice é o de sempre, e squares.back() abrevia squares[squares.size() - 1]. A linha 13 atribui a um elemento existente, e a 14 remove o último com pop_back. As operações desta aula são, em resumo, as seguintes.
v.push_back(x)acrescentaxno final;v.pop_back()remove o último (com o vetor vazio, é comportamento indefinido).v.size()é o tamanho, do tipostd::size_t, o inteiro sem sinal dos tamanhos que a aula Arranjos apresentou;v.empty()equivale av.size() == 0.v[i]ev.at(i)dão o elemento de índicei, que deve cumpriri < v.size();v.front()ev.back()dão o primeiro e o último, e exigem que o vetor não esteja vazio.v.resize(n)deixa o vetor comnelementos: remove os que sobram no final ou acrescenta no final elementos inicializados como com chaves vazias (zeros, para os números).
Como nos arranjos, v[i] não garante nenhuma verificação, e v.at(i) sim: com um índice 7 e cinco elementos, detém o programa com a mensagem vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), que nomeia o índice e o tamanho. O mecanismo que produz essa detenção, uma exceção, estuda-se em Programação em C++ II.
Chaves e parênteses
O curso inicializa tudo com chaves, e o vetor é o primeiro tipo do curso em que trocar as chaves por parênteses muda o resultado. Com chaves, os valores escritos são os elementos; com parênteses, o primeiro número é a quantidade de elementos e o segundo, se houver, o valor de cada um.
#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 tem um elemento, o 5; b, cinco zeros; c, o 3 e o 7; d, três setes. A lista entre chaves diz, portanto, “estes são os elementos”, e os parênteses ficam reservados para pedir n cópias de um mesmo valor, como um vetor de contadores em zero.
O último de um vetor vazio
Um vetor vazio é um caso legítimo (a entrada sem dados, o filtro que ninguém passa), e cada fórmula com o seu tamanho deve ser revista nesse caso; a mais traiçoeira é v.size() - 1. Como v.size() é um inteiro sem sinal, subtrair-lhe 1 quando vale 0 não dá , mas dá a volta, como se viu na aula Os inteiros e sua representação, até o maior valor 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); // Limite errado: com o vetor vazio, a condição é verdadeira. 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.O sistema interrompeu o programa com o sinal 6.
A primeira linha da saída é , isto é, , o maior std::size_t desta plataforma. Disso se segue que a condição do laço, 0 <= 18446744073709551615, é verdadeira, e que a primeira iteração lê values[0] de um vetor que não tem elemento 0: comportamento indefinido. O que se observa depende das opções, como nos arranjos: compilada sem otimização, a biblioteca do GCC 15 verifica o índice de operator[] e detém o programa com uma asserção que diz __n < this->size(); com otimização essa verificação desaparece, e o laço, que não se detém a tempo, tenta ler onde não há nenhum elemento (verificado no Linux com -O2: como o vetor vazio não tem bloco, o programa termina com uma falha de segmentação, sem mensagem que explique a causa). A correção é escrever o limite sem subtração, i < values.size(), que com o vetor vazio é falso desde o começo; e, onde a subtração parecer necessária, perguntar antes por values.empty().
Carregando o conteúdo…