Ponteiros não são tão difíceis — você só aprendeu do jeito errado

Ao primeiro contato, ponteiros acabam assustando muitas pessoas. Na maioria das vezes, isso não acontece porque ponteiros é um tópico difícil, mas porque são mal explicados. Vou tentar te ajudar com isso.

Suponha que você declare um valor no seu código e queira aplicar uma função que altere essa variável. Por exemplo:

struct exemplo {
  int x;
};

void set_x(struct exemplo a, int x) {
  a.x = x;
}

int main(void) {
  struct exemplo b = {0};  // iniciaiza tudo com 0
  set_x(b, 1);
  printf("b.x = %d\n", b.x);
  return 0;
}

À primeira vista, parece que isso deveria funcionar. Mas não funciona.

Em C, os argumentos de função são passados por valor. Isso significa que a função recebe uma cópia do que você passa para ela. No código acima, estamos passando uma cópia da struct, portanto quando alteramos essa cópia nada acontece com o valor original. Quando usamos ponteiros, a função ainda recebe uma cópia, porém estamos passando uma cópia do endereço. Como o endereço aponta para o lugar original, conseguimos alterar o valor real.

É por esse e outros motivos que usamos ponteiros.

Para fazer esse código funcionar, podemos mudar um pouco. Em vez de passar a variável para a função, vamos passar o endereço dela.

Quando escrevemos:

int a = 10;

Estamos criando um espaço na memória que armazena o valor 10, e esse espaço tem um endereço na memória do computador. Por exemplo, 0x00000001.

Quando criamos um ponteiro:

int* ptr = &a;

Estamos criando um espaço na memória que armazena o endereço de outro valor; neste caso, *ptr aponta para o endereço 0x00000001 da variável a. Então, na memória, teremos algo assim.

NomeEndereçoValorO que significa?
a0x0000000110Um número inteiro.
ptr0x000000020x00000001O endereço de memória da variável a.

Para acessar o valor de a através do ponteiro, precisamos usar o * (operados de desreferência).

  • ptr sozinho vale 0x00000001 (endereço de a).
  • *ptr “segue” o endereço e chega no valor de a (10).

Obs: Cuidado! *ptr não é o nome da variável. O nome da variável é apenas ptr. O asterisco no int* serve para dizer que o tipo daquela variável é um ponteiro para um número inteiro. O asterisco antes do nome *ptr é apenas uma instrução para o computador dizendo para acessar o que está naquele endereço.

Agora que entendemos isso, podemos ver que, se mudarmos o valor apontado por ptr (usando *ptr = 0) para 0, o valor de a também será alterado para 0, porque ambos se referem ao mesmo local de memória.

Sabendo disso, conseguimos corrigir o problema anterior:

struct exemplo {
  int x;
};

void set_x(struct exemplo* a, int x) {
  a->x = x;
}

int main(void) {
  struct exemplo b = {0};  // iniciaiza tudo com 0
  struct exemplo* ptr = &b;
  set_x(ptr, 1);
  printf("b.x = %d\n", b.x);
  return 0;
}

Agora, repare no que mudamos

  • Criamos um ponteiro que armazena o endereço da struct;
  • A função set_x agora recebe um ponteiro para a struct, não uma struct comum;
  • Dentro da função, a->x = x modifica o valor naquele endereço de memória referenciado.

Agora o código funciona perfeitamente.

Obs: note que a->x = x; é apenas syntax sugar para (*a).x = x;.

Claro que ponteiros são usados por vários outros motivos, mas este exemplo já é suficiente para entender a ideia central :).