Ir para o conteúdo

CCT-UFCA/Ciência da Computação/Introdução à Programação/Ponteiro

De Wikiversidade

Organização da memória

[editar | editar código]

Um dos recursos mais importantes e utilizados na linguagem C são os ponteiros. Eles desempenham um papel fundamental, especialmente quando associados à alocação dinâmica — assunto que será abordado mais adiante. Seu uso oferece uma poderosa ferramenta para a construção de algoritmos mais eficientes. Por outro lado, é muito comum ouvir, principalmente de iniciantes na programação, queixas sobre a aparente complexidade dos ponteiros. Por isso, antes de explorarmos o tema de forma mais aprofundada, é essencial ter sempre em mente o funcionamento da memória principal ao trabalhar com ponteiros.

Prosseguindo, é importante relembrar que, quando executamos um programa, ele é carregado na memória RAM. Isso significa que tanto o código quanto as variáveis declaradas fazem parte do programa e, portanto, são armazenados na memória principal. Para facilitar a compreensão, observe a representação da memória principal abaixo. Em algum local da memória estará o programa meu_programa.c, juntamente com suas variáveis alocadas armazenando os valores a elas atribuídos. Segue a ideia da representação da memória:

RAM

SO programas ... meu_programa.c ...

meu_programa.c

int a int b int c fload d ...

⚠️ Apesar da ilustração sugerir, e de fato ocorrer em muitos casos, a alocação na memória não necessariamente ocorre em blocos contíguos de células - o endereçamento é feito de forma independente, com base nos espaços disponíveis na memória no momento da execução.

Esse poderia ser um exemplo fictício do meu_programa.c.

#include <stdio.h>

int main () {
    int a = 10;
    int b = 20;
    int c = 30;
    float d = 40;
    
    printf("&a = %p\n", &a);
    printf("&b = %p\n", &b);
    printf("&c = %p\n", &c);
    printf("&d = %p\n", &d);
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    printf("d = %f\n", d);
    
    return 0;
}

Saída:

&a = 0x7ffe51dc970c
&b = 0x7ffe51dc9708
&c = 0x7ffe51dc9704
&d = 0x7ffe51dc9700
a = 10
b = 20
c = 30
d = 40.000000

Se observarmos os endereços das variáveis no exemplo fornecido, podemos perceber um detalhe importante:

&d = 0x7ffe51dc9700
&c = 0x7ffe51dc9704
&b = 0x7ffe51dc9708
&a = 0x7ffe51dc970c

Cada variável do tipo int ocupa 4 bytes e, em muitas arquiteturas (como x86_64), ela é alocada em endereços que decrescem na pilha conforme novas variáveis são criadas. Esse comportamento pode variar de acordo com o compilador e a arquitetura, mas a disposição ilustrada é típica em sistemas 64 bits. Com esse endereço em mãos, é possível alterar ou ler o valor contido em cada uma dessas variáveis.

Além disso, saber que cada tipo possui um tamanho fixo (por exemplo, int - 4 bytes; double - 8 bytes) será fundamental quando você chegar ao tópico de aritmética de ponteiros, pois o deslocamento em memória depende exatamente desses tamanhos. Para entender melhor o tamanho de cada tipo e seus modificadores, consulte a seção “Tipos Básicos de Dados, Operadores e Expressões”.

Conceito de ponteiro

[editar | editar código]

Agora, entrando de fato no conceito de ponteiros: um ponteiro é uma variável, assim como as que já estamos acostumados a utilizar (int, char, float...), porém o que o torna diferente das variáveis comuns é que, em vez de armazenar diretamente um valor, ele armazena um endereço de memória. Em outras palavras, quando queremos que um ponteiro faça referência a uma variável comum, utilizamos o operador &, que retorna o endereço de memória dessa variável.

