Pular para conteúdo

4. std::vector

O problema que std::vector resolve

Em C, um vetor é um bloco de memória de tamanho fixo, decidido em tempo de compilação (ou alocado na mão com malloc, com você controlando o tamanho manualmente):

int notas[100];   // e se voce precisar de 101?
int quantidade = 0;
// ... quantidade++ toda vez que adiciona um elemento, torcendo pra nao passar de 100

std::vector<T> é um vetor que cresce sozinho. Ele mora em <vector>:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> notas;         // comeca vazio
    notas.push_back(8);             // adiciona no final
    notas.push_back(7);
    notas.push_back(10);

    std::cout << notas.size() << "\n";  // 3 -- sem contador manual
    std::cout << notas[0] << "\n";       // acesso por indice, igual C
}

Criando um vector

std::vector<int> a;                       // vazio
std::vector<int> b(5);                    // 5 elementos, todos 0
std::vector<int> c(5, 10);                // 5 elementos, todos 10
std::vector<int> d = {1, 2, 3, 4};        // lista de inicializacao
std::vector<std::string> nomes = {"Ana", "Bruno", "Carla"};

Operações essenciais

std::vector<int> v = {10, 20, 30};

v.push_back(40);          // adiciona no final: {10,20,30,40}
v.pop_back();              // remove o ultimo: {10,20,30}
v.size();                  // 3
v.empty();                 // false
v.clear();                 // esvazia o vector

v[0];                      // acesso rapido, SEM checar limites (igual C)
v.at(0);                   // acesso com checagem: lanca excecao se indice invalido
v.front();                 // primeiro elemento
v.back();                  // ultimo elemento

v[10] num vetor de 3 elementos é comportamento indefinido, igual em C — o compilador não te protege. v.at(10) lança std::out_of_range, uma forma segura de detectar o erro: em vez de ler memória alheia silenciosamente, o programa aborta na hora, com uma mensagem que diz o que aconteceu. Lançar uma exceção é um jeito que C++ tem de sinalizar erro; tratá-las fica fora do escopo deste curso, então na prática o efeito aqui é o programa parar — o que, comparado a continuar rodando com lixo, é o resultado desejável.

Removendo elementos

Tirar o último elemento é a remoção barata, e é a que você vai usar na maior parte do tempo:

std::vector<int> v = {10, 20, 30};

v.pop_back();              // agora v e {10, 20}
std::cout << v.size();     // 2

Repare no que pop_back() não faz: ele não devolve o elemento que tirou. Se você precisa do valor, leia antes de remover:

int ultimo = v.back();     // le
v.pop_back();              // e so entao remove

Escrever int ultimo = v.pop_back(); não compila, e é o erro mais comum aqui — a operação de ler e a de remover são separadas de propósito.

Para esvaziar o vector inteiro de uma vez:

v.clear();
std::cout << v.size();     // 0
std::cout << v.empty();    // 1

Por que remover do meio é caro

Um vector guarda os elementos lado a lado na memória, sem buracos. É isso que faz v[i] ser instantâneo — e é isso que torna a remoção do meio cara.

Tirar o 20 de {10, 20, 30, 40} não deixa um vazio no lugar dele: o 30 e o 40 precisam andar uma casa para trás para fechar o buraco.

antes:   10   20   30   40
              ↑ remover daqui
                   ←    ←     o 30 e o 40 se deslocam
depois:  10   30   40

Num vector de 4 elementos isso é irrelevante. Num de 100 mil, remover o primeiro elemento move 99.999 elementos — e remover mil vezes move quase cem milhões. Por isso a regra prática: mexa na ponta sempre que puder. push_back e pop_back não deslocam ninguém; qualquer remoção no meio desloca tudo que vem depois. O capítulo 13 volta a esse custo com nome e notação, ao comparar fila com lista encadeada.

E remover de uma posição qualquer, como se faz?

Com v.erase(...), que precisa de um iterador para dizer qual posição — e iterador é assunto do capítulo 8. Até lá, se você precisar de um vector "sem os elementos que não servem", o caminho é o do capítulo 5: percorrer o original e montar um vector novo só com os que ficam, com push_back. Dá o mesmo resultado, não desloca nada, e usa só o que você já sabe.

Percorrendo um vector

A forma clássica, igual ao for de C:

