Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Matrizes e Ponteiros – Parte 2 – Alocação dinâmica de memória

Olá todos e desculpem a demora! Espero que todos tenham pulado o Carnaval no “Unidos da Programação em C”!🙂
Bem, vamos continuar de onde paramos, então vamos dar prosseguimento ao “curso”.

Quando, na primeira parte do assunto matrizes e ponteiros, falamos sobre os ponteiros, nós dissemos que “existem situações onde precisamos utilizar uma posição de memória que não conhecemos previamente. Na realidade, isso é mais comum do que se imagina: não fosse assim, todo programa deveria ser artificialmente limitado em suas capacidades baseado em números arbitrários de informação a ser processada. Por exemplo: um programa teria que criar previamente 500 variáveis de notas para processar o boletim escolar de uma classe de 20 alunos, sendo inútil para uma classe de 501 alunos. Isso também aumentaria o custo de desenvolvimento e manutenção de um programa de computador, além de utilizar os recursos de um computador de maneira pouco eficiente.” Isso acontece porque pelas regras do padrão ANSI C Original (seguida pela maior parte dos compiladores), o compilador não pode gerar código que utilize-se de matrizes de tamanho varíavel (em uma versão atualizada do padrão, C-99, os compiladores já passaram a dar suporte a esse recurso, porém nos focaremos ao C padrão ANSI.). Por exemplo, em C padrão ANSI, o código abaixo:
printf (“Quantos elementos precisamos?”);
scanf (“%d”,&nElementos);
int elementos[nElementos];

É considerado errado e gera um erro similar (PS: se você colocar esse tipo de código em um compilador com suporte ao C-99, em geral ele irá compilar, mas irá dar alertas. Será necessário fazer o compilador enxergar o código como C-99, não ANSI C Original. verifique o manual do compilador para maiores informações).
Para “escaparmos” a esse tipo de problema, a solução é fugir das matrizes e recorrermos aos ponteiros. Mas como?
O C Original prevê funções de alocação de memória dinâmica. Com o uso dessas funções, podemos obter, em tempo de execução, qualquer quantidade de memória que precisarmos (claro, imaginando que ela esteja disponível), normalmente de um espaço conhecido como heap e gerenciado pelo sistema operacional. Com isso, não precisamos artificalmente definirmos o uso de memória em um programa C (PS: várias outras linguagens, como Pascal, possuem também mecanismos de alocação dinâmica de memória), podendo aproveitar os recursos do sistema com maior eficiência.
Bem, chega de papo! Vamos ao que interessa.

O código

Vamos refazer um programa similar a algo que fizemos anteriormente, que é o programa da seqüência de Fibonacci (fãs de Código Da Vinci, regozijem-se). Porque disse similar? Pois a seqüência de Fibonacci é calculada de maneira muitíssimo parecida com a do Fatorial, que vimos quando falamos de recursividade. Para não recorrermos à recursão e mostrarmos a seqüência de Fibonacci resultante, utilizaremos alocação dinâmica de memória para reservamos o espaço adequado e aritmética de ponteiros para realizar os cálculos. Abaixo está o código:

#include <stdio.h>
#include <sdtlib.h>

int main(void)
{
  unsigned long int i=0,numeroFib=0,*sequenciaFib,*operFib;

  do
    {
      printf(“Indique o tamanho da sequencia Fibonacci (a partir do 3): “);
      scanf(“%d”,&numeroFib);
    }while (numeroFib<3);

  sequenciaFib=(unsigned long int *)malloc(sizeof(unsigned long int)*numeroFib);
  if(!sequenciaFib)
    {
      printf(“Sem memoria!\n”);
      return(1);
    }


  operFib=sequenciaFib;

  *operFib=1;
  ++operFib;
  *operFib=1;
  ++operFib;

  for (i=0;i<<numeroFib-2;i++)
    {
      *operFib=*(operFib-1)+*(operFib-2);
      operFib++;
    }

  operFib=sequenciaFib;

  for (i=0;<numeroFib;i++)
    {
      printf(“%do. numero Fibonacci e %u e esta armazenado no endereco %p\n”,i+1,*operFib,operFib);
      ++operFib;
    }

  free(sequenciaFib);

  return(0);
}