Por exemplo, no trecho de código int *b = &a;, estamos declarando que b é um ponteiro para um inteiro (indicado pelo uso do * na declaração) e estamos atribuindo a ele o endereço de memória da variável a (obtido pelo &a). Dessa forma, dizemos que b aponta para a. Isso significa que qualquer alteração feita diretamente na variável a poderá ser acessada através do ponteiro b, e, da mesma forma, podemos alterar o valor de a utilizando o ponteiro.

Por outro lado, quando fazemos int b = a;, o que acontece é simplesmente uma cópia do valor de a para b. Assim, se posteriormente alterarmos o valor de a, o valor de b permanecerá inalterado, pois são variáveis independentes nesse contexto.

Exemplos:

[editar | editar código]
Atribuição comum
[editar | editar código]
#include <stdio.h>

int main () {
    int a = 10;
    int b = a;
    
    a = 20;
    
    printf("a = %d, b = %d\n", a, b); // a = 20 e b = 10
    
    return 0;
}

Nesse exemplo, o valor da variável a é alterado de 10 para 20. No entanto, como na atribuição de a para b não foi utilizado um ponteiro, essa alteração não impacta o valor de b. Portanto, durante a execução do printf, o valor de b permanecerá o mesmo.

Atribuição com ponteiro
[editar | editar código]
#include <stdio.h>

int main () {
    int a = 10;
    /* Declarando e atribuinto um ponteiro a variável 'a'*/
    int *b = &a;
    
    a = 20;
    
    printf("a = %d, *b = %d\n", a, *b); // a = 20. *b = 20
    
    return 0;
}

Agora, usando um ponteiro para armazenar o endereço da variável a, temos acesso direto a ela utilizando a referência que b armazenou.

Isso pode ser observado no printf, onde são exibidos:

  • O valor de a
  • O valor do conteúdo apontado por b (ou seja, *b), que é exatamente o valor atual de a.

Como b armazena o endereço de a, qualquer alteração feita em a também se reflete em *b.

Mostrando o endereço armazenado no ponteiro
[editar | editar código]

Se quisermos visualizar o conteúdo do ponteiro (isto é, o endereço de memória que ele guarda) usamos o especificador %p no printf.

#include <stdio.h>
int main () {
    int a = 10;
    int b = a;
    
    a = 20;
    
    printf("b = %p\n", b);
    printf("&a = %p\n", &a);
    
    return 0;
}

Como um ponteiro armazena um endereço de memória, essa instrução exibirá o endereço onde a variável a está localizada. E, como esperado, o valor de b (que é o endereço de a) será igual ao valor de &a. Saída possível:

b = 0x7ffe5e07af24
&a = 0x7ffe5e07af24

⚠️ O valor hexadecimal exibido pode variar a cada execução, pois depende de onde na memória a variável foi alocada no momento.

Aritmética de ponteiros

[editar | editar código]

Outro ponto importante é a aritmética de ponteiros, que pode ser útil para reduzir a verbosidade do código ou otimizar acessos. Operadores como ++, --, += 1 e -= 1 costumam aparecer em laços, chamadas de função ou em trechos onde é preciso adicionar ou subtrair uma unidade de uma variável. Esses mesmos operadores também funcionam com ponteiros, mas de modo diferente. Veja:

#include <stdio.h>

int main() {
    int arr[] = {1, 3, 5};
    int *ptArr;
    ptArr = arr; // equivalente a ptArr = &arr[0]
    ptArr++;
    printf("%d\n", *ptArr); // Saída: 3
    return 0;
}

Nesse exemplo, ptArr aponta para arr[0]. Quando escrevemos ptArr++, o ponteiro não incrementa apenas "1 byte" nem altera o valor de arr[0]; ele avança uma unidade do tipo ao qual aponta. Como ptArr é ponteiro para int e sizeof(int) costuma ser 4 bytes, o aumento desloca ptArr de arr[0] para arr[1]. Por isso, *ptArr passa a valer 3, e não 2. Esse comportamento de avançar ou retroceder "por sizeof(tipo)" vale para qualquer tipo de ponteiro em C.