for (std::size_t i = 0; i < v.size(); i++) {
    std::cout << v[i] << " ";
}

A forma moderna, range-for (C++11+), quando você não precisa do índice:

for (int x : v) {
    std::cout << x << " ";
}

Se quiser alterar os elementos durante a iteração, use referência:

for (int& x : v) {
    x = x * 2;   // dobra cada elemento do vector de verdade
}

Sem o &, x seria uma cópia de cada elemento, e alterar x não mudaria nada em v.

Tamanho, índice e std::size_t

Repare no tipo do índice do laço clássico acima: std::size_t, e não int. Não é preciosismo — é o tipo que v.size() realmente devolve.

std::vector<int> v = {10, 20, 30};
int n = v.size();            // compila, mas o compilador reclama
std::size_t m = v.size();    // o tipo certo

std::size_t é um tipo inteiro sem sinal: representa 0, 1, 2, ... e nunca um número negativo. Faz sentido — um tamanho ou um índice não têm como ser negativos, e o tipo documenta isso. Não é invenção do C++: é o mesmo size_t que C já usa em strlen, malloc e sizeof; a única novidade é o std:: na frente. Ele chega junto com <vector> e <string>; formalmente, mora em <cstddef>.

O atrito aparece quando você mistura os dois tipos:

for (int i = 0; i < v.size(); i++) {   // int comparado com size_t
    std::cout << v[i] << " ";
}
warning: comparison of integer expressions of different signedness:
'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'}

O programa roda, mas o aviso é real: para comparar, o compilador precisa converter um dos lados, e existem casos em que essa conversão dá errado.

Há duas saídas, e o curso usa as duas — a escolha depende do que o índice faz.

Laço simples, para frente: declare o índice como std::size_t.

for (std::size_t i = 0; i < v.size(); i++) {
    std::cout << v[i] << " ";
}

Índice que entra em conta que pode ficar negativa: converta o tamanho para int antes.

int n = static_cast<int>(v.size());
for (int i = n - 1; i >= 0; i--) {      // percorrendo de tras para frente
    std::cout << v[i] << " ";
}

static_cast: a conversão explícita

static_cast<int>(v.size()) quer dizer "converta este valor para int, e eu sei o que estou fazendo". É o que o cast de C — (int) v.size() — fazia, com duas diferenças que importam: o nome é longo e procurável (uma busca por static_cast acha todas as conversões do seu programa), e o compilador só aceita conversões que fazem sentido, em vez de obedecer calado a qualquer coisa. Neste curso, use static_cast; o cast de C ainda funciona, mas não escreva mais.

A forma geral é static_cast<TipoDestino>(expressão), e serve para qualquer conversão que você queira deixar registrada no código:

int total = 7;
int quantidade = 2;
double media = static_cast<double>(total) / quantidade;   // 3.5, e nao 3

Sem o cast, total / quantidade é divisão inteira e dá 3. Este é o segundo uso mais comum de static_cast no curso, e não tem nada a ver com vector.

A armadilha: sem sinal não fica negativo

Como std::size_t nunca é negativo, subtrair demais não produz um número negativo: dá a volta e chega no maior valor possível.

std::vector<int> v;                     // vazio, entao v.size() e 0
std::cout << v.size() - 1 << "\n";
18446744073709551615

Daí saem os dois erros que mais aparecem no capítulo 5:

  • for (std::size_t i = 0; i < v.size() - 1; i++) num vector vazio não roda "zero vezes": roda dezoito quintilhões de vezes, lendo memória que não é sua. Com int n = static_cast<int>(v.size()), n - 1 dá -1, a condição é falsa de saída e o laço não roda — que era a intenção.
  • for (std::size_t i = v.size() - 1; i >= 0; i--) nunca termina: i >= 0 é sempre verdadeiro para um tipo sem sinal, e quando i vale 0 e é decrementado, vira o número gigante acima. Para andar de trás para frente, use o int com static_cast.

Na dúvida sobre um índice, troque v[i] por v.at(i) enquanto desenvolve: o acesso inválido vira uma exceção em vez de um crash silencioso, e você descobre na hora que o laço passou dos limites.

Vector de structs

Um vector guarda qualquer tipo, inclusive structs que você mesmo definiu:

struct Aluno {
    std::string nome;
    double nota;
};

