C++ Ponteiros e Arrays
Arrays e ponteiros são parentes muito próximos, e entender esse parentesco explica várias coisas estranhas do C++: por que notas[2] funciona, por que um array "esquece" o tamanho quando entra numa função e por que o C++ moderno criou o std::span. A base de tudo você já viu em Como a Memória Funciona: os itens de um array moram colados, um depois do outro, como casas vizinhas numa rua.
- Por que o nome de um array, sozinho, vira o endereço do primeiro item.
- A aritmética de ponteiros:
p + 1anda um item, não um byte. - Que
notas[i]é só um jeito curto de escrever*(notas + i). - Como percorrer um array com um ponteiro.
- Por que, dentro de uma função,
sizeofde um array dá 8 (e o aviso-Wsizeof-array-argument). - O
std::spanC++20, que resolve esse problema.
Antes, leia Ponteiros e Arrays. Os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook, Linux de 64 bits), na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Os endereços mudam a cada execução; o que importa é a diferença entre eles.
A ideia: casas numa rua, sem buracos
Imagine um conjunto de quatro casas iguais, lado a lado, numa rua. Para chegar a qualquer uma delas, basta saber onde fica a primeira e quantas casas andar: "a primeira é a 52; a terceira fica duas casas depois". Um array é isso. E um ponteiro que aponta para a primeira casa pode andar pela rua, de casa em casa.
1. O nome do array vira ponteiro
Quando você usa o nome de um array numa expressão, ele quase sempre se transforma, sozinho, no endereço do primeiro item. Por isso dá para criar um ponteiro sem escrever &:
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int* p = notas; // sem &: o nome do array vira o endereço do 1º item
std::cout << "notas = " << notas << "\n";
std::cout << "¬as[0] = " << ¬as[0] << "\n";
std::cout << "p = " << p << "\n";
std::cout << "*p = " << *p << "\n";
}
$ g++ -std=c++23 -Wall -Wextra nome.cpp -o nome $ ./nome notas = 0x7ffed706ca00 « o array "virou" um endereço ¬as[0] = 0x7ffed706ca00 « que é o do primeiro item p = 0x7ffed706ca00 *p = 7 « seguindo o ponteiro: a primeira nota
Essa transformação se chama decaimento (em inglês, array-to-pointer decay): o array "decai" para um ponteiro. Ela é herança da linguagem C. Duas exceções importantes: sizeof(notas) e ¬as enxergam o array inteiro, sem decair. Guarde a palavra "decair": ela explica a armadilha da seção 4.
2. Aritmética de ponteiros: p + 1 anda um item
Você pode somar um número inteiro a um ponteiro. Mas p + 1 não é "o endereço seguinte, um byte depois". É "o próximo item", e o C++ calcula quantos bytes pular a partir do tipo do ponteiro:
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int* p = notas;
std::cout << "p = " << p << " *p = " << *p << "\n";
std::cout << "p + 1 = " << p + 1 << " *(p+1) = " << *(p + 1) << "\n";
std::cout << "p + 2 = " << p + 2 << " *(p+2) = " << *(p + 2) << "\n";
std::cout << "p + 3 = " << p + 3 << " *(p+3) = " << *(p + 3) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra aritmetica.cpp -o aritmetica $ ./aritmetica p = 0x7ffc2d276340 *p = 7 p + 1 = 0x7ffc2d276344 *(p+1) = 8 « +4 bytes = sizeof(int) p + 2 = 0x7ffc2d276348 *(p+2) = 6 « +8 bytes p + 3 = 0x7ffc2d27634c *(p+3) = 9 « +12 bytes (c = 12 em hexadecimal)
| Expressão | p | p + 1 | p + 2 | p + 3 |
|---|---|---|---|---|
| Endereço | …340 | …344 | …348 | …34c |
| Bytes ocupados | 340 a 343 | 344 a 347 | 348 a 34b | 34c a 34f |
Valor (*) | 7 | 8 | 6 | 9 |
| Mesmo que | notas[0] | notas[1] | notas[2] | notas[3] |
int ocupa 4 casas. Somar 1 ao ponteiro pula as 4 de uma vez, caindo bem no começo do próximo item.Os parênteses em *(p + 1) são obrigatórios: *p + 1 seria "o valor de *p, mais 1", ou seja, 7 + 1 = 8. Aqui deu o mesmo número por coincidência; com *(p + 2) e *p + 2 a diferença aparece (6 contra 9).
Com double, o passo é 8
Para provar que o passo depende do tipo, o mesmo teste com um array de double (8 bytes cada):
#include <iostream>
int main() {
double precos[3] = {4.5, 3.0, 12.9};
double* p = precos;
std::cout << "p = " << p << "\n";
std::cout << "p + 1 = " << p + 1 << "\n";
std::cout << "p + 2 = " << p + 2 << "\n";
}
$ g++ -std=c++23 -Wall -Wextra double.cpp -o double $ ./double p = 0x7ffc9951c0f0 p + 1 = 0x7ffc9951c0f8 « +8 = sizeof(double) p + 2 = 0x7ffc9951c100 « +16 (f0 + 10 em hexadecimal = 100)
É por isso que o ponteiro precisa ter o tipo certo (int*, double*): o tipo diz o tamanho de cada passo.
notas[i] é *(notas + i)
Agora o colchete deixa de ser mágico. Escrever notas[2] é exatamente o mesmo que "pegue o endereço do primeiro item, ande 2 itens e leia o que estiver lá". E o colchete funciona em ponteiros também:
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int* p = notas;
std::cout << notas[2] << " " << *(notas + 2) << " " << p[2] << " " << *(p + 2) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra colchete.cpp -o colchete $ ./colchete 6 6 6 6 « quatro jeitos de escrever a mesma coisa
Isso também explica por que o primeiro item é o [0]: ele está a zero passos do começo.
3. Percorrer um array com um ponteiro
Se p + 1 é o próximo item, então ++p (somar 1 ao próprio p) faz o ponteiro andar para o próximo. Dá para percorrer o array inteiro assim:
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int* fim = notas + 4; // uma casa DEPOIS do último item
for (int* p = notas; p != fim; ++p) {
std::cout << *p << " ";
}
std::cout << "\n";
std::cout << "Itens entre o começo e o fim: " << fim - notas << "\n";
}
$ g++ -std=c++23 -Wall -Wextra percorrer.cpp -o percorrer $ ./percorrer 7 8 6 9 Itens entre o começo e o fim: 4
- int* fim = notas + 4;
- Um ponteiro para a casa logo depois da última nota. O C++ permite calcular esse endereço "um depois do fim", mas não permite ler o que está lá (
*fimseria um erro grave). - int* p = notas;
- O ponteiro começa no primeiro item.
- p != fim
- Continua enquanto
pnão chegou ao fim. - ++p
- Anda um item (4 bytes).
- *p
- Lê o item atual.
- fim - notas
- Subtrair dois ponteiros do mesmo array dá quantos itens há entre eles (não quantos bytes).
Esse par "começo e um depois do fim" é a base dos iteradores da biblioteca padrão (v.begin() e v.end()). No dia a dia, para só percorrer, o for de intervalo (for (int n : notas)) é mais simples e não tem como errar o fim.
Missão: o laço já anda pelas notas com o ponteiro p. Complete o corpo para somar em soma o valor apontado por p. A saída deve ser Soma: 30.
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int soma = 0;
for (int* p = notas; p != notas + 4; ++p) {
// some o valor apontado por p em soma
}
std::cout << "Soma: " << soma << "\n";
}
#include <iostream>
int main() {
int notas[4] = {7, 8, 6, 9};
int soma = 0;
for (int* p = notas; p != notas + 4; ++p) {
soma = soma + *p;
}
std::cout << "Soma: " << soma << "\n";
}
Nada impede você de escrever *(p + 10) num array de 4 itens. O C++ não confere: ele calcula o endereço e lê o que estiver lá, que pode ser outra variável, lixo ou memória proibida. Isso é comportamento indefinido, e os sanitizers são a ferramenta para pegar esse erro. Com std::vector e std::array, o .at(i) confere e avisa. No editor do guia, ler fora do array passa calado (mostra um valor qualquer e o programa segue): é exatamente o perigo do comportamento indefinido.
4. A armadilha: o array perde o tamanho na função
Lembra do decaimento? Ao passar um array para uma função, o que viaja é só o endereço do primeiro item. A função recebe um ponteiro e não tem como saber quantos itens existem. Mesmo que você escreva o parâmetro como int notas[], ele é um int* disfarçado:
#include <iostream>
void mostrar(int notas[]) {
std::cout << "Dentro da função: " << sizeof(notas) << " bytes\n";
}
int main() {
int notas[4] = {7, 8, 6, 9};
std::cout << "No main: " << sizeof(notas) << " bytes\n";
mostrar(notas);
}
$ g++ -std=c++23 -Wall -Wextra funcao.cpp -o funcao
funcao.cpp: In function ‘void mostrar(int*)’:
funcao.cpp:4:49: warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int*’ [-Wsizeof-array-argument]
4 | std::cout << "Dentro da função: " << sizeof(notas) << " bytes\n";
| ~^~~~~~
funcao.cpp:3:18: note: declared here
3 | void mostrar(int notas[]) {
| ~~~~^~~~~~~
$ ./funcao
No main: 16 bytes « 4 itens × 4 bytes: o array inteiro
Dentro da função: 8 bytes « o tamanho de um ponteiro!
- In function ‘void mostrar(int*)’
- Olhe o cabeçalho: o próprio g++ escreve a função como
mostrar(int*). Oint notas[]que você digitou virouint*. - warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int*’
- "
sizeofnum parâmetro array de função vai devolver o tamanho deint*". O compilador sabe que você provavelmente queria outra coisa. - [-Wsizeof-array-argument]
- O nome do aviso. Vem ligado por padrão.
O truque sizeof(notas) / sizeof(notas[0]) da aula Como a Memória Funciona daria, dentro da função, 8 ÷ 4 = 2 itens, um número errado e sem erro nenhum na execução. É por isso que esse truque só serve onde o array foi declarado.
O jeito antigo: mandar o tamanho junto
No estilo C, a função recebe dois parâmetros: o ponteiro e a quantidade.
#include <iostream>
#include <cstddef>
void mostrar(const int* notas, std::size_t quantidade) {
for (std::size_t i = 0; i < quantidade; ++i) {
std::cout << notas[i] << " ";
}
std::cout << "\n";
}
int main() {
int notas[4] = {7, 8, 6, 9};
mostrar(notas, 4);
}
$ g++ -std=c++23 -Wall -Wextra funcao-tam.cpp -o funcao-tam $ ./funcao-tam 7 8 6 9
Funciona, mas nada impede alguém de chamar mostrar(notas, 40). O const int* diz que a função só vai ler as notas (veja const com Ponteiros e Referências), e std::size_t é o tipo usado para tamanhos e quantidades.
O jeito moderno: std::span C++20
Um std::span ("vão", "trecho") é um ponteiro que carrega o tamanho junto. Ele não copia os itens: só olha para eles, como uma janela. E aceita, sem mudar nada na função, arrays, std::array e std::vector:
#include <iostream>
#include <span>
#include <vector>
void mostrar(std::span<const int> notas) {
std::cout << notas.size() << " notas: ";
for (int n : notas) {
std::cout << n << " ";
}
std::cout << "\n";
}
int main() {
int notas[4] = {7, 8, 6, 9};
std::vector<int> outras = {10, 5};
mostrar(notas); // array: o span descobre o tamanho sozinho
mostrar(outras); // vector também serve
}
$ g++ -std=c++23 -Wall -Wextra span.cpp -o span $ ./span 4 notas: 7 8 6 9 2 notas: 10 5
- #include <span>
- O cabeçalho do
std::span. - std::span<const int>
- "Uma janela para vários
intque eu só vou ler". Sem oconst, a função poderia mudar os itens. - notas.size()
- O tamanho viajou junto. Funciona também o
forde intervalo.
O std::span é do C++20. Com -std=c++17, o g++ 15 responde error: ‘span’ is not a member of ‘std’ e explica: note: ‘std::span’ is only available from C++20 onwards. Antes dele, a alternativa era a dupla ponteiro + tamanho, ou receber const std::vector<int>& (que só aceita vetores). Esta aula só apresenta o std::span; ele tem mais recursos, como pegar só um pedaço (notas.first(2)).
Formas de passar uma lista para uma função: qual usar
| Parâmetro | Sabe o tamanho? | Aceita | Quando usar |
|---|---|---|---|
int notas[] ou int* notas | Não | Arrays e ponteiros | Só em código C ou antigo. |
const int* notas, std::size_t n | Se quem chama acertar o n | Arrays, ponteiros | Bibliotecas em estilo C. |
const std::vector<int>& notas | Sim | Só std::vector | Quando o programa só usa vetores (o caso mais comum). |
std::span<const int> notas C++20 | Sim | Arrays, std::array, std::vector | Função que só lê uma sequência, venha de onde vier. |
Quando dá errado
$ g++ -std=c++23 -Wall -Wextra funcao.cpp -o funcao
funcao.cpp: In function ‘void mostrar(int*)’:
funcao.cpp:4:49: warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int*’ [-Wsizeof-array-argument]
4 | std::cout << "Dentro da função: " << sizeof(notas) << " bytes\n";
| ~^~~~~~
funcao.cpp:3:18: note: declared here
3 | void mostrar(int notas[]) {
| ~~~~^~~~~~~
O que aconteceu: o array decaiu para ponteiro ao entrar na função, e sizeof mediu o ponteiro (8 bytes). No editor do guia (Clang), a mensagem é sizeof on array function parameter will return size of 'int *' instead of 'int[]'.
Como resolver: receba std::span<const int> e use .size(), ou passe a quantidade num segundo parâmetro, ou use std::vector.
$ g++ -std=c++17 -Wall -Wextra span.cpp -o span
span.cpp:5:6: error: variable or field ‘mostrar’ declared void
5 | void mostrar(std::span<const int> notas) {
| ^~~~~~~
span.cpp:5:19: error: ‘span’ is not a member of ‘std’
5 | void mostrar(std::span<const int> notas) {
| ^~~~
span.cpp:5:19: note: ‘std::span’ is only available from C++20 onwards
… « mais erros, todos consequência deste
O que aconteceu: o código pediu C++17, e o std::span só existe a partir do C++20. A nota diz isso com todas as letras: "std::span só está disponível do C++20 em diante". A primeira mensagem ("declarada como void") e as seguintes são consequência: sem saber o que é span, o g++ se perdeu. Leia sempre a primeira mensagem que faz sentido (veja Lendo Erros do Compilador).
Como resolver: compile com -std=c++23 (o padrão do guia) e inclua <span>. No g++ 15, o programa até compila sem o #include <span>, porque outro cabeçalho o traz junto, mas não conte com isso: em outro compilador, pode faltar.
O que aconteceu: o ponteiro passou do fim do array, por exemplo com p <= fim em vez de p != fim, ou com a quantidade errada. O C++ leu as casas vizinhas, sem avisar.
Como resolver: confira a condição de parada; prefira o for de intervalo, std::span ou std::vector, que sabem o tamanho. Para investigar, compile com -fsanitize=address (aula Sanitizers).
Referência rápida
| Código | O que faz |
|---|---|
int* p = notas; | O nome do array decai para o endereço do primeiro item (¬as[0]). |
p + i | O endereço do item i passos à frente (anda i × sizeof(tipo) bytes). |
*(p + i) ou p[i] | O valor do item i. |
++p / --p | Anda um item para a frente / para trás. |
fim - comeco | Quantos itens há entre dois ponteiros do mesmo array. |
notas + 4 (num array de 4) | "Um depois do fim": pode calcular e comparar, não pode ler. |
void f(int v[]) | É o mesmo que void f(int* v): o tamanho se perde. |
std::span<const int> v | C++20 Ponteiro + tamanho; v.size(), for (int n : v). |
Exercícios
1. O segundo item
Com int* p = notas;, complete para mostrar notas[1] usando aritmética de ponteiros:
2. Quantos bytes?
Num array de double, p está em 0x1000. Em que endereço está p + 2? (Escreva em hexadecimal, com 0x.)
3. Com int notas[4] = {7, 8, 6, 9};, quanto vale *notas + 2?
4. Dentro de void f(int v[]), chamada com um array de 10 int, quanto dá sizeof(v) no Linux de 64 bits?
5. O que o std::span traz que um int* não traz?
Missão final: a média de qualquer turma
Missão: a função media recebe as notas como std::span. Complete-a para somar as notas e dividir pela quantidade (notas.size()). Cuidado: divida como double, senão 15 ÷ 2 vira 7. A saída deve ser:
Média A: 7.5 Média B: 8
#include <iostream>
#include <span>
double media(std::span<const int> notas) {
// some as notas e divida pela quantidade (notas.size())
return 0;
}
int main() {
int turma_a[4] = {7, 8, 6, 9};
int turma_b[2] = {10, 6};
std::cout << "Média A: " << media(turma_a) << "\n";
std::cout << "Média B: " << media(turma_b) << "\n";
}
#include <iostream>
#include <span>
double media(std::span<const int> notas) {
int soma = 0;
for (int n : notas) {
soma = soma + n;
}
return static_cast<double>(soma) / notas.size();
}
int main() {
int turma_a[4] = {7, 8, 6, 9};
int turma_b[2] = {10, 6};
std::cout << "Média A: " << media(turma_a) << "\n";
std::cout << "Média B: " << media(turma_b) << "\n";
}
Missão extra (provoque o aviso): dentro de contar, troque o return 0; por return sizeof(lista) / sizeof(lista[0]); e rode. Leia o aviso e veja que a função não responde 6: ela mediu um ponteiro, não o array. (No Linux de 64 bits da Ana, responde Itens: 2.)
#include <iostream>
int contar(int lista[]) {
return 0;
}
int main() {
int precos[6] = {4, 3, 12, 7, 1, 5};
std::cout << "Itens: " << contar(precos) << "\n";
}
#include <iostream>
int contar(int lista[]) {
return sizeof(lista) / sizeof(lista[0]);
}
int main() {
int precos[6] = {4, 3, 12, 7, 1, 5};
std::cout << "Itens: " << contar(precos) << "\n";
}
Resumo
- O nome de um array, numa expressão, decai para o endereço do primeiro item:
int* p = notas;. p + 1anda um item (sizeof(int)= 4 bytes paraint, 8 paradouble).notas[i]é o mesmo que*(notas + i); o colchete funciona em ponteiros.- Dá para percorrer com
for (int* p = notas; p != notas + n; ++p), mas oforde intervalo é mais seguro. - Array passado para função vira ponteiro e perde o tamanho:
sizeofdá 8 (-Wsizeof-array-argument). - Use
std::spanC++20 (oustd::vector) para passar sequências com o tamanho junto. - Próxima aula: nullptr, o ponteiro que não aponta para nada.