Saltar para o conteúdo
Topos Uranos

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

  1. Criar um std::vector, acrescentar-lhe elementos com push_back e percorrê-lo, lendo com ele uma série de dados de comprimento desconhecido.
  2. 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.
  3. 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::max ou std::ranges::fold_left.
  4. 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.

Programa em C++
#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);}
Saída
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) acrescenta x no final; v.pop_back() remove o último (com o vetor vazio, é comportamento indefinido).
  • v.size() é o tamanho, do tipo std::size_t, o inteiro sem sinal dos tamanhos que a aula Arranjos apresentou; v.empty() equivale a v.size() == 0.
  • v[i] e v.at(i) dão o elemento de índice i, que deve cumprir i < v.size(); v.front() e v.back() dão o primeiro e o último, e exigem que o vetor não esteja vazio.
  • v.resize(n) deixa o vetor com n elementos: 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.

Programa em C++
#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);}
Saída
[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á −1-1, mas dá a volta, como se viu na aula Os inteiros e sua representação, até o maior valor de std::size_t.

Programa em C++
#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]);    }}
Saída
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 é 18 446 744 073 709 551 61518\,446\,744\,073\,709\,551\,615, isto é, 264−12^{64} - 1, 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().