Staffer Group BR™

Fórum interativo totalmente gratuito!

/* Widget *//* Resposta rápida bloqueada */
Cadastre-se
→ Crie a sua conta

 VisualizaçõesPermissão deste fórum:
Você não pode responder aos tópicos neste fórum


CurtirDiretório deste fórum:
Fóruns » Principal »  » Tutoriais » Programação Externa

#1
 D'Leandro™

avatar
Fundador
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.
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;
}
Vamos conhecer a sintaxe e o funcionamento das funções que utilizamos. Primeiramente, declaramos uma variável ponteiro que, como foi dito acima, é declarada de um tipo correspondente. Isso ocorre porque os bits na memória são lidos de maneira diferente dependendo do tipo. Em C, podemos através de cast ponteiros, podemos setar um ponteiro de float para ser lido como inteiro… umas doideras assim (que eu, particularmente, nunca utilizei). Fazendo algumas brincadeiras com isso, podemos ver como um descuido desses pode causar reações adversas no programa.

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;
}
Acima, temos dois exemplos simples demonstrando tudo o que eu falei. Temos x como variável estática e p e q como ponteiros. Percebam que em C, ao contrário de Pascal, é possível declarar ponteiros e variáveis estáticas na mesma linha. Iniciando nosso programa, atribuímos o valor 10 para a variável x, da maneira convencional mesmo. Em seguida, fazemos com que p aponte para x, passando para p (sem o ^ ou o *) o endereço de x, através do operador @ (Pascal) e & (C). Em seguida, q recebe p, ou seja, agora os dois ponteiros são iguais (ambos apontam para x). Depois, o endereço de memória apontado por q (que é o mesmo que x), recebe o que tem no endereço apontado por p (também x, ou seja, 10), mais 10. Ou seja, agora, x vale 20, e q e p continuam apontando para x. Para ilustrar a situação, elaborei o desenho abaixo que pode ajudar a compreender o que aconteceu.


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;
}
A única ressalva no código acima, é que alguns compiladores podem “chiar” na utilização do malloc(), atribuido à nossa struct. Caso isso aconteça, faça a conversão de ponteiros (cast). Dessa forma, a linha ficaria assim:
Código:
aluno = (reg_aluno*) malloc(sizeof(reg_aluno));
Um outro ponto que devemos frisar aqui, é a maneira de “zerarmos” um ponteiro. Ao fazermos isso, evitamos deixar o ponteiro com “lixo”, o que pode ocorrer dele acessar alguma área de memória não permitida, e consequentemente ter o programa encerrado de maneira forçada pelo sistema operacional. Dessa forma, ao declararmos um ponteiro, recomenda-se inicializá-lo assim:
Código:
var
   ponteiro: ^integer;
 
begin
   ponteiro := nil;
Código:
int main(void)
{
   int *ponteiro = NULL; // sim, o NULL é em caixa alta!
Para encerrarmos o post, mostrarei um exemplo de como fazer funções que recebem parâmetros como refência. Não sabe o que é um parâmetro passado como referência? Bom, vamos a uma breve explicação: quando chamamos uma função, os parâmetros são chamados por cópia ou por referência. Os parâmetros por cópia, como foram os já ensinados aqui anteriormente, utilizam uma cópia do valor passado por argumento em seus cálculos. Ou seja, se você passa uma variável y para uma função, seu valor não será alterado, mesmo que y sofra alterações dentro da função. Já nos parâmetros por referência, a variável passada como argumento sofre alterações caso seja alterada dentro da função. Isso é útil quando queremos que uma função retorne mais de um valor (já que com o retorno convencional podemos devolver somente um valor).

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;
}
Apesar do post sobre ponteiros, perceba que em Pascal não utilizamos ponteiros pra fazermos os parâmetros serem passados como referência. Para isso, basta adicionar a palavra var antes das variáveis que serão passadas como referência. Já em C, declaramos os parâmetros de referência como ponteiros, e ao chamarmos a função, utilizamos o operador & para passarmos a referência das variáveis de recepção da função. Bom, amigos, finalizamos então nosso post sobre os temidos ponteiros. Eles são um assunto delicado na programação, por vezes nos dando imenso controle sobre a aplicação, e por outras vezes nos tirando todo o controle do programa (e a paciência também). Mas é somente com a prática que vocês conseguirão domá-los de uma vez (até hoje apanho deles de vez em quando).

Este artigo foi escrito por Rafael Toledo e publicado no site rafaeltoledo.net
Até a próxima! :D
Ver perfil do usuário