Ponteiros

Share

As variáveis utilizadas em um programa são posições de memória de tamanho compatível com seu tipo e referenciadas por um nome identificador. Assim, se definimos uma variável i do tipo int, pedimos ao computador para cercar uma certa quantidade de memória com espaço suficiente para guardarmos dados do tipo inteiro. A partir daí o identificador “i” é o nosso atalho para esta posição de memória que podemos usar para guarda um dado do tipo inteiro.

Um ponteiro é uma variável que guarda a posição de memória de outra variável ao invés de guardar um dado específico.

Para declararmos uma variável como um ponteiro, precisamos definir o tipo base para qual ela vai apontar. A seguir mostramos exemplos de ponteiros para variáveis inteiras, reais e caracteres:

int *pN;
float *pR;
char *pC;

Com as variáveis pI, pR e pL exemplo nós ainda não podemos guardar dados pois elas ainda não estão apontando para posições de memória! Vamos então criar três posições de memória e fazer com que nossos ponteiros apontem para elas usando o operador &:

// Variáveis ponteiros
int *pN;
float *pR;
char *pL;
 
// Variáveis reais
int I = 1;
int F = 2.3;
char C = 'A';
 
// Apontar para as variáveis
pN = &I;
pR = &F;
pL = &C;

O operador unário & retorna a posição de memória da variável à sua direita. Como os ponteiros guardam posição de memória, dizemos que a partir de agora as variáveis estão apontando para as variáveis. Se quisermos que uma variável não aponte para lugar nenhum, atribuímos ao ponteiro o valor NULL e dizemos que ela aponta para o “terra” ou nil.

int *pNulo;
p = NULL;

Nunca se esqueça que os ponteiros guardam apenas posições de memória e não valores! Para acessarmos os valores das posições de memória apontadas precisamo usar o operador ‘*’:

 //Define e inicia uma variável inteira
int I = 1;
//Define um ponteiro apontando para esta variável
int *pN = &I;
//Imprime 1
printf("%d\n", I);
printf("%d\n", *pN);
//Incrementa I via ponteiros
*pN = *pN + 1;

Podemos então ler os diversos operadores como:

I
Variável I
&I
Posição/Endereço de memória de I
int *pN
Definição de um ponteiro para inteiros pN
&pN
Posição/Endereço de memória de pN
pN
Posição/Endereço de memória que pN aponta
*pN
Valor do dado/valor contido na posição de memória apontada por pN

O uso de ponteiros exige uma maior atenção com o uso dos operadores ‘*’ e ‘&’ e esquecê-los pode acarretar em uma mudança indesejada da posição apontada.

Exemplo 1

Façamos um exemplo que explora as operações básicas apresentadas:

/* Construa um programa que:
 * - Defina uma variável A;
 * - Defina um ponteiro P apontando para A;
 * - Imprima a posição de memória de A;
 * - Atribua 10 a A via P
 * - Imprima o valor apontado por P
 * - Incremente A via ponteiro P
 * - Compare se os valores de A e P são iguais
 */
#include <stdio.h>
int main(){
	int A;
	int *P;
	P = &A;
	printf("Posição de memória de A: %p\n", &A);
	printf("P aponta para a posição: %p\n", P);
	*P = 10;
	printf("Valor guardado em P: %d\n", *P);
	*P = *P + 1;
	printf("Valor guardado em P: %d\n", *P);
	printf("A == P: %d == %p retorna: %d\n", A, P, A == P);
	printf("A == *P: %d == %d retorna: %d\n", A, *P, A == *P);
}

Exemplo 2

Um uso direto de ponteiro se dá quando necessitamos passar parâmetros para funções e procedimento por referência. Uma função normalmente cria uma cópia dos parâmetro passados ao ser chamada. Se usarmos como parâmetro um ponteiro, podemos alterar as variáveis apontadas de dentro da função sem o uso de variáveis globais.

/*
 * Construa um programa que possua dois procedimentos para realizar a 
 * troca do conteúdo de duas variáveis passadas como parâmetro.
 * No primeiro procedimento use passagem de parâmetros por cópia normalmente.
 * Utilizando ponteiros, realize a passagem de parâmetros por referência no 
 * segundo procedimento. 
 */
#include <stdio.h>
void trocaPorCopia(int a, int b){
	int c;
	c = a;
	a = b;
	b = c;
}
void trocaPorRef(int *a, int *b){
	int c;
	c = *a;
	*a = *b;
	*b = c;
}
int main(){
	int x = 1;
	int y = 2;
	printf("x: %d y: %d\n", x, y);
	trocaPorCopia(x, y); // Não tem efeito nas variáveis fora do procedimento
	printf("Apos trocaPorCopia x: %d y: %d\n", x, y);
	trocaPorRef(&x, &y); // Tem efeito nas variáveis fora do procedimento
	printf("Apos trocaPorRef x: %d y: %d\n", x, y);
}

Alocação Dinâmica

Definir variáveis no código é tido como alocação de memória estática, pois após uma definição comum, a memória é reservada no início do processo e será liberada após o término do processo. Entretanto, há a possibilidade de criarmos as variáveis em tempo de execução, de forma dinâmica usando ponteiros.

A função malloc() da biblioteca stdlib.h se encarrega de pedir ao SO uma certa quantidade de bytes para ser utilizado em nosso programa. Esta função retorna um ponteiro de memória apontando para a nova posição ou o valor nulo quando não é mais memória disponível.

#include <stdio.h>
#include <stdlib.h>
int main(){
	int *P;
	P = malloc(sizeof(int));
	*P = 10;
	printf("Valor guardado na posição %p:  %d\n", P, *P);
}