Bem, não precisamos comentar o início, à exceção que precisamos incluir a stdlib.h (STanDard LIBrary, se você não se lembra bem), pois ela é quem fornece a função malloc (Memory ALLOCation) que precisaremos. Logo falaremos sobre ela. Em seguida declaramos quatro variáveis unsigned long int… Na realidade, duas unsigned long int e dois ponteiros unsigned long int (unsigned long int *). Fizemos a opção de usar o unsigned long int pois podemos com isso executar um número muito alto de interações e, ao mesmo tempo, não precisamos nos preocupar com valores negativos de Fibonacci (que não existem). Para não termos problemas, fazemos com que o sistema só aceite um valor mínimo de 3 (ou seja, para mostrar até o terceiro número de Fibonacci).

Alocação dinâmica de memória – os comandos malloc e free:

Continuando o programa, encontramos o comando:

  sequenciaFib=(unsigned long int *)malloc(sizeof(unsigned long int)*numeroFib);

Essa linha é que faz todo o truque de alocação dinâmica de memória com o uso do comando malloc (Memory ALLOCation). malloc aloca uma determinada quantidade de bytes para um ponteiro do tipo void (void *). É importante lembrar disso pois podemos provocar sérios erros senão lembrarmos constantemente disso. Para que possamos usar corretamente malloc, devemos obter o número de bytes necessários para armazenarmos as informações necessárias. Nesse caso, utilizamos o operador sizeof para obter o tamanho de bytes de um unsigned long int (lembrando que esse valor pode mudar conforme a plataforma) e multiplicamos pelo número de elementos que precisaremos (no caso, o tamanho da seqüência de Fibonacci que desejamos desenvolver). Depois disso, devemos dar um typecast para indicar ao sistema que vamos transformar o ponteiro void retornado por malloc em um ponteiro unsigned long int (unsigned long int *). Desse modo, temos como saída um ponteiro do tipo unsigned long int pronto para nosso uso.
Antes de usarmos o ponteiro, é interessante de, por via das dúvidas, ver se realmente temos a memória alocada. Isso pode não parecer válido agora, mas qualquer pequena falha ao lidar com alocação dinâmica de memória pode comprometer o uso do sistema. O comando malloc nos oferece um bom mecanismo para isso: ele retorna null quando a alocação de memória falhou. Portanto, podemos testar isso com o seguinte código:

  if(!sequenciaFib)
    {

      printf(“Sem memoria!\n”);

      return(1);

    }

Provocando uma saída do sistema caso a memória que precisemos não seja alocada. Isso nos garante que não vamos operar sobrescrevendo memória apontada por um “ponteiro desgarrado” (ponteiro não-inicializado corretamente).
Agora, e essa é uma regra importante: sempre que você alocar memória, desaloquea-a assim que não for mais necessária. Isso vai garantir que não só seu programa, mas qualquer programa no computador onde o seu estiver executando, irá encontrar memória sempre que precisar. Caso não desaloque memória, você pode entrar em uma situação conhecida como “memory leakage” (Vazamento de memória), onde o sistema irá dar falha após várias alocações de memória seguirem-se sem a devida desalocação (o C não possui os sistemas de coletor de lixo – Garbage Collector – existente em outras linguagens devido à sua filosofia de trabalhar com o mínimo possível de complexidade). Para desalocar-se memória é usado o comando free. No nosso caso, o último comando do programa é:
  free(sequenciaFib);

Que avisa ao sistema operacional que desejamos desalocar o espaço de memória que alocamos para sequenciaFib, devolvendo esses recursos ao sistema operacional.

Aritmética de ponteiros:

