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.

Você vai aprender
  • Que o nome do array vira um ponteiro para o primeiro item.
  • Que p + 1 anda um item, e não um byte (a "aritmética de ponteiros").
  • Que *(p + i) e p[i] são exatamente a mesma coisa.
  • Como percorrer um array com um ponteiro.
  • A diferença entre sizeof de 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("&notas[0] = %p\n", (void *) &notas[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
&notas[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 &notas[0]
int *p = &notas[0];    // escrito por extenso
"Vira" ponteiro, mas não "é" ponteiro O array continua sendo um array: 4 gavetas de 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 &notas. 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
…0e07…0e48…0e86…0ec9p → *pp+1 → *(p+1)p+2 → *(p+2)p+3 → *(p+3)
Cada caixa é um 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ãoLê-seCom 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 colchetesCom ponteiroSignificado
notas[0]*notasO primeiro item
notas[i]*(notas + i)O item de índice i
&notas[i]notas + iO 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). Enquanto p estiver 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 = &notas[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.

7869&notas[0]notas do main: 16 bytesnotas da função: 8 byteso que viajou
O array mora no 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âmetroO que é de verdadeUse?
int *v, int nUm ponteiro e o número de itens.Sim. É o padrão em C: claro e honesto.
int v[], int nO 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 dentroO 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

"assignment to expression with array type": um array não recebe outro
#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).

"‘sizeof’ on array function parameter": contar itens na função
$ 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).

Andou além do fim do array

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ódigoO que faz
int *p = notas;O nome do array vira o endereço do primeiro item (&notas[0]).
p + iO 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 - pQuantos itens há entre dois ponteiros do mesmo array (ptrdiff_t, formato %td).
p < notas + nCompara 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]:

printf("%d\n", );

2. Quantos bytes?

Um double *q vale 0x1000. Quanto vale q + 1? (em hexadecimal, com 0x)

q + 1 =

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 que int *p = &notas[0];.
  • p + 1 anda um item (4 bytes para int, 8 para double), 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.
  • sizeof do array mede tudo; sizeof do 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.