std::vector<Aluno> turma;
turma.push_back({"Carlos", 8.5});
turma.push_back({"Marcia", 6.0});

for (const Aluno& a : turma) {
    std::cout << a.nome << ": " << a.nota << "\n";
}

Repare em const Aluno& no laço: evita copiar cada Aluno (poderia ter campos grandes) e, como só estamos lendo, const documenta a intenção e o compilador garante.

"Matrizes": vector de vector

C++ não tem matriz nativa (assim como C não tem, de verdade — int m[3][4] é açúcar sintático para um bloco contíguo). A forma idiomática de representar uma matriz com tamanho variável é um vector de vector:

// matriz 3x4, todos os elementos comecando em 0
std::vector<std::vector<int>> matriz(3, std::vector<int>(4, 0));

matriz[1][2] = 99;

for (const auto& linha : matriz) {
    for (int valor : linha) {
        std::cout << valor << " ";
    }
    std::cout << "\n";
}

matriz.size() dá o número de linhas; matriz[0].size() dá o número de colunas (supondo que todas as linhas tenham o mesmo tamanho — nada impede uma "matriz irregular", já que cada linha é um vector independente).

Passando vector para função

Regra igual à do capítulo 2: const std::vector<T>& para ler sem copiar, std::vector<T>& para alterar o vector do chamador.

double media(const std::vector<double>& notas) {
    double soma = 0.0;
    for (double n : notas) soma += n;
    return notas.empty() ? 0.0 : soma / notas.size();
}

void dobra_todos(std::vector<int>& v) {
    for (int& x : v) x *= 2;
}

Erros comuns

"error: void value not ignored as it ought to be" — você escreveu algo como int ultimo = v.pop_back();. pop_back() não devolve nada (void), então não há valor para guardar. Leia com v.back() primeiro e remova depois, em duas linhas.

"Segmentation fault" ao acessar v[i] fora dos limites — igual C: v[tamanho] não é checado. Se você não tem certeza do índice, use v.at(i) durante o desenvolvimento (é mais lento, mas te avisa do erro em vez de corromper memória silenciosamente).

"error: no matching function for call to..." ao passar {1,2,3} direto — em contextos ambíguos, o compilador às vezes não sabe se {1,2,3} é um vector<int> ou outra coisa. Declare a variável explicitamente: std::vector<int> v = {1,2,3}; em vez de tentar passar a lista direto num lugar ambíguo.

warning: comparison of integer expressions of different signedness — você comparou um int i com v.size(), que é std::size_t (sem sinal). Declare std::size_t i = 0, ou guarde o tamanho em int n = static_cast<int>(v.size()) antes do laço.

Laço que trava ou lê memória alheia num vector vazio — quase sempre é v.size() - 1 com std::size_t: num vector vazio isso não dá -1, dá o maior número possível. Passe pelo int antes de subtrair.

Alterar um vector enquanto percorre com range-for e "for sem &" — for (int x : v) { v.push_back(x); } é comportamento indefinido (adicionar elementos pode realocar a memória interna do vector, invalidando o laço em andamento). Nunca modifique o tamanho do vector dentro de um laço que o percorre.

Resumo

  • std::vector<T> é um vetor de tamanho dinâmico — push_back cresce, size() sempre está certo.
  • [] não checa limites (igual C); .at() checa e lança exceção.
  • pop_back() remove o último e não devolve nada — leia back() antes, se precisar do valor; clear() esvazia tudo.
  • Remover do meio desloca todos os elementos seguintes, e é caro: prefira mexer na ponta. Remover de uma posição qualquer (erase) precisa de iterador, que é o capítulo 8.
  • Range-for (for (T x : v)) é a forma idiomática de percorrer quando não precisa do índice; use T& para alterar os elementos.
  • Vector de struct funciona normalmente; prefira const T& no range-for para evitar cópias.
  • "Matriz" em C++ moderno costuma ser vector<vector<T>>.
  • size() devolve std::size_t, um inteiro sem sinal: use std::size_t i no laço para frente, e int n = static_cast<int>(v.size()) quando o índice entrar em conta que pode ficar negativa.
  • static_cast<T>(x) é a conversão explícita de C++, no lugar do (T) x de C.
  • Nunca altere o tamanho de um vector enquanto o percorre com range-for.