Bem, agora vamos continuar analisando o código, vendo um tópico que causa muiita confusão e portanto deve ser clareado totalmente, que é a aritmética de ponteiros.
Nós já vimos na aula anterior que existem formas de fazer um ponteiro “correr” pela memória vendo seus conteúdos, somando e subtraindo elementos do ponteiro em questão. Para isso, usa-se a aritmética de ponteiros, que nada mais é que usarmos os operandos matemáticos mais simples (+, , ++ e ) para que eles alterem a informação de que endereço deve ser apontado por um ponteiro.
Vamos analisar então como o programa irá usar a aritmética de ponteiros:

  operFib=sequenciaFib;

  *operFib=1;
  ++operFib;
  *operFib=1;
  ++operFib;

  for (i=0;i<<numeroFib-2;i++)
    {
      *operFib=*(operFib-1)+*(operFib-2);
      operFib++;
    }

O primeiro comando é simples: salvamos em operFib o endereço apontado por sequeciaFib. Perceba que ambos estão sem o operador de derreferenciamento (sim, o nome é confuso) *. Nesse caso, é o que queremos: aqui estamos atribundo o endereço apontado por sequenciaFib como o endereço a ser apontador operFib.
A pergunta que você deve estar se fazendo é: “por que usar dois ponteiros?
Na realidade aqui é importante fazer uma consideração MUITO SÉRIA: o C NÃO POSSUI “LEMBRANÇA” do endereço apontado por um ponteiro. Ele trabalha sempre no “instante”, por assim dizer. Se você modificar o endereço de sequenciaFib, ele será usado em tudo o mais por esse novo valor, INCLUSIVE NA DESALOCAÇÃO. Isso pode gerar problemas sérios, pois o sistema operacional, no momento de alocar memória, também armazena a informação de quanta memória foi alocada, mas não a posição inicial-final. Se você “mover o ponteiro” e mandar desalocar a memória, você pode muito bem tentar desalocar memória de programas que não o seu, incluindo aí programas do sistema operacional. Desse modo, você pode comprometer o funcionamento do sistema como um todo ao fazer esse tipo de desalocação. Por via das dúvidas, é muito mais interessante fazer todas as operações por um segundo ponteiro, esse sim “livre” para ser usado à vontade e modificado como necessário. Você pode, obviamente, fazer o “movimento” do ponteiro usando sequenciaFib, se você souber como “recuar” o ponteiro de volta para o início de tudo, mas o nível de complexidade que isso vai gerar, em especial se você precisar modificar a memória alocada ou amarrá-la a outras coisas por meio de ponteiros de ponteiros (veremos esse tópico no futuro) vai mostrar que usar um ponteiro “âncora” representa um gasto de memória muito pequeno e o aumento de complexidade de código não compensará.
Bem, voltando ao nosso código: agora colocamos operFib como ponteiro para traabalharmos. Vamos ver o que ele irá fazer:
  *operFib=1;
  ++operFib;
  *operFib=1;
  ++operFib;

A primeira coisa que fazemos é: pegar os dois primeiros espaços de memória alocados para sequenciaFib (e que vamos alterar usando o ponteiro operFib) e definir eles como 1. Eles são os primeiros dois números de Fibonacci, segundo a definição do mesmo. O que fazemos é repetido então pode-se pensar bem. A primeira linha: *operFib=1, define que queremos armazenar na posição de memória apontada por operFib o valor 1. Aqui estamos tratando a memória apontada, portanto a derreferenciamos usando o operador *. Em seguida, a segunda linha, ++operFib, indica que queremos passar para o próximo elemento de operFib.
AQUI MORA DRAGÕES!!!
Pode parecer que simplesmente somamos 1 ao valor de operFib, como mostramos na outra aula sobre ponteiros. Mas já falamos lá que nesse caso, o C soma o equivalente ao número de bytes ocupados por 1 elemento do tipo apontado pelo ponteiro (no caso, unsigned long int). Normalmente, fazendo as coisas da maneira correta, nem precisamos nos preocupar com isso, mas é muito importante e explica, por exemplo, o porquê da exigência do typecast do ponteiro resultante do malloc de void * para o tipo necessário (além da óbvia mensagem de warning ou erro ao colocar-se um ponteiro void * em um ponteiro unsigned long int *). Com o devido typecast, o compilador, ao gerar o código, faz com que todas as abstrações sejam devidamente configuradas no nível do código de máquina. Sem isso, seria muito fácil o programador provocar erros e bugs devido ao apontamento incorreto de posições.
Dito isso, não há muito mais o que falar nesse momento.
O laço for que se segue, porém, é muito mais interessante:

  for (i=0;i<<numeroFib-2;i++)
    {
      *operFib=*(operFib-1)+*(operFib-2);
      operFib++;
    }