Como o tamanho dos tipos primitivos pode variar de acordo com o sistema operacional e arquitetura do computador, usamos o operador sizeof() para retornar o tamanho exato em bytes de um tipo passado como parâmetro.

Da mesma forma que pedimos ao SO para reservar uma posição de memória, também devemos solicitar a sua liberação quando não estivermos mais utilizando. Para tanto, vamos usar a função free(), veja o exemplo abaixo:

#include <stdio.h>
#include <stdlib.h>
int main(){
	int *P;
	P = malloc(sizeof(int)); // reserva memória
	*P = 10;
	free(P); // libera memória
}

Deve-se ter um cuidado especial ao se utilizar alocação dinâmica logo após a definição de um ponteiro e logo após a liberação da memória. Uma variável ponteiro é iniciada com um valor “lixo”, da mesma forma que uma variável primitiva e este valor pode ser usado erroneamente como uma posição de memória (estes casos são conhecidos “ponteiros selvagens”). O segundo caso ocorre logo após a liberação através do free(). O comando free() libera a posição de memória mas não altera a posição de memória apontada por ela (tornando o ponteiro “selvagem”). Em ambos os casos podemos atribuir o valor NULL ao ponteiro para garantirmos que este não vá apontar para algum lugar indesejado.

#include <stdio.h>
#include <stdlib.h>
int main(){
	int *P; //P selvagem
	printf("Valor guardado na posição %p:  %d\n", P, *P);
	P = malloc(sizeof(int)); //P aponta para o novo endereço
	*P = 10;
	printf("Valor guardado na posição %p:  %d\n", P, *P);
	free(P); // Libera-se a posição mas o P volta a ser selvagem
	printf("Valor guardado na posição %p:  %d\n", P, *P);
	P = NULL; // P torna-se um apontador para nil
	printf("Sem valor guardado na posição %p:  \n", P );
}

Ponteiros podem ser usados para acesso a elementos de um vetor já que os elementos destes são guardados seqüencialmente na memória. O exemplo abaixo ilustra as duas formas de acesso:

#include <stdio.h>
int main(){
	int mc[4] = {'A','B','C','D'};
	int i;
	for(i = 0; i<4; i++){
		printf("&mc[%d]:%p\tmc[%d]:%d\n",i,&mc[i],i, mc[i]);
 
	}
	int *pc = &mc[0];
	for(i = 0; i<4; i++){
		printf("(pc+%d):%p\t*(pc+%d):%d\n",i,pc+i,i,*(pc+i));
	}
}

Da mesma forma, podemos alocar uma quantidade maior de memória usando um fator multiplicador inteiro na função malloc() e com isto acessar estas posições tanto por aritmética de ponteiros ou por índices de um vetor. O exemplo abaixo reserva uma posição de memória, atribui valores e em seguida apresenta os valores de duas formas diferentes.

#include <stdio.h>
#include <stdlib.h>
 
char main(){
 
	char *p = NULL;
	p = malloc(3*sizeof(char));
	if(p == NULL){
		printf("Sem memória!\n");
		exit(1);
	}
	int i = 0;
	for(i = 0; i<3; i++){
		*(p+i) = i+'A';
	}
 
	for(i = 0; i<3; i++){
		printf("*(p+%d) @%p %c\n", i, &p+i,*(p+i));
	}
	for(i = 0; i<3; i++){
		printf("p[%d] @%p %c\n", i, &p[i],p[i]);
	}
 
	free(p);
	p = NULL;
}

Ponteiros para estruturas exigem apenas um maior cuidado com a ordem dos operadores. Como uma estrutura é um tipo, a criação de ponteiros para estruturas é igual para tipos primitivos:

#include <stdio.h>
struct SCasa {
	int quartos;
	float preco;
	char classe;
};
typedef struct SCasa Casa;
 
int main(){
	//Cria uma variável do tipo Casa
	Casa c;
	c.quartos = 3;
	c.preco = 150456.23;
	c.classe = 'B';
 
	//Cria um apontador de Casa apontando para c
	Casa *p = &c;
 
	//Imprime os dados via ponteiro
	printf("&c: %p sizeof(c): %d\n",&c,sizeof(c));
	printf("&p: %p p: %p\n",&p, p);
	printf("\t(*p).quartos: %d\n",(*p).quartos);
	printf("\t(*p).preco: %.2f\n",(*p).preco);
	printf("\t(*p).classe: %c\n",(*p).classe);
}

Podemos também utilizar a alocação dinâmica da mesma forma que em tipos primitivos. Existe um operador especial que serve atalho para se obter valores dos campos de um ponteiro para estruturas, o chamado “operador seta” feito por um sinal de menos seguido de um operador maior que: ->. No próximo exemplo utilizamos a alocação dinâmica e o operador seta para realizar o acesso aos campos.

#include <stdio.h>
#include <stdlib.h>
struct SCasa {
	int quartos;
	float preco;
	char classe;
};
typedef struct SCasa Casa;
 
int main(){
	//Cria um ponteiro para o tipo Casa
	Casa *p = NULL;
 
	//Aloca a memória
	p = malloc(sizeof(Casa));
	if(p == NULL){
		printf("Sem memória!\n");
		exit(1);
	}
	//Atribui valores aos campos apontados
	p->quartos = 3;
	p->preco = 150456.23;
	p->classe = 'B';
 
	//Imprime os valores dos campos apontados
	printf("&p: %p sizeof(Casa): %d\n",&p,sizeof(Casa));
	printf("\tp->quartos: %d\n",p->quartos);
	printf("\tp->preco: %.2f\n",p->preco);
	printf("\tp->classe: %c\n",p->classe);
 
	//Libera a memória
	free(p);
	p = NULL;
}
Share

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

*