CCT-UFCA/Ciência da Computação/Introdução à Programação/Ponteiro
Ponteiro
[editar | editar código]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.000000Se observarmos os endereços das variáveis no exemplo fornecido, podemos perceber um detalhe importante:
&d = 0x7ffe51dc9700
&c = 0x7ffe51dc9704
&b = 0x7ffe51dc9708
&a = 0x7ffe51dc970cCada 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.