Vamos analisar ele: a primeira coisa é que a condição de saída é o número de elementos de nossa seqüência de Fibonacci-2. Fazemos isso pois os dois primeiros valores já foram estipulados.
Em seguida vem o comando que é mais interessante nesse laço:

      *operFib=*(operFib-1)+*(operFib-2);

Aqui, usando a aritmética de ponteiros, conseguimos declarar normalmente a função para o cálculo de um número de Fibonacci: o valor da posição apontada por operFib será a soma do valor na posição apontada pelo elemento anterior ao atual (*(operFib-1)) e pelo valor anterior a este (*(operFib-2)).
Como ler uma dessas seqüência. A primeira coisa é perceber que temos uma aritmética de ponteiro aí, que é pedir para retornar o endereço corresponte a operFib-1. Nesse caso, lembre-se de que é subtraído o número de bytes do tamanho do tipo indicado pelo ponteiro operFib. Por exemplo, imagine que operFib aponte no momento o endereço 1000 (no caso, para facilitar a compreensão, trate como decimal – normalmente os endereços são apresentados pelo C em Hexadecimal) e que o tamanho de um unsigned long int seja 64 bits (ou seja, 8 bytes). Ao pedirmos para obtermos o elemento anterior (operFib-1), ele irá nos apresentar o valor que está na posição de memória 1000-8, ou seja, na posição 992. Por sua vez, o elemento anterior a esse (operFib-2) estará na posição de memória 1000-16, ou seja, 984.
Isso não afeta operFib?“, você deve estar se perguntando?
Na verdade não. Ao usarmos os operadores aritméticos na aritmética de ponteiros, seus comportamentos são similares aos dos mesmos na aritmética “convencional”, ou seja, quando fazemos contas. Isso é importante ressaltar pois os comportamentos dos operando ++ e — é similar, assim como os efeitos de pré e pós-fixação que vimos quando falamos sobre os operadores e lógica em C. Em especial, é importante que você tome muito cuidado com pré e pós-fixação na aritmética de ponteiros. Por exemplo, se eu usar: *(meuPonteiro++), ele me retornará o valor apontado por meuPonteiro no momento em que o comando é executado e logo em seguida irá executar a soma de uma posição de memória do tipo apontado pelo ponteiro. Porém, se eu usar *(++meuPonteiro), ele irá avançar uma posição de memória antes de efetuar a leitura da memória. Particularmente não uso esse tipo de construção pois pode gerar confusão. Na dúvida, não a use: prefira escrever um código mais claro e limpo. Quando ganhar experiência poderá escrever código mais avançado.
Bem, em seguida o program irá executar o comando operFib++;, que irá avançar uma posição de memória dentro dos valores apontados. Isso afetará a próxima interação, inclusive o cálculo acima mostrado, onde a posição operFib-1 representará a posição de operFib antes dessa soma, ou seja, o valor calculado nessa iteração.
Bem, isso mostra como iremos calcular a nossa seqüência de Fibonacci.
Em seguida temos o código que irá exibir nosso:
  operFib=sequenciaFib;

  for (i=0;<numeroFib;i++)
    {
      printf(“%do. numero Fibonacci e %u e esta armazenado no endereco %p\n”,i+1,*operFib,operFib);
      ++operFib;
    }
