4. std::vector¶
Vídeos
08 - Vector C++ Parte 1 · 09 - Parte 2 (Matrizes) · 10 - Parte 3 (Matrizes) · 11 - Parte 4 (structs)
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:
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:
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:
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.
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:
A forma moderna, range-for (C++11+), quando você não precisa do índice:
Se quiser alterar os elementos durante a iteração, use referência:
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:
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.
Í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.
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. Comint n = static_cast<int>(v.size()),n - 1dá-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 quandoivale0e é decrementado, vira o número gigante acima. Para andar de trás para frente, use ointcomstatic_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_backcresce,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 — leiaback()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; useT¶ 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()devolvestd::size_t, um inteiro sem sinal: usestd::size_t ino laço para frente, eint 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) xde C.- Nunca altere o tamanho de um vector enquanto o percorre com range-for.