Recadinho: Olá pessoal! Aqui estou eu continuando a nossa série de posts sobre programação para iniciantes para tratarmos de um assunto bastante delicado: os temidos Ponteiros.
Caso queira retornar ao indice inicial do curso, clique neste link.
Ponteiros são capazes tanto de te dar poder total sobre a sua aplicação, possibilitando manipulações de memória, casting, e uma porção de coisas, como também podem destruir sua aplicação facilmente (e em casos extremos causar erros de memória inclusive no sistema operacional).
Hoje em dia, algumas linguagens mais novas (como o Java), aboliram o uso de ponteiros, retirando do programador toda a responsabilidade de alocar memória, desalocar após o uso (o Java possui um Garbage Collector, ou coletor de lixo).
Mas, o que seriam estes tais ponteiros? Ponteiros nada mais são do que variáveis cujo o seu conteúdo é um endereço de memória. Como assim? Ele guarda, não um valor propriamente dito… é uma referência a algum lugar na memória que, provavelmente, contém algum dado. Mas que tipo de dado? Para o tipo de dados definido na declaração do ponteiro. O que isso significa? Significa que os ponteiros necessitam de um tipo para serem definidos. Em C, podemos definir um ponteiro sem tipo, genérico (utilizando o “tipo” void). Porém, o uso desse artifício deve ser bastante ponderado, pois pode acarretar em um comportamento equivocado ou mesmo em erros de memória.
Bom, primeiramente, como os ponteiros referem-se a memória, é um pouco estranho pensarmos nisso no algoritmo. Se vocês estão acompanhando a nossa série desde o começo, já devem estar conseguindo fazer alguns programas bastante funcionais, e devem ter percebido que nem sempre precisamos fazer o algoritmo todo do programa. Processamentos banais, estruturas de repetição simples, naturalmente surgem na cabeça do programador. E com o tempo, ao se familiarizarem-se com alguma linguagem de programação, irão pensar já no código. Voltando aos ponteiros, eles servirão para nos auxiliarem em algumas tarefas mais “cabeludas” onde precisaremos modificar alguma coisa “na unha”, ou o que a sua imaginação quiser. Alguns dos usos mais comuns de ponteiros são na passagem de parâmetros por referência e na alocação dinâmica (em tempo de execução) de memória.
Pra começar, então, podemos ver um exemplo de ponteiros em Pascal e em C. No seguinte trecho, vamos simplesmente criar um ponteiro, alocar memória para ele, atribuir um valor à variável e escrever na tela. Para fins didáticos, também vou colocar em pseudo-código (algoritmo), mas como eu disse acima, na minha opinião manipulação de ponteiros em forma algorítmica é algo meio estranho… mas enfim, vamos criar aqui uma notação própria. Lembre-se que o algoritmo deve ser claro para descrever uma ideia. Portanto, fique à vontade para adaptar sua própria notação.
Bom, a sintaxe da declaração é da mesma forma que uma variável comum, porém adicionando-se o caractere referente ao ponteiro, que em Pascal é representado pelo caracter ^ (acento circunflexo), que na declaração é colocado antes do tipo, e na utilização da variável, vai após o nome dela. Já em C, a sintaxe consiste no * (asterisco) precedendo o nome da variável. Perceba que em alguns pontos do código, também utilizamos a variável sem o caractere. Bom, antes de explicarmos o porquê, também temos de falar de outro caractere relativo aos ponteiros: o caractere de referência. Em Pascal, esse caractere é o @ (arroba) e em C é o & (ê comercial).
Com essa sopa de caracteres, vamos agora explicar como funcionam e pra que servem tais notações. Primeiramente, temos o ponteiro sem caractere nenhum. Neste caso (como na primeira linha de código propriamente dito do nosso exemplo), a variável sem nenhum dos dois caracteres, indica o endereço de memória. Ou seja, naquela linha (utilizando o new ou o malloc), estamos reservando uma área de memória apontada pela variável ponteiro. Então, fixando: sem qualquer caractere, o ponteiro representa um endereço de memória. Por exemplo 0x00AC1D. Em um segundo caso, temos o caractere de conteúdo: ^ em Pascal e * em C. Esse caractere indica o conteúdo de. Como assim? Em vez de indicar o local, você vai utilizar o valor que está armazenado no local indicado pelo ponteiro. Se não tiver nada (ou tiver lixo, como acontece sempre), ele simplesmente vai interpretar o que está lá de acordo com o tipo do ponteiro. Por isso é sempre bom anular ou inicializar o ponteiro, através das constantes nil em Pascal ou NULL em C. Já já veremos exemplos de código utilizando isso. Por fim, temos o caractere de referência @ em Pascal e & em C. Ao utilizar este caractere, obtemos o endereço onde tal variável armazenada, ou seja, o caractere indica o endereço de alguma variável. Utilizamos bastante ao atribuirmos, por exemplo, o endereço de uma variável estática a um ponteiro (veremos isso em alguns exemplos logo mais). Se utilizarmos tal caractere em um ponteiro, por exemplo, teremos o endereço onde a variável do tipo ponteiro foi criada, e não o que ela aponta. Meio confuso? Os exemplos a seguir devem ajudar a clarear um pouco as ideias. (nota mental: meu cérebro ainda não se acostumou a escrever ideia sem acento!)
x recebe 10. q e p ainda não foram inicializados
p agora aponta para x.
q recebe p. Agora q e p apontam para x.
o local apontado por q (ou seja, x), receberá o valor contido no local apontado por p (também x, ou seja, 10) mais 10. No final, x valerá 20, pois é o local apontado por q.
Sim, as imagens foram feitas no Paint. Mas enfim, percebam que as referências a uma variável por dois ponteiros pode ficar realmente confusa! Hahahaha. Mas espero que com essas ilustrações, o entendimento do código descrito acima tenha ficado melhor.
Veremos agora, como ficaria a alocação dinâmica de um registro. Percebam que ocorre da mesma maneira que uma variável comum, porém com a diferença que em C, os campos do registro são referenciados não mais com o . mas sim com o operador seta ->. Abaixo, um exemplo de uso de um registro alocado dinamicamente, em Pascal e em C.
A seguir, exemplos em Pascal e em C de funções que recebem 2 números e devolvem por referência os números somados e subtraídos.
Este artigo foi escrito por Rafael Toledo e publicado no site rafaeltoledo.net
Até a próxima! :D
Caso queira retornar ao indice inicial do curso, clique neste link.
Ponteiros são capazes tanto de te dar poder total sobre a sua aplicação, possibilitando manipulações de memória, casting, e uma porção de coisas, como também podem destruir sua aplicação facilmente (e em casos extremos causar erros de memória inclusive no sistema operacional).
Hoje em dia, algumas linguagens mais novas (como o Java), aboliram o uso de ponteiros, retirando do programador toda a responsabilidade de alocar memória, desalocar após o uso (o Java possui um Garbage Collector, ou coletor de lixo).
Mas, o que seriam estes tais ponteiros? Ponteiros nada mais são do que variáveis cujo o seu conteúdo é um endereço de memória. Como assim? Ele guarda, não um valor propriamente dito… é uma referência a algum lugar na memória que, provavelmente, contém algum dado. Mas que tipo de dado? Para o tipo de dados definido na declaração do ponteiro. O que isso significa? Significa que os ponteiros necessitam de um tipo para serem definidos. Em C, podemos definir um ponteiro sem tipo, genérico (utilizando o “tipo” void). Porém, o uso desse artifício deve ser bastante ponderado, pois pode acarretar em um comportamento equivocado ou mesmo em erros de memória.
Bom, primeiramente, como os ponteiros referem-se a memória, é um pouco estranho pensarmos nisso no algoritmo. Se vocês estão acompanhando a nossa série desde o começo, já devem estar conseguindo fazer alguns programas bastante funcionais, e devem ter percebido que nem sempre precisamos fazer o algoritmo todo do programa. Processamentos banais, estruturas de repetição simples, naturalmente surgem na cabeça do programador. E com o tempo, ao se familiarizarem-se com alguma linguagem de programação, irão pensar já no código. Voltando aos ponteiros, eles servirão para nos auxiliarem em algumas tarefas mais “cabeludas” onde precisaremos modificar alguma coisa “na unha”, ou o que a sua imaginação quiser. Alguns dos usos mais comuns de ponteiros são na passagem de parâmetros por referência e na alocação dinâmica (em tempo de execução) de memória.
Pra começar, então, podemos ver um exemplo de ponteiros em Pascal e em C. No seguinte trecho, vamos simplesmente criar um ponteiro, alocar memória para ele, atribuir um valor à variável e escrever na tela. Para fins didáticos, também vou colocar em pseudo-código (algoritmo), mas como eu disse acima, na minha opinião manipulação de ponteiros em forma algorítmica é algo meio estranho… mas enfim, vamos criar aqui uma notação própria. Lembre-se que o algoritmo deve ser claro para descrever uma ideia. Portanto, fique à vontade para adaptar sua própria notação.
- Código:
Algoritmo Alocacao_Dinamica
[Declaração de Variáveis]
ponteiro : ^inteiro
[Processamento]
alocar(ponteiro)
ponteiro^ ← 10
escreva("Variável alocada dinamicamente com o valor ", ponteiro^)
desalocar(ponteiro)
[Fim]
- Código:
program alocacao_dinamica;
uses crt;
var
ponteiro: ^integer;
begin
new(ponteiro);
ponteiro^ := 10;
writeln('Variável alocada dinamicamente com o valor ', ponteiro^);
dispose(ponteiro);
end.
- Código:
// Alocação dinâmica de memória
#include <stdio.h>
#include <stdlib.h> // Necessário para malloc() e free()
int main(void)
{
int *ponteiro;
ponteiro = malloc(sizeof(int));
*ponteiro = 10;
printf("Variável alocada dinamicamente com o valor %d", *ponteiro);
free(ponteiro);
return 0;
}
Bom, a sintaxe da declaração é da mesma forma que uma variável comum, porém adicionando-se o caractere referente ao ponteiro, que em Pascal é representado pelo caracter ^ (acento circunflexo), que na declaração é colocado antes do tipo, e na utilização da variável, vai após o nome dela. Já em C, a sintaxe consiste no * (asterisco) precedendo o nome da variável. Perceba que em alguns pontos do código, também utilizamos a variável sem o caractere. Bom, antes de explicarmos o porquê, também temos de falar de outro caractere relativo aos ponteiros: o caractere de referência. Em Pascal, esse caractere é o @ (arroba) e em C é o & (ê comercial).
Com essa sopa de caracteres, vamos agora explicar como funcionam e pra que servem tais notações. Primeiramente, temos o ponteiro sem caractere nenhum. Neste caso (como na primeira linha de código propriamente dito do nosso exemplo), a variável sem nenhum dos dois caracteres, indica o endereço de memória. Ou seja, naquela linha (utilizando o new ou o malloc), estamos reservando uma área de memória apontada pela variável ponteiro. Então, fixando: sem qualquer caractere, o ponteiro representa um endereço de memória. Por exemplo 0x00AC1D. Em um segundo caso, temos o caractere de conteúdo: ^ em Pascal e * em C. Esse caractere indica o conteúdo de. Como assim? Em vez de indicar o local, você vai utilizar o valor que está armazenado no local indicado pelo ponteiro. Se não tiver nada (ou tiver lixo, como acontece sempre), ele simplesmente vai interpretar o que está lá de acordo com o tipo do ponteiro. Por isso é sempre bom anular ou inicializar o ponteiro, através das constantes nil em Pascal ou NULL em C. Já já veremos exemplos de código utilizando isso. Por fim, temos o caractere de referência @ em Pascal e & em C. Ao utilizar este caractere, obtemos o endereço onde tal variável armazenada, ou seja, o caractere indica o endereço de alguma variável. Utilizamos bastante ao atribuirmos, por exemplo, o endereço de uma variável estática a um ponteiro (veremos isso em alguns exemplos logo mais). Se utilizarmos tal caractere em um ponteiro, por exemplo, teremos o endereço onde a variável do tipo ponteiro foi criada, e não o que ela aponta. Meio confuso? Os exemplos a seguir devem ajudar a clarear um pouco as ideias. (nota mental: meu cérebro ainda não se acostumou a escrever ideia sem acento!)
- Código:
var
x : integer;
p, q : ^integer;
begin
x := 10;
p := @x;
q := p;
q^ := p^ + 10;
writeln('Resultado: ', p^);
end.
- Código:
int main(void)
{
int x, *p, *q;
x = 10;
p = &x;
q = p;
*q = *p + 10;
printf("Resultado: %d", *p);
return 0;
}
x recebe 10. q e p ainda não foram inicializados
p agora aponta para x.
q recebe p. Agora q e p apontam para x.
o local apontado por q (ou seja, x), receberá o valor contido no local apontado por p (também x, ou seja, 10) mais 10. No final, x valerá 20, pois é o local apontado por q.
Sim, as imagens foram feitas no Paint. Mas enfim, percebam que as referências a uma variável por dois ponteiros pode ficar realmente confusa! Hahahaha. Mas espero que com essas ilustrações, o entendimento do código descrito acima tenha ficado melhor.
Veremos agora, como ficaria a alocação dinâmica de um registro. Percebam que ocorre da mesma maneira que uma variável comum, porém com a diferença que em C, os campos do registro são referenciados não mais com o . mas sim com o operador seta ->. Abaixo, um exemplo de uso de um registro alocado dinamicamente, em Pascal e em C.
- Código:
type
reg_aluno = record
nome : string[30];
idade : integer;
end;
var
aluno : ^reg_aluno;
begin
new(aluno);
write('Nome do aluno: ');
readln(aluno^.nome);
write('Idade: ');
readln(aluno^.idade);
writeln('Dados lidos.');
writeln('Nome: ', aluno^.nome, ' - Idade: ', aluno^.idade);
dispose(aluno);
end.
- Código:
typedef struct {
char nome[30];
int idade;
} reg_aluno;
int main(void)
{
reg_aluno *aluno;
aluno = malloc(sizeof(reg_aluno));
printf("Nome do aluno: ");
scanf("%s", aluno->nome);
printf("Idade: ");
scanf("%d", &aluno->idade);
printf("Dados lidos.\n");
printf("Nome: %s - Idade: %d\n", aluno->nome, aluno->idade);
return 0;
}
- Código:
aluno = (reg_aluno*) malloc(sizeof(reg_aluno));
- Código:
var
ponteiro: ^integer;
begin
ponteiro := nil;
- Código:
int main(void)
{
int *ponteiro = NULL; // sim, o NULL é em caixa alta!
A seguir, exemplos em Pascal e em C de funções que recebem 2 números e devolvem por referência os números somados e subtraídos.
- Código:
var
num1, num2, soma, subtracao : integer;
procedure aplicarOperacoes(n1, n2: integer; var soma, subt: integer);
begin
soma := n1 + n2;
subt := n1 - n2;
end;
begin
num1 := 5;
num2 := 3;
aplicarOperacoes(num1, num2, soma, subtracao);
writeln('A soma de ', num1, ' e ', num2, ' é ', soma, ' e a subtraçao é ', subtracao);
end.
- Código:
void aplicarOperacoes(int n1, int n2, int *soma, int *subt)
{
*soma = n1 + n2;
*subt = n1 - n2;
}
int main(void)
{
int num1, num2, soma, subtracao;
num1 = 5;
num2 = 3;
aplicarOperacoes(num1, num2, &soma, &subtracao);
printf("A soma de %d e %d é %d e a subtração é %d.\n", num1, num2, soma, subtracao);
return 0;
}
Este artigo foi escrito por Rafael Toledo e publicado no site rafaeltoledo.net
Até a próxima! :D