C++ Arrays

A Ana tem 5 alunos. Para guardar as notas, ela poderia criar nota1, nota2, nota3, nota4, nota5… e se a turma tiver 40 alunos? E como fazer um laço passar por variáveis com nomes diferentes? A solução é um array (em português, "arranjo" ou "vetor"): uma variável que guarda vários valores do mesmo tipo, lado a lado, cada um numa posição numerada.

Você vai aprender
  • Como criar um array: int notas[5] = {7, 9, 6, 8, 10};.
  • Por que as posições (os índices) começam do 0.
  • Como ler, trocar e percorrer os valores com for.
  • Como perguntar o tamanho com std::size C++17.
  • O que acontece quando você acessa uma posição que não existe, e como o -fsanitize=address pega isso.
  • Os limites do array "cru": tamanho fixo e esquecimento do tamanho nas funções. E por que o C++ moderno prefere std::array e std::vector.

Os programas foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.

Por que aprender arrays "estilo C"?

No C++ moderno, para listas você vai usar quase sempre std::array (tamanho fixo) ou std::vector (tamanho que cresce). Mas o array estilo C, herdado da linguagem C, aparece em muito código existente, em exemplos da internet e por baixo dos próprios std::array e std::vector. Entender como ele funciona, e onde ele é perigoso, explica por que as alternativas existem.


A ideia: uma fileira de armários numerados

Pense numa fileira de armários de escola, todos iguais, colados uns nos outros. A fileira inteira tem um nome ("armários do 3º ano"), e cada armário tem um número na porta. Para pegar algo, você diz o nome da fileira e o número do armário.

Na escolaNo arrayExemplo
O nome da fileiraO nome do arraynotas
Quantos armários a fileira temO tamanho (fixo!)5
O número na portaO índice0, 1, 2, 3, 4
O que está dentroO elemento (o valor)7, 9, 6…
"Todos os armários são do mesmo modelo"Todos os elementos têm o mesmo tipoint

Uma diferença importante: os armários do C++ começam do número 0, não do 1.


1. Criar um array e usar os elementos

#include <iostream>

int main() {
    int notas[5] = {7, 9, 6, 8, 10};
    std::cout << "Primeira nota: " << notas[0] << '\n';
    std::cout << "Terceira nota: " << notas[2] << '\n';
    std::cout << "Última nota: " << notas[4] << '\n';

    notas[2] = 7;
    std::cout << "Terceira nota corrigida: " << notas[2] << '\n';
}
int
O tipo de cada elemento. Todos os 5 são int.
notas
O nome do array inteiro.
[5]
O tamanho: quantos elementos cabem. Fica fixo para sempre: um array de 5 nunca vira de 6.
= {7, 9, 6, 8, 10};
Os valores iniciais, na ordem das posições: o 7 vai para a posição 0, o 9 para a 1…
notas[2]
Usar um elemento: o nome, e entre colchetes o índice. notas[2] é o terceiro (posições 0, 1, 2). Funciona como uma variável comum: dá para mostrar, usar em contas e atribuir (notas[2] = 7;).
$ g++ -std=c++23 -Wall -Wextra notas.cpp -o notas
$ ./notas
Primeira nota: 7
Terceira nota: 6
Última nota: 10
Terceira nota corrigida: 7

Por que o índice começa do 0?

O índice não é "o número do aluno", e sim a distância desde o começo do array. O primeiro elemento está a 0 passos do começo; o segundo, a 1 passo; o último de um array de 5, a 4 passos. Por isso:

Índice01234
Valor796810
Em português1º2º3º4º5º (último)

Num array de tamanho N, os índices válidos vão de 0 a N − 1. O índice N não existe. Grave isso: é a fonte do erro mais perigoso desta aula.


2. Formas de criar

#include <iostream>

int main() {
    int a[5] = {1, 2};
    int b[5] = {};
    int c[] = {4, 5, 6};

    for (int x : a) std::cout << x << ' ';
    std::cout << '\n';
    for (int x : b) std::cout << x << ' ';
    std::cout << '\n';
    for (int x : c) std::cout << x << ' ';
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra iniciar.cpp -o iniciar
$ ./iniciar
1 2 0 0 0
0 0 0 0 0
4 5 6 
FormaResultado
int a[5] = {1, 2};Os que faltam viram 0: 1 2 0 0 0.
int b[5] = {};Todos 0. A forma certa de criar um array "zerado".
int c[] = {4, 5, 6};Sem o tamanho: o compilador conta os valores (aqui, 3).
int d[5];Sem valores: os elementos ficam com lixo. Evite!

(Os laços acima são o for de intervalo, que funciona com arrays também. Escrevemos cada um em uma linha, sem chaves, só para o exemplo caber; no seu código, use chaves.)

O que é "lixo"? Um array sem valores iniciais fica com o que estava na memória antes, restos de outros programas ou de outras partes deste. Veja uma execução real:

#include <iostream>

int main() {
    int d[5];
    for (int x : d) std::cout << x << ' ';
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra lixo.cpp -o lixo
$ ./lixo
1 0 1075516120 32765 73728 

Esses números não significam nada e podem mudar a cada execução ou em outro computador. Nem o -Wall -Wextra avisou. Por isso a regra do guia: sempre inicialize, nem que seja com {}.

Arrays de outros tipos

Qualquer tipo serve: double precos[3] = {12.5, 8.0, 27.0};, char vogais[5] = {'a', 'e', 'i', 'o', 'u'}; ou a lista de compras da Ana em std::string:

#include <iostream>
#include <string>

int main() {
    std::string lista[4] = {"arroz", "feijão", "café", "leite"};
    for (const auto& item : lista) {
        std::cout << "- " << item << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra nomes.cpp -o nomes
$ ./nomes
- arroz
- feijão
- café
- leite

3. Percorrer com for

A grande vantagem do array é poder usar um laço para passar por todos os elementos. O índice vira a variável do for: do 0 até o tamanho, com < (o "jeito do C++" daquela aula):

#include <iostream>

int main() {
    constexpr int TOTAL = 5;
    int notas[TOTAL] = {7, 9, 6, 8, 10};

    for (int i = 0; i < TOTAL; i++) {
        std::cout << "Aluno " << i + 1 << ": " << notas[i] << '\n';
    }

    double soma = 0.0;
    for (int nota : notas) {
        soma += nota;
    }
    std::cout << "Média: " << soma / TOTAL << '\n';
}
constexpr int TOTAL = 5;
O tamanho guardado numa constante (veja Constantes). Se a turma mudar para 6 alunos, você troca um número só. O tamanho de um array precisa ser conhecido na hora de compilar, e é isso que o constexpr garante.
i < TOTAL
De 0 a 4. Com <=, o laço tentaria ler notas[5], que não existe.
notas[i]
O elemento da posição i. Em cada volta, i muda, e o mesmo código lê um elemento diferente.
i + 1
Para a tela, a numeração "de gente" (Aluno 1, não Aluno 0).
for (int nota : notas)
Quando você não precisa do índice, o for de intervalo é mais simples e não tem como errar o tamanho.
$ g++ -std=c++23 -Wall -Wextra percorrer.cpp -o percorrer
$ ./percorrer
Aluno 1: 7
Aluno 2: 9
Aluno 3: 6
Aluno 4: 8
Aluno 5: 10
Média: 8

Missão: a turma da tarde tem 4 alunos. Some as notas com um laço e mostre Média: 8.5.

#include <iostream>

int main() {
    constexpr int TOTAL = 4;
    int notas[TOTAL] = {8, 9, 7, 10};
    double soma = 0.0;
    // some as notas aqui

    std::cout << "Média: " << soma / TOTAL << '\n';
}
#include <iostream>

int main() {
    constexpr int TOTAL = 4;
    int notas[TOTAL] = {8, 9, 7, 10};
    double soma = 0.0;
    for (int i = 0; i < TOTAL; i++) {
        soma += notas[i];
    }
    std::cout << "Média: " << soma / TOTAL << '\n';
}

4. Perguntar o tamanho: std::size C++17

Em vez de guardar o tamanho numa constante, você pode perguntar ao array com std::size, que vem do cabeçalho <iterator>:

#include <iostream>
#include <iterator>

int main() {
    int notas[] = {7, 9, 6, 8, 10};
    std::cout << "A turma tem " << std::size(notas) << " notas\n";
}
$ g++ -std=c++23 -Wall -Wextra tamanho.cpp -o tamanho
$ ./tamanho
A turma tem 5 notas

Agora, acrescentar um valor nas chaves já atualiza o tamanho. Antes do C++17, a forma tradicional era sizeof(notas) / sizeof(notas[0]) (o tamanho em bytes do array inteiro dividido pelo de um elemento), que você ainda vai encontrar em código antigo.

Um detalhe: std::size devolve um número sem sinal (std::size_t, que nunca é negativo). Compará-lo com um int num for gera um aviso:

#include <iostream>
#include <iterator>

int main() {
    int notas[] = {7, 9, 6, 8, 10};
    for (int i = 0; i < std::size(notas); i++) {
        std::cout << notas[i] << ' ';
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra sinal.cpp -o sinal
sinal.cpp: In function ‘int main()’:
sinal.cpp:6:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::size_t’ {aka ‘long unsigned int’} [-Wsign-compare]
    6 |     for (int i = 0; i < std::size(notas); i++) {
      |                     ~~^~~~~~~~~~~~~~~~~~

"Comparação de inteiros com sinais diferentes: int e std::size_t". Aqui o programa funciona, mas comparar números com e sem sinal pode dar resultados absurdos com valores negativos (veja Inteiros). Três saídas: use o for de intervalo (sem índice, sem problema), declare o índice como std::size_t i, ou guarde o tamanho numa constante int, como na seção 3. O mesmo aviso aparece com o .size() do std::vector.


5. Fora do intervalo: o erro mais perigoso

O que acontece se você pedir notas[5] num array de 5 elementos? Em muitas linguagens, o programa para com uma mensagem clara. Em C++, com arrays crus, ninguém confere. O programa simplesmente lê (ou escreve!) a memória que estiver logo depois do array, seja ela o que for. Este laço tem o erro do "um a mais" (<= no lugar de <, veja for):

#include <iostream>

int main() {
    int notas[5] = {7, 9, 6, 8, 10};
    for (int i = 0; i <= 5; i++) {
        std::cout << notas[i] << ' ';
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra fora.cpp -o fora
$ ./fora
7 9 6 8 10 0 

Nenhum erro, nenhum aviso, e um 0 que não é nota de ninguém apareceu no fim. Esse 0 é o que estava na memória logo depois do array, nesta execução, neste computador. Isso se chama comportamento indefinido (em inglês, undefined behavior): a linguagem não define o que acontece. Pode aparecer um número qualquer, o programa pode travar, ou pode "funcionar" hoje e falhar amanhã. Escrever fora do array é ainda pior: estraga outras variáveis e é a origem de muitas falhas de segurança famosas. O assunto tem aula própria: Comportamento Indefinido.

Pegando o erro com -fsanitize=address

Como o erro é silencioso, existem ferramentas que vigiam a memória enquanto o programa roda. A mais usada é o AddressSanitizer ("higienizador de endereços"), ligado com a opção -fsanitize=address do g++. O -g acrescenta as informações que permitem ao relatório citar o arquivo e a linha:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address fora.cpp -o fora
$ ./fora
=================================================================
==8==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x74898bdf0034 at pc 0x64f3413be447 bp 0x7ffdd98b4da0 sp 0x7ffdd98b4d90
READ of size 4 at 0x74898bdf0034 thread T0
    #0 0x64f3413be446 in main /home/ana/cpp/fora.cpp:6
    #1 0x78898de2a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #2 0x78898de2a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x64f3413be164 in _start (/home/ana/cpp/fora+0x1164) (BuildId: edb3d6275155e381074227e67ed3cca84cbb56a0)

Address 0x74898bdf0034 is located in stack of thread T0 at offset 52 in frame
    #0 0x64f3413be238 in main /home/ana/cpp/fora.cpp:3

  This frame has 1 object(s):
    [32, 52) 'notas' (line 4) <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/ana/cpp/fora.cpp:6 in main
Shadow bytes around the buggy address:     « daqui em diante, um mapa da memória (cortado)
==8==ABORTING
$ echo $?
1

Assusta, mas só quatro linhas importam:

stack-buffer-overflow
"Estouro de um bloco de memória na pilha": acesso além do fim de um array local.
READ of size 4
Foi uma leitura de 4 bytes (o tamanho de um int).
#0 … in main /home/ana/cpp/fora.cpp:6
Onde: na função main, linha 6 do fora.cpp, o std::cout << notas[i].
[32, 52) 'notas' (line 4) <== … overflows this variable
Qual variável: o array notas, criado na linha 4, ocupa os bytes 32 a 51; o acesso foi no byte 52, logo depois do fim.

O programa foi interrompido no momento do erro (ABORTING, código de saída 1). Nem as notas válidas apareceram: elas estavam esperando para ir para a tela quando o programa foi abortado. Use -fsanitize=address sempre que estiver testando programas com arrays; a aula Sanitizers mostra todos os detalhes.

No editor do guia

O relatório acima vem do g++ no terminal. O editor do guia roda o programa de outro jeito (em WebAssembly, veja O Editor do Guia), então o mesmo fora.cpp pode mostrar outro número no lugar do 0. Isso é exatamente o que "comportamento indefinido" quer dizer: não há resultado certo. Para ver o relatório do AddressSanitizer, compile no seu computador como acima.


6. Os limites do array estilo C

Tamanho fixo

O tamanho é decidido quando você escreve o programa e não muda nunca. Não dá para "acrescentar o sexto aluno" num array de 5. Se a quantidade só é conhecida durante a execução (quantos produtos o usuário vai digitar?), o array estilo C não serve. Isso é trabalho para o std::vector.

Você pode encontrar na internet código como int n; std::cin >> n; int notas[n];. Isso não é C++ padrão (chama-se VLA, variable length array, um recurso da linguagem C). O g++ aceita em silêncio, como extensão; o Clang do editor avisa: warning: variable length arrays in C++ are a Clang extension [-Wvla-cxx-extension]. Não use: com um n grande, o programa quebra sem aviso.

Mostrar o array inteiro não funciona

#include <iostream>

int main() {
    int notas[5] = {7, 9, 6, 8, 10};
    std::cout << notas << '\n';
}
$ g++ -std=c++23 -Wall -Wextra cout.cpp -o cout
$ ./cout
0x7ffcf485b480

Em vez das notas, apareceu um número em hexadecimal: o endereço do array na memória (muda a cada execução). Na maioria das situações, o nome de um array "vira" o endereço do primeiro elemento. Essa conversão tem nome, decaimento (decay), e é explicada em Ponteiros e Arrays. Para mostrar os valores, use um laço.

O array esquece o tamanho quando vai para uma função

Esta é a ideia mais importante da seção, mesmo que você ainda não tenha estudado funções. Quando um array é entregue a uma função, por causa do decaimento, a função recebe só o endereço do começo. O tamanho fica para trás. Por isso, este código nem compila:

#include <iostream>
#include <iterator>

void mostrar(int notas[]) {
    for (std::size_t i = 0; i < std::size(notas); i++) {
        std::cout << notas[i] << ' ';
    }
}

int main() {
    int notas[5] = {7, 9, 6, 8, 10};
    mostrar(notas);
}
$ g++ -std=c++23 -Wall -Wextra funcao.cpp -o funcao
funcao.cpp: In function ‘void mostrar(int*)’:
funcao.cpp:5:42: error: no matching function for call to ‘size(int*&)’
    5 |     for (std::size_t i = 0; i < std::size(notas); i++) {
      |                                 ~~~~~~~~~^~~~~~~
funcao.cpp:5:42: note: there are 2 candidates     « e mais umas 20 linhas de notas

Repare na primeira linha: In function ‘void mostrar(int*)’. Você escreveu int notas[], mas o compilador entende int* (um ponteiro para int), e um ponteiro não tem tamanho. O jeito "estilo C" é passar o tamanho junto, num segundo parâmetro, e torcer para ninguém passar o número errado:

#include <iostream>
#include <iterator>

void mostrar(int notas[], int tamanho) {
    for (int i = 0; i < tamanho; i++) {
        std::cout << notas[i] << ' ';
    }
    std::cout << '\n';
}

int main() {
    int notas[5] = {7, 9, 6, 8, 10};
    mostrar(notas, 5);
}
$ g++ -std=c++23 -Wall -Wextra funcao2.cpp -o funcao2
$ ./funcao2
7 9 6 8 10 

Se alguém chamar mostrar(notas, 6), volta o problema da seção 5.


Por que preferir std::array e std::vector

Array estilo C
int a[5]
std::array
std::array<int, 5>
std::vector
std::vector<int>
TamanhoFixoFixoCresce e diminui
Sabe o próprio tamanhoSó onde foi criadoSempre (.size())Sempre (.size())
Acesso conferidoNuncaCom .at(i)Com .at(i)
Copiar com =, comparar com ==NãoSimSim
Vai para funções inteiroNão (vira ponteiro)SimSim
Quando usarCódigo antigo, casos especiaisQuantidade fixa e conhecidaO padrão para listas

Tudo o que você aprendeu aqui (índices do 0, [i], laços, o perigo do "fora do intervalo") vale igualzinho para os dois. Eles só acrescentam segurança e comodidade. Próximos passos: Arrays Multidimensionais (tabelas), std::array e std::vector.


Quando dá errado

Um número estranho no fim (ou no meio) da lista
$ ./fora
7 9 6 8 10 0 

O que aconteceu: o laço leu uma posição que não existe (seção 5). Quase sempre é <= no lugar de <, ou um laço que começa do 1 em vez do 0.

Como confirmar: compile com -g -fsanitize=address; o relatório aponta a linha.

Como consertar: índices de 0 a tamanho − 1: for (int i = 0; i < TOTAL; i++). Melhor ainda: for de intervalo.

"too many initializers for ‘int [3]’"
int main() {
    int notas[3] = {7, 9, 6, 8};
}
$ g++ -std=c++23 -Wall -Wextra demais.cpp -o demais
demais.cpp: In function ‘int main()’:
demais.cpp:2:31: error: too many initializers for ‘int [3]’
    2 |     int notas[3] = {7, 9, 6, 8};
      |                               ^
demais.cpp:2:9: warning: unused variable ‘notas’ [-Wunused-variable]
    2 |     int notas[3] = {7, 9, 6, 8};
      |         ^~~~~

O que significa: "valores iniciais demais para int [3]" (um array de 3 int). Você declarou 3 posições e passou 4 valores. O aviso de "variável não usada" é só porque o exemplo não usa o array. O Clang do editor diz error: excess elements in array initializer.

Como consertar: aumente o tamanho, tire o valor sobrando ou omita o tamanho (int notas[] = {…}) e deixe o compilador contar.

Apareceu um número como 0x7ffc…

O que aconteceu: std::cout << notas mostra o endereço do array, não os valores.

Como consertar: mostre com um laço, elemento por elemento.

Valores sem sentido logo na primeira leitura

O que aconteceu: o array foi criado sem valores iniciais (int d[5];) e guarda lixo.

Como consertar: int d[5] = {}; zera tudo.

"no matching function for call to ‘size(int*&)’"

O que aconteceu: você usou std::size num array que foi passado para uma função, onde ele já virou um ponteiro e perdeu o tamanho.

Como consertar: passe o tamanho como parâmetro ou, melhor, use std::array ou std::vector, que levam o tamanho com eles.


Referência

FormaO que faz
int a[5] = {1, 2, 3, 4, 5};Cria um array de 5 int com valores.
int a[5] = {};Cria com todos os elementos valendo 0.
int a[] = {1, 2, 3};O tamanho (3) é contado pelo compilador.
a[i]O elemento da posição i (de 0 a tamanho − 1). Sem conferência!
a[i] = valor;Troca o elemento da posição i.
std::size(a)C++17 O tamanho (<iterator>). Só onde o array foi criado.
for (int i = 0; i < N; i++)Percorre com índice.
for (int x : a)Percorre sem índice.
g++ … -g -fsanitize=addressDetecta acesso fora do intervalo ao rodar.

Exercícios

1. O último elemento

Num array int v[8], qual é o índice do último elemento?

v[]

2. Criar zerado

Complete para criar um array de 10 int, todos valendo 0:

int contagem[10] = ;

3. A condição certa

Complete o laço que percorre int precos[6] sem sair do intervalo:

for (int i = 0; ; i++)

4. Depois de int a[4] = {5, 6};, quanto vale a[3]?

5. O que o C++ faz quando você lê notas[5] num array de 5 elementos?

6. Por que, no C++ moderno, se prefere std::vector ao array estilo C para uma lista de produtos digitados pelo usuário?

Missão final: a maior nota

Missão: percorra o array com um for com índice e descubra a maior nota e em que posição ela está. Mostre Maior nota: 10 (aluno 4); lembre-se de que o aluno 4 está no índice 3. Dica: comece com maior = notas[0] e posicao = 0.

#include <iostream>

int main() {
    constexpr int TOTAL = 5;
    int notas[TOTAL] = {7, 9, 6, 10, 8};
    int maior = notas[0];
    int posicao = 0;
    // procure a maior nota aqui

    std::cout << "Maior nota: " << maior << " (aluno " << posicao + 1 << ")\n";
}
#include <iostream>

int main() {
    constexpr int TOTAL = 5;
    int notas[TOTAL] = {7, 9, 6, 10, 8};
    int maior = notas[0];
    int posicao = 0;
    for (int i = 1; i < TOTAL; i++) {
        if (notas[i] > maior) {
            maior = notas[i];
            posicao = i;
        }
    }
    std::cout << "Maior nota: " << maior << " (aluno " << posicao + 1 << ")\n";
}

Resumo

  • Um array guarda vários valores do mesmo tipo: int notas[5] = {7, 9, 6, 8, 10};.
  • Os índices vão de 0 a tamanho − 1; notas[0] é o primeiro.
  • Sempre inicialize: {} zera tudo; sem valores, sobra lixo.
  • Percorra com for (int i = 0; i < N; i++) ou, sem índice, com for (int x : notas). std::size(notas) dá o tamanho C++17.
  • Acesso fora do intervalo não é conferido: é comportamento indefinido. Teste com -g -fsanitize=address.
  • O tamanho é fixo, std::cout << notas mostra um endereço, e numa função o array vira ponteiro e esquece o tamanho.
  • Por isso, prefira std::array (tamanho fixo) e std::vector (o padrão para listas).