Primeira coisa que fazemos é “resetar” operFib, sobrescrevendo o endereço dele com o de sequenciaFib. Perceba que manipulamos o endereço, não o conteúdo do mesmo que continuará intacto.
Em seguida, um laço irá apresentar para nós o número Fibonacci recuperado de operFib, seu valor e o endereço onde ele está armazenado, adicionando uma posição de memória a cada vez que a iteração for for executada. A saída apresentada será algo como (para 12 números de Fibonacci):
1o. numero Fibonacci e 1 e esta armazenado no endereco 0x9a91008
2o. numero Fibonacci e 1 e esta armazenado no endereco 0x9a9100c
3o. numero Fibonacci e 2 e esta armazenado no endereco 0x9a91010
4o. numero Fibonacci e 3 e esta armazenado no endereco 0x9a91014
5o. numero Fibonacci e 5 e esta armazenado no endereco 0x9a91018
6o. numero Fibonacci e 8 e esta armazenado no endereco 0x9a9101c
7o. numero Fibonacci e 13 e esta armazenado no endereco 0x9a91020
8o. numero Fibonacci e 21 e esta armazenado no endereco 0x9a91024
9o. numero Fibonacci e 34 e esta armazenado no endereco 0x9a91028
10o. numero Fibonacci e 55 e esta armazenado no endereco 0x9a9102c
11o. numero Fibonacci e 89 e esta armazenado no endereco 0x9a91030
12o. numero Fibonacci e 144 e esta armazenado no endereco 0x9a91034

Aqui, perceba o endereço marcado em vermelho (ele variará na sua máquina conforme as execuções, e dificilmente será o mesmo que estou apresentando aqui). Note que ele vai subindo de 4 em 4 (c em Hexadecima representa o número 12). Isso deve-se ao fato de que na plataforma onde executei esse código, o tamanho de um unsigned long int é de 4 bytes (32 bits). Esse valor pode variar conforme a plataforma, mas o importante aqui é que ele vai almentando de 4 em 4 bytes, ou seja, a cada posição do tamanho de um unsigned long int, enfatizando o que dissemos anteriormente sobre o tamanho do tipo de dado na questão da aritmética de ponteiro. Se você lembrar do código e da saída do programa que fizemos ao começarmos a explorar ponteiros, você verá que lá ele subia de 1 em 1 byte, o tamanho de um char. Se você tivesse usando um tipo cujo tamanho fosse de 20 bytes, a aritmética de ponteiro aumentaria o valor da posição de memória de 20 em 20 bytes (imaginando que eles fossem cotiguos, ou seja, em seqüência, que é uma obrigatoriedade para o malloc funcionar corretamente).
Por fim, antes de terminar o programa, o mesmo libera a memória com o comando free. Embora não seja exatamente obrigatório (a maioria dos sistemas operacionais modernos desalocam completamente qualquer memória utilizada por um programa ao término de sua execução), é uma excelente prática ao sair do programa desalocar qualquer ponteiro que possa vir a estar alocado no momento do encerramento do programa. Na realidade, a melhor prática é desalocar qualquer memória alocada dinâmicamente tão logo o programa não mais precise dela, de modo a aproveitar melhor os recursos do computador. Tenha isso sempre em mente ao desenvolver com ponteiros.
Bem, aqui terminamos essa nossa aula. O próximo tópico ainda envolverá ponteiros: na realidade, ele irá mostrar as complexidades envolvendo ponteiros e funções, inclusive a técnica de ponteiros de função, muito usada em programação. Até lá, sugiro que brinque um pouco. Tente “remover” as limitações que impedem o ponteiro de “desgarrar” e veja as conseqüências (PS: não faça isso em produção. Vou repetir: NÃO FAÇA ISSO EM PRODUÇÃO. NEM PENSE EM FAZER ISSO EM PRODUÇÃO!!!). Analise os códigos aos poucos. Tente implementar outros algoritmos (por exemplo, o algoritmo de Fatorial).
Até a próxima, e lembre-se: os comentários estão abertos para dúvidas e sugestões!

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: