C Ponteiros e Arrays
Em C, ponteiros e arrays são parentes muito próximos. Tão próximos que, na maior parte do tempo, o nome de um array se comporta como um ponteiro, e um ponteiro pode ser usado com colchetes, como um array. Entender esse parentesco explica várias coisas que pareciam mágica: por que uma função consegue mudar os itens de um array que você passou, por que o sizeof "erra" dentro de uma função e como andar de item em item somando ao ponteiro.
- Que o nome do array vira um ponteiro para o primeiro item.
- Que
p + 1anda um item, e não um byte (a "aritmética de ponteiros"). - Que
*(p + i)ep[i]são exatamente a mesma coisa. - Como percorrer um array com um ponteiro.
- A diferença entre
sizeofde um array e de um ponteiro, e a armadilha que isso cria nas funções.
Os programas foram compilados na máquina da Ana, na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Antes de continuar, tenha frescas as aulas Arrays e Ponteiros.
A ideia: casas numa rua, sem buracos
Pense numa rua em que as casas têm todas o mesmo tamanho e ficam coladas, uma depois da outra. Se você sabe onde fica a primeira casa, sabe onde ficam todas: "a terceira casa fica duas casas depois da primeira". Você não precisa de um papel com o endereço de cada uma: um papel com o endereço da primeira basta.
Um array é essa rua. Você viu em Memória e Endereços que os itens moram em gavetas vizinhas, sem buracos. Um ponteiro para o primeiro item é o papel com o endereço da primeira casa. A partir dele, "andar duas casas" é uma conta.
1. O nome do array vira ponteiro
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int *p = notas;
printf("notas = %p\n", (void *) notas);
printf("¬as[0] = %p\n", (void *) ¬as[0]);
printf("p = %p\n", (void *) p);
printf("*p = %d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra nome.c -o nome $ ./nome notas = 0x7fff31424a00 ¬as[0] = 0x7fff31424a00 « o mesmo endereço p = 0x7fff31424a00 *p = 7 « p aponta para o primeiro item
Repare na linha int *p = notas;: não tem &. Quando você usa o nome de um array numa expressão, o C o troca automaticamente pelo endereço do primeiro item. Em inglês, isso se chama array decay ("o array decai", vira ponteiro). As duas linhas abaixo fazem a mesma coisa:
int *p = notas; // o nome do array vira ¬as[0] int *p = ¬as[0]; // escrito por extenso
int, em lugar fixo. O que acontece é que, na maior parte das expressões, o nome dele é trocado pelo endereço do primeiro item. As exceções importantes são o sizeof (seção 5) e o ¬as. E um array não pode receber outro valor: notas = outro; é erro (veja "Quando dá errado").
2. p + 1 anda um item, não um byte
Somar um número a um ponteiro é chamado de aritmética de ponteiros. A regra que surpreende: p + 1 não é "o endereço seguinte", é "o item seguinte". O compilador multiplica o 1 pelo tamanho do tipo apontado:
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
double precos[3] = {4.5, 3.0, 12.9};
int *p = notas;
double *q = precos;
printf("p = %p *p = %d\n", (void *) p, *p);
printf("p + 1 = %p *(p+1) = %d\n", (void *) (p + 1), *(p + 1));
printf("p + 2 = %p *(p+2) = %d\n", (void *) (p + 2), *(p + 2));
printf("q = %p *q = %.2f\n", (void *) q, *q);
printf("q + 1 = %p *(q+1) = %.2f\n", (void *) (q + 1), *(q + 1));
return 0;
}
$ gcc -std=c23 -Wall -Wextra passo.c -o passo $ ./passo p = 0x7ffc5828d0e0 *p = 7 p + 1 = 0x7ffc5828d0e4 *(p+1) = 8 « +4 bytes: um int p + 2 = 0x7ffc5828d0e8 *(p+2) = 6 « +8 bytes: dois ints q = 0x7ffc5828d0f0 *q = 4.50 q + 1 = 0x7ffc5828d0f8 *(q+1) = 3.00 « +8 bytes: um double
int (4 gavetas). Para um ponteiro de int, "+1" pula as 4 gavetas de um item inteiro e cai exatamente no começo do próximo.É por isso que o tipo do ponteiro importa tanto: ele diz o tamanho do passo. Com int *, cada passo é de 4 bytes; com double *, de 8; com char *, de 1. Se p + 1 andasse só um byte, cairia no meio do item, o que não teria sentido nenhum.
Missão: sem usar colchetes, mostre a terceira nota (o item de índice 2) usando o ponteiro p e a aritmética de ponteiros: *(p + 2).
Primeira nota: 7 Terceira nota: 6
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int *p = notas;
printf("Primeira nota: %d\n", *p);
// mostre "Terceira nota: " e o item de índice 2, pelo p
return 0;
}
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int *p = notas;
printf("Primeira nota: %d\n", *p);
printf("Terceira nota: %d\n", *(p + 2));
return 0;
}
Os parênteses importam
*(p + 2) e *p + 2 são coisas diferentes. O * vem antes do + na precedência:
| Expressão | Lê-se | Com notas = {7, 8, 6, 9} |
|---|---|---|
*(p + 2) | "anda 2 itens, depois pega o valor" | 6 (o item 2) |
*p + 2 | "pega o valor, depois soma 2" | 9 (7 + 2) |
3. p[i] é *(p + i)
Aqui está o segredo dos colchetes: em C, a[i] é definido como *(a + i). Os colchetes são só um jeito mais bonito de escrever "ande i itens e pegue o valor". Por isso eles funcionam tanto no nome do array quanto num ponteiro:
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int *p = notas;
for (int i = 0; i < 4; i++) {
printf("i=%d notas[i]=%d *(notas+i)=%d p[i]=%d *(p+i)=%d\n",
i, notas[i], *(notas + i), p[i], *(p + i));
}
return 0;
}
$ gcc -std=c23 -Wall -Wextra colchete.c -o colchete $ ./colchete i=0 notas[i]=7 *(notas+i)=7 p[i]=7 *(p+i)=7 i=1 notas[i]=8 *(notas+i)=8 p[i]=8 *(p+i)=8 i=2 notas[i]=6 *(notas+i)=6 p[i]=6 *(p+i)=6 i=3 notas[i]=9 *(notas+i)=9 p[i]=9 *(p+i)=9
As quatro formas dão sempre o mesmo valor. Isso também explica por que os índices começam em 0: o índice é a distância desde o primeiro item. O primeiro está a 0 passos dele mesmo.
| Com colchetes | Com ponteiro | Significado |
|---|---|---|
notas[0] | *notas | O primeiro item |
notas[i] | *(notas + i) | O item de índice i |
¬as[i] | notas + i | O endereço do item de índice i |
p[i] = 5; | *(p + i) = 5; | Escreve no item de índice i |
4. Percorrer um array com um ponteiro
Em vez de um índice i que vai de 0 a 3, dá para usar um ponteiro que começa no primeiro item e vai andando, um item por vez, até passar do último:
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int soma = 0;
for (int *p = notas; p < notas + 4; p++) {
printf("p aponta para %p, que guarda %d\n", (void *) p, *p);
soma += *p;
}
printf("Soma: %d\n", soma);
return 0;
}
$ gcc -std=c23 -Wall -Wextra percorrer.c -o percorrer $ ./percorrer p aponta para 0x7ffe5748e290, que guarda 7 p aponta para 0x7ffe5748e294, que guarda 8 p aponta para 0x7ffe5748e298, que guarda 6 p aponta para 0x7ffe5748e29c, que guarda 9 Soma: 30
- int *p = notas;
- Começo: o ponteiro aponta para o primeiro item.
- p < notas + 4;
- Condição:
notas + 4é o endereço logo depois do último item (o "fim" da rua). Enquantopestiver antes dele, ainda há item para ler. Ponteiros podem ser comparados com<quando apontam para o mesmo array. Apontar para "um depois do último" é permitido; ler ali (*(notas + 4)) não. - p++
- Passo: anda um item (4 bytes, porque é
int *). - *p
- O item atual.
Qual usar, índice ou ponteiro? Para o computador, tanto faz: o gcc gera praticamente o mesmo código. Para quem lê, o for com índice (notas[i]) costuma ser mais claro, e é o que você deve preferir. Percorrer com ponteiro aparece muito em código C de verdade, principalmente com strings, então é importante saber ler.
A distância entre dois ponteiros
Assim como dá para somar um número a um ponteiro, dá para subtrair dois ponteiros do mesmo array. O resultado é quantos itens há entre eles (e não quantos bytes):
#include <stdio.h>
int main(void) {
int notas[5] = {7, 8, 6, 9, 10};
int *inicio = notas;
int *maior = ¬as[4];
printf("maior - inicio = %td itens\n", maior - inicio);
return 0;
}
$ gcc -std=c23 -Wall -Wextra distancia.c -o distancia $ ./distancia maior - inicio = 4 itens « 16 bytes ÷ 4 bytes por int
O resultado da subtração é do tipo ptrdiff_t ("diferença de ponteiros"), e o formato do printf para ele é %td. Somar dois ponteiros, por outro lado, não faz sentido (somar dois endereços de casas não dá endereço nenhum) e é erro.
5. sizeof: array × ponteiro
A exceção mais importante à regra "o nome vira ponteiro" é o sizeof. Com o nome do array, ele mede o array inteiro. Com um ponteiro, mede só o ponteiro, o "papel com o endereço":
#include <stdio.h>
void mostrar(int notas[]) {
printf("Na função: sizeof(notas) = %zu\n", sizeof(notas));
}
int main(void) {
int notas[4] = {7, 8, 6, 9};
int *p = notas;
printf("No main: sizeof(notas) = %zu\n", sizeof(notas));
printf("No main: sizeof(p) = %zu\n", sizeof(p));
mostrar(notas);
return 0;
}
$ gcc -std=c23 -Wall -Wextra tamanho.c -o tamanho
tamanho.c: In function ‘mostrar’:
tamanho.c:4:54: warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int *’ [-Wsizeof-array-argument]
4 | printf("Na função: sizeof(notas) = %zu\n", sizeof(notas));
| ^
tamanho.c:3:18: note: declared here
3 | void mostrar(int notas[]) {
| ~~~~^~~~~~~
$ ./tamanho
No main: sizeof(notas) = 16 « o array: 4 itens × 4 bytes
No main: sizeof(p) = 8 « o ponteiro: um endereço
Na função: sizeof(notas) = 8 « na função, notas já é um ponteiro!
A terceira linha é a surpresa. Quando você passa um array para uma função, o que viaja é só o endereço do primeiro item (o nome "virou ponteiro" na chamada). Dentro da função, int notas[] é só outro jeito de escrever int *notas. O array de verdade ficou no main. O gcc percebe e avisa: "o sizeof num parâmetro de array vai devolver o tamanho de int *". No editor do guia, os números são 16, 4 e 4 (lá um ponteiro ocupa 4 bytes), e o Clang avisa do mesmo jeito.
main. A função recebe só o "papel" com o endereço do primeiro item; ela não sabe quantos itens vêm depois.A armadilha: contar itens dentro da função
A conta sizeof(notas) / sizeof(notas[0]), que descobre quantos itens um array tem, dá resultado errado dentro de uma função:
#include <stdio.h>
double media(int notas[]) {
int n = sizeof(notas) / sizeof(notas[0]);
int soma = 0;
for (int i = 0; i < n; i++) {
soma += notas[i];
}
printf("(a função acha que são %d notas)\n", n);
return (double) soma / n;
}
int main(void) {
int notas[5] = {7, 8, 6, 9, 10};
printf("Média: %.2f\n", media(notas));
return 0;
}
$ gcc -std=c23 -Wall -Wextra armadilha.c -o armadilha
armadilha.c: In function ‘media’:
armadilha.c:4:19: warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int *’ [-Wsizeof-array-argument]
4 | int n = sizeof(notas) / sizeof(notas[0]);
| ^
armadilha.c:3:18: note: declared here
3 | double media(int notas[]) {
| ~~~~^~~~~~~
$ ./armadilha
(a função acha que são 2 notas) « 8 bytes do ponteiro ÷ 4 bytes do int
Média: 7.50 « a média só de 7 e 8. A certa é 8.00
Nenhuma falha, nenhuma mensagem de erro: só um resultado errado, que passaria despercebido se você não conferisse. No editor, a função acha que é 1 nota (4 ÷ 4) e mostra Média: 7.00. O conserto é a regra de ouro das funções que recebem arrays em C: mande o tamanho junto, contado no lugar onde o array de verdade existe:
#include <stdio.h>
double media(int *notas, int n) {
int soma = 0;
for (int i = 0; i < n; i++) {
soma += notas[i];
}
return (double) soma / n;
}
int main(void) {
int notas[5] = {7, 8, 6, 9, 10};
int n = sizeof(notas) / sizeof(notas[0]);
printf("Média: %.2f\n", media(notas, n));
return 0;
}
$ gcc -std=c23 -Wall -Wextra media.c -o media $ ./media Média: 8.00
Repare que o parâmetro agora está escrito como int *notas, que é o que ele realmente é. int notas[] também funcionaria (é o mesmo tipo), mas engana quem lê. Dentro da função, notas[i] continua funcionando, porque p[i] vale para qualquer ponteiro.
Formas de passar um array para uma função: qual usar
| Parâmetro | O que é de verdade | Use? |
|---|---|---|
int *v, int n | Um ponteiro e o número de itens. | Sim. É o padrão em C: claro e honesto. |
int v[], int n | O mesmo que int *v. | Pode; muita gente usa para dizer "aqui vem um array". |
int v[5] | Também o mesmo que int *v! O 5 é ignorado pelo compilador. | Evite: promete um tamanho que ninguém confere. |
int v[] sem o tamanho, e sizeof dentro | O sizeof mede o ponteiro. | Nunca. |
Como a função recebe um ponteiro para os itens originais, ela pode mudá-los (o main vê a mudança). Quando a função só precisa ler, a aula const e Ponteiros mostra como prometer isso com const int *v.
Quando dá errado
#include <stdio.h>
int main(void) {
int notas[4] = {7, 8, 6, 9};
int outras[4] = {1, 2, 3, 4};
notas = outras;
notas++;
printf("%d\n", notas[0]);
return 0;
}
$ gcc -std=c23 -Wall -Wextra atribuir.c -o atribuir
atribuir.c: In function ‘main’:
atribuir.c:7:11: error: assignment to expression with array type
7 | notas = outras;
| ^
atribuir.c:8:10: error: lvalue required as increment operand
8 | notas++;
| ^~
O que significa: "atribuição a uma expressão de tipo array" e "é preciso um lvalue no ++". O nome do array se comporta como o endereço do primeiro item, mas esse endereço é fixo: o array mora onde mora, e não dá para "mudá-lo de lugar" nem fazê-lo andar. No editor, o Clang diz array type 'int[4]' is not assignable e cannot increment value of type 'int[4]'.
Como consertar: para andar, use um ponteiro (int *p = notas; p++;). Para copiar os itens, copie um por um num for, ou use memcpy (veja Funções de String).
$ gcc -std=c23 -Wall -Wextra armadilha.c -o armadilha
armadilha.c: In function ‘media’:
armadilha.c:4:19: warning: ‘sizeof’ on array function parameter ‘notas’ will return size of ‘int *’ [-Wsizeof-array-argument]
4 | int n = sizeof(notas) / sizeof(notas[0]);
| ^
O que significa: dentro da função, o parâmetro é um ponteiro, e o sizeof mede o ponteiro (8 bytes), não o array.
Como consertar: conte os itens no lugar em que o array foi criado e passe o número como outro parâmetro: media(notas, n).
O que acontece: *(p + 10) num array de 4 itens lê uma gaveta que não é do array. O C não confere: não há erro de compilação e, na maioria das vezes, nem falha de segmentação. O programa simplesmente lê (ou, pior, escreve) o que estiver lá. É o mesmo problema do índice fora do array da aula Arrays, porque p[10] e *(p + 10) são a mesma coisa. No editor do guia, isso também passa calado.
Como evitar: todo laço com ponteiro precisa de um fim claro (p < notas + n). Para caçar esse tipo de erro, compile com -fsanitize=address (veja Sanitizers).
Referência rápida
| Código | O que faz |
|---|---|
int *p = notas; | O nome do array vira o endereço do primeiro item (¬as[0]). |
p + i | O endereço do item i posições à frente (anda i × sizeof(*p) bytes). |
*(p + i) ≡ p[i] | O item de índice i. Exatamente a mesma coisa. |
p++ / p-- | Anda um item para frente / para trás. |
q - p | Quantos itens há entre dois ponteiros do mesmo array (ptrdiff_t, formato %td). |
p < notas + n | Compara ponteiros do mesmo array (condição de fim de laço). |
sizeof(notas) | O array inteiro (só onde o array foi criado). |
sizeof(p) | Só o ponteiro: 8 no Linux de 64 bits, 4 no editor. |
void f(int *v, int n) | O jeito certo de receber um array numa função: ponteiro e tamanho. |
Exercícios
1. O segundo item
O ponteiro p aponta para notas[0]. Complete, sem colchetes, para mostrar notas[1]:
2. Quantos bytes?
Um double *q vale 0x1000. Quanto vale q + 1? (em hexadecimal, com 0x)
3. Com colchetes
Reescreva *(v + 3) com colchetes:
4. Dentro de void f(int v[]), no Linux de 64 bits, quanto vale sizeof(v)?
5. Com int notas[4] = {7, 8, 6, 9}; int *p = notas;, quanto vale *p + 2?
6. Qual linha dá erro de compilação?
Missão final: a média de qualquer turma
Missão: escreva a função media, que recebe um ponteiro para as notas e a quantidade: double media(int *v, int n). Ela deve somar as n notas e devolver a média. O main já está pronto e chama a função com duas turmas de tamanhos diferentes.
Turma A: 8.00 Turma B: 6.50
#include <stdio.h>
// escreva aqui: double media(int *v, int n) { ... }
int main(void) {
int turma_a[5] = {7, 8, 6, 9, 10};
int turma_b[2] = {5, 8};
printf("Turma A: %.2f\n", media(turma_a, 5));
printf("Turma B: %.2f\n", media(turma_b, 2));
return 0;
}
#include <stdio.h>
double media(int *v, int n) {
int soma = 0;
for (int i = 0; i < n; i++) {
soma += v[i];
}
return (double) soma / n;
}
int main(void) {
int turma_a[5] = {7, 8, 6, 9, 10};
int turma_b[2] = {5, 8};
printf("Turma A: %.2f\n", media(turma_a, 5));
printf("Turma B: %.2f\n", media(turma_b, 2));
return 0;
}
Resumo
- O nome de um array, numa expressão, vira o endereço do primeiro item:
int *p = notas;é o mesmo queint *p = ¬as[0];. p + 1anda um item (4 bytes paraint, 8 paradouble), e não um byte.p[i]é, por definição,*(p + i). Os colchetes funcionam em arrays e em ponteiros.- Dá para percorrer um array com
for (int *p = v; p < v + n; p++), mas o índice costuma ser mais claro. sizeofdo array mede tudo;sizeofdo ponteiro mede só o endereço. Numa função, o "array" recebido é um ponteiro: mande o tamanho junto.- Um array não pode ser atribuído nem andar (
notas++é erro). E o C não confere se você passou do fim. - Próxima aula: Passar por Ponteiro, funções que mudam variáveis de quem as chamou.