Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos da Categoria: Ponteiros

Matrizes e Ponteiros – Parte 3 – Ponteiros e Funções

Olá!

Esse tópico será um pouco diferenciado, portanto vou avisando que será BEM MAIS LONGO e complexo do que os anteriores. Isso porque iremos ver como as funções em C são afetadas pelos ponteiros. É um tópico bem avançado em teoria, mas é interessante mencioná-lo agora, enquanto os conceitos relacionados a ponteiros estão “frescos” na memória. Na verdade, esse tópico será dividido em dois subtópicos:

  • Ponteiros de função, ou seja, usar um ponteiro para chamar diferentes funções decididas em tempo de programação. Isso é muito usado, por exemplo, em plugins de programas. Dizendo de maneira bem simplista, poderíamos carregar para a memória o conteúdo de um arquivo e fornecer o endereço onde esse conteúdo carregado se localiza para um ponteiro de função que o sistema executaria esse código dinamicamente (na verdade, existem mais complicadores, mas em termos gerais podemos dizer isso);
  • Passagem de ponteiros para funções: existem detalhes a serem mencionados quando passamos um ponteiro para uma função. Isso ocorre com muita freqüencia, uma vez que muitos “tipos” que um programador pode esperar de uma linguagem de programação (como strings, filas, listas, etc…) são resolvidas em C pelo uso de ponteiros;

Bem, vamos começar com o primeiro de nossos subtópicos:

Ponteiros de Função

Como já cansamos de dizer nesse “curso”, ponteiros são variáveis que armazenam um endereço no qual está um determinado conteúdo. Se pensarmos dessa forma, podemos imaginar que isso possa se aplicar a trechos de código.
“Por que?”, você deve se perguntar. Porque, embora o código não fique no heap ou em outros pontos de memória como os dos dados, ele PRECISA ESTAR na memória para funcionar. Ou seja, se um código não estiver carregado na memória RAM (ao menos no momento da execução), ele não pode ser executado. De fato, quando chamamos um programa binário, a primeira coisa que o sistema operacional faz é chamar uma rotina chamada LOADER para copiar os conteúdos do arquivo onde o binário se encontra para um espaço de memória específico (programas podem eles próprio solicitarem ao SO para carregarem partes de si que ficaram de fora da memória em tempo de execução – isso se chama carga dinâmica e é um tópico mais avançado, que não iremos tratar em um futuro próximo). Em seguida, são feitos procedimentos para iniciar a execução do programa que mudam conforme a arquitetura do sistema operacional e do hardware, mas em geral envolve inicializar uma série de variáveis de baixo nível do sistema operacional e algumas informações no nível dos registradores de hardware (um registrador é um espaço de memória muito pequeno que é usado diretamente pela CPU para coordenar as operações de sistema. Fundamentalmente são eles que carregam as informações e códigos “para dentro” da CPU e “devolvem” o resultado). De qualquer modo, o programa é carregado em memória e em seguida inicializado sua execução.
Quando um programa encontra uma chamada a uma função, ele “salta” para o local de memória onde o código daquela função está e começa a executar o código da função (existem certos procedimentos internos para impedir que a execução da função afete de maneira indesejada o funcionamento do código que a chamou, mas não entraremos em detalhes aqui). Ao terminar essa execução, ele “volta” para o comando seguinte ao qual a função foi chamado. Para isso, obviamente o sistema armazena antes as informações sobre onde ele estava antes de a função ser chamada.
Se pensarmos bem, vamos ver que o tempo todo existe manipulação de endereços no momento da execução de código. E se há endereços, há ponteiros!
Chega de papo! Nosso programa exemplo da vez é uma “calculadora” simples. O código é o seguinte:

#include <stdio.h>
#include <stdlib.h>

int soma (int, int);
int subtracao (int, int);
int multiplicacao (int, int);
int divisao (int, int);

int main(void)
{
  int (*funcPointer)(); // o meu ponteiro de funcao vem aqui

  int a=0,b=0,c=0,oper=0;

  printf (“Entre com dois numeros inteiros separados por espaco: “);
  scanf (“%d %d”, &a, &b);

  do
    {
      printf (“Escolha uma operação a se realizar com esses numeros:\n”);
      printf (“1-) Soma\n”);
      printf (“2-) Subtracao\n”);
      printf (“3-) Multiplicacao\n”);
      printf (“4-) Divisao\n”);
      scanf(“%d”, &oper);
    } while (!((oper>=1) && (oper<=4)));

  switch(oper)
    {
    case 1:
      {
        funcPointer=soma;
        break;
      }
    case 2:
      {
        funcPointer=subtracao;
        break;
      }
    case 3:
      {
        funcPointer=multiplicacao;
        break;
      }
    case 4:
      {
        funcPointer=divisao;
        break;
      }
    }

  c=(*funcPointer)(a,b);

  printf(“O resultado da operacao desejada com os valores %d e %d e %d\n”,a,b,c);

  printf(“A funcao usada para executar a operacao desejada localiza-se em %p\n”,funcPointer);

  return(0);

}

int soma (int a, int b)
{
  return a+b;
}

int subtracao (int a, int b)
{
  return a-b;
}

int multiplicacao (int a, int b)
{
  return a*b;
}

int divisao (int a, int b)
{
  if (b==0)
    {
      printf(“Nao posso dividir nada por zero!\n”);
      return 0;
    }
  return a/b;
}

Bem, se observarmos bem, não existem grandes novidades aqui. Portanto, vamos nos restringir às explicações sobre os ponteiros de função. A primeira coisa importante a se notar está na declaração do ponteiro de função:

  int (*funcPointer)(); // o meu ponteiro de funcao vem aqui

Se você reparar, lembra levemente um protótipo de uma função. Porém, perceba que você tem o (*funcPointer)(), que indica que é um ponteiro de função. Poderíamos ler esse código como “funcPointer é um ponteiro que aponta para o código de uma função que devolve int“. Outra coisa a se notar é que aqui não tentamos resolver a parte da parametrização. Isso é importante de ser lembrado pois em teoria você pode usar funcPointer para executar qualquer função que retorne int. No momento em que usarmos o ponteiro de função, porém, é o momento no qual teremos que “resolver” a parte da parametrização. No momento, basta saber que aqui na realidade declaramos uma varíavel, ponteiro, que irá apontar para o código de uma função que irá retornar um valor int.
Em seguida, temos um switch…case que se baseia em um valor que passamos para ele como um int para decidir qual a operação a ser feita. Cada número possui um código similar ao abaixo:

        funcPointer=multiplicacao;

Se olharmos no início, veremos que declaramos funções que irão retornar valores int (ou seja, do tipo que é aceito por funcPointer):

int soma (int, int);
int subtracao (int, int);

int multiplicacao (int, int);

int divisao (int, int);

OK. Mas a sua pergunta deve ser “como o C sabe tratar as coisas nesse caso?”. Para o C, se você passa o nome de uma função sem os seus parâmetros, você está na realidade passando uma variável, ponteiro, para uma função cujo retorno será o tipo que a função retorna. Se pegarmos o nosso caso:

        funcPointer=multiplicacao;

Veremos que na nossa lista de funções declaradas existe uma função int multiplicacao (int, int);. Desse modo, o C trata o multiplicacao nesse caso não como um comando, mas como uma variável. Portanto, ele devolverá o endereço da posição de memória onde essa função começa. Isso é comprovado pelo comando:

  printf(“A funcao usada para executar a operacao desejada localiza-se em %p\n”,funcPointer);

que irá exibir o endereço de memória onde a função está armazenada.

Voltando ao programa: já sabemos como declarar um ponteiro de função (algo como tipo (*nomeDoPonteiroDeFuncao)()) e sabemos como passar o endereço para um ponteiro de função (como para qualquer outro). E para executarmos esse comando, como fazemos?

  c=(*funcPointer)(a,b);

É dessa forma. Usamos uma escrita similar à da declaração do ponteiro, mas agora indicando os parâmetros desejados.

AQUI MORA DRAGÕES!!!

O C não faz checagem de cabeçalho de função e das tipagens envolvidas em um ponteiro de função. Isso passa a ser responsabilidade do programador, tomar os cuidados de passar os parâmetros da forma correta. Portanto, devemos ter muito cuidado quanto à parametrização das funções a serem passadas via poibteiros de função. Em geral, quando precisamos desse recurso, utilizamos algum tipo de padronização dos parâmetros. Mesmo assim, vale a ressalva de que se tome muito cuidado com isso.
Perceba uma coisa: o retorno da função executada pelo ponteiro é armazenado na variável int c. Se o retorno para o ponteiro de função fosse void, não teríamos necessidade de guardar retorno nenhum. Os retornos continuam sendo resolvidos normalmente e warnings e erros são retornados por devolver retornos errados. Mas temos que tomar cuidado também quanto a isso: imagine que você declare a função divisao como retornando float. Embora o compilador retorne um warning relacionado à passagem de tipo incompatível, ainda assim ele irá gerar o código binário (você consegue evitar isso usando as opções de compilação pedântica do seu compilador – consulte a documentação do mesmo para maiores informações).
De resto, o código não possui muitos mistérios: uma vez que o switch escolha a função a ser executada, baseada na entrada do usuário, o ponteiro funcPointer recebe o endereço da mesma. Em seguida, é feita a execução da mesma, como dissemos anteriormente, e o valor é retornando para a varável c. Por fim, os comandos printf ao final de main exibira os resultados e o endereço onde a função alocada se encontra. Como “brincadeira”, para você entender melhor, mexa a seqüência nas quais as funções estão dispostas no código fonte. Essa seqüência meio que determina qual função ficará primeiro no código binário. Assim você poderá perceber que o código é manipulado não importa onde.
Bem, brinque um pouco com esse programa para entender ponteriros de função.

Passagem de ponteiros (Passagem por referência e por valor):

Até agora, todas as nossas funções tem passado seus parâmetros por valor, ou seja, passamos uma cópia da informação armazenada na variável que passamos. Isso permite um certo isolamento entre a função chamada e a que chama, o que garante uma segurança quanto aos conteúdos da variável “local”.
Porém, em muitas situações em C, precisamos manipular diretamente a informação das variáveis em uma função. Um exemplo de função que faz isso é a função da biblioteca STDIO scanf. Ela precisa saber onde está a variável cujo conteúdo será alterado para fazer a leitura da informação via teclado (utilizando-se do sistema operacional) e gravando o conteúdo no mesmo local apontado pela varíavel em questão. Isso é chamado tecnicamente de Passagem de parâmetros por referência e com isso o conteúdo no endereço apontado pela variável pode ser manipulado.
O C oferece, através dos ponteiros, formas de passarmos parâmetros por referência. Veremos isso no nosso próximo programa, que é uma brincadeira similar ao programa de TV Roda-a-Roda (antigo Roletrando):

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define NUM_TENTATIVAS 6
#define TAMANHO_PALAVRAS 20

int checaLetra (const char* palavraEscolhida, const char* letrasCertas, char letra);
void explicarRegras (void);
int contaFaltantes (const char* letrasCertas);
int testaCerto (const char* palavraEscolhida, int* tentativasErradas);

int main (void)
{

  // Declarando variaveis

  // Biblioteca de palavras
  char palavras[][TAMANHO_PALAVRAS]={“rotina”, “retina”, “palhaco”, “lembranca”,
                                     “sistema”, “musica”,”curioso”, “fantasia”,
                     “malabares”,”sonhador”,”atitude”,”pacoca”};

  // Ponteiros necessarios
  char *palavraEscolhida, *letrasCertas;

  // Algumas variaveis de apoio
  int tentativasErradas=0,i;

  explicarRegras();

  srand(time(NULL)); // Serve para modificar a tabela de números pseudo-aleatórios

  palavraEscolhida=palavras[rand()%((sizeof(palavras)/TAMANHO_PALAVRAS)-1)];

  letrasCertas=malloc(strlen(palavraEscolhida)+1);
           
  if (!letrasCertas)
    {
      printf(“Nao consegui alocar memoria!\n”);
      return(1);
    }

  memset(letrasCertas,”, strlen(palavraEscolhida)+1);
  memset(letrasCertas,’-‘, strlen(palavraEscolhida));

  int tentativaAtual=0;
  while(tentativasErradas
    {
      char minhaLetra;
      tentativaAtual++;
     
      printf (“Okay, tentativa no %d (%d tentativas erradas):”, tentativaAtual, tentativasErradas);
      scanf (“%c”, &minhaLetra);
      getchar();
      if (!checaLetra(palavraEscolhida,letrasCertas,minhaLetra))
    {
      printf(“Que pena, essa letra nao aparece!\n”);
      tentativasErradas++;
      continue;
    }

      printf(“A palavra ate agora e: %s\n”,letrasCertas);

      if (contaFaltantes(letrasCertas)<=(strlen(palavraEscolhida)/2))
    {
      if((contaFaltantes(letrasCertas)==0)||testaCerto(palavraEscolhida,&tentativasErradas))
        {
          printf(“Muito bem! Você acertou!\n”);
          return(0);
        }
    }
    }
}

void explicarRegras (void)
{
  // Explicando as regras do jogo para o jogador
  printf (“Okay, vamos explicar as regras!\n”);
  printf (“Eu conheco algumas palavras, e vou escolher aleatoriamente uma delas.\n”);
  printf (“Voce vai me dizer qual letra voce acha que essa palavra tem!\n”);
  printf (“Se voce errar, vou considerar uma tentativa errada!\n”);
  printf (“Se voce acertar, vou te mostrar onde as letras estao!\n”);
  printf (“Quando voce tiver acertado metade das palavras, vou te dar “);
  printf (“a chance de dizer qual palavra e essa. \n”);
  printf (“Se voce errar, vou considerar uma tentativa errada!\n”);
  printf (“Se voce acertar antes de acabar as tentativas, voce vence!\n\n\n”);
  printf (“Algumas observacoes:\n”);
  printf (“Voce tem %d tentativas que voce pode errar.\n”,NUM_TENTATIVAS);
  printf (“Nenhuma palavra possui acentos, cedilha ou trema.\n”);
  printf (“Nao estou diferenciando maisculas e minusculas. \n\n”);
}

int checaLetra (const char* palavra, const char* letrasCertas, char letra)
{
  // Acertos obtidos (0 indica que nao tem a letra em questao na palavra)
  int acertos=0;

  // Ponteiros a serem inicializados
  char *palavraAnalisada, *certas;
 
  // Inicialidando os ponteiros com os valores recebidos
  palavraAnalisada=palavra;
  certas=letrasCertas;

  char letraComparada=((letra>=’A’)&&(letra<=’Z’))?letra-‘A’+’a’:letra;

  while (*palavraAnalisada)
  {
    if (*palavraAnalisada==letraComparada)
      {
    acertos++;
    *certas=letraComparada;
      }
    palavraAnalisada++;
    certas++;
  }

  return acertos;
}

int contaFaltantes(const char* letrasCertas)
{
  char *letras=letrasCertas;
  int faltantes=0;

  while (*letras) if (*(letras++)==’-‘) faltantes++;

  return faltantes;
}

int testaCerto (const char* palavraEscolhida, int *tentativasErradas)
{
  char *minhaResposta;

  minhaResposta=(char*)malloc(strlen(palavraEscolhida)+1);
  memset(minhaResposta,”, strlen(palavraEscolhida)+1);
     
  if (!minhaResposta)
    {
      printf(“Nao consegui alocar memoria!\n”);
      return(1);
    }

  char *conversao=minhaResposta;
  printf(“Qual palavra voce acha que e essa? “);
  fgets(minhaResposta,strlen(palavraEscolhida)+1,stdin);

  // Convertendo para minusculas e eliminando os caracteres de nova linha – \n
  while (*conversao)
    {
      *conversao=((*conversao>=’A’)&&(*conversao<=’Z’))?*conversao-‘A’+’a’:*conversao;
      conversao++;
    }

  minhaResposta[strlen(minhaResposta)]=”;
 
  if(strncmp(minhaResposta,palavraEscolhida,strlen(palavraEscolhida))==0)
    return(1);
  else
    {
      printf(“Que pena, voce errou!\n”);
      (*tentativasErradas)++;
      return(0);
    }
}

Existe pouca coisa nova aqui, à exceção do uso que faremos de passagem de ponteiros a funções e suas idiossincrásias. Inicialmente fazemos algumas inclusões e criamos duas constantes: NUM_TENTATIVAS e TAMANHO_PALAVRAS. Ambas são usadas para evitarmos “números mágicos”. Em especial, nós estamos incluindo a biblioteca padrão string.h, que define funções especiais para strings, além de ter a função malloc(), que precisaremos para alocar dinamicamente algum espaço de memória. Também declaramos quatro funções por meio de seus protótipos:

int checaLetra (const char* palavraEscolhida, const char* letrasCertas, char letra);
void explicarRegras (void);
int contaFaltantes (const char* letrasCertas);
int testaCerto (const char* palavraEscolhida, int* tentativasErradas);

As funções checaLetra, contaFaltantes e testaCerto são as funções que nos interessam. explicarRegras não faz nada, exceto exibir algum texto informativo explicando o funcionamento do nosso jogo. Não vou me ater a explicar o jogo, pois o jogo mesmo irá se explicar. 😉
No main, começamos declarando todas as variáveis úteis:

  char palavras[][TAMANHO_PALAVRAS]={“rotina”, “retina”, “palhaco”, “lembranca”,
                                     “sistema”, “musica”,”curioso”, “fantasia”,
                     “malabares”,”sonhador”,”atitude”,”pacoca”};

  // Ponteiros necessarios
  char *palavraEscolhida, *letrasCertas;

  // Algumas variaveis de apoio
  int tentativasErradas=0,i;

Declaramos uma matriz bidimencional de caracteres (ou, para ser mais exato, uma matriz de strings), onde uma das dimensões está em branco e a outra tem como tamanho TAMANHO_PALAVRAS. Isso é permitido para no máximo uma dimensão: se uma dimensão estiver em branco, o C irá alocar o espaço suficiente para conter os elementos nela, mas APENAS se a inicialização acontecer no momento da declaração, como no nosso código. Em seguida, declaramos dois ponteiros de caracteres, palavraEscolhida e letrasCertas, e algumas variáveis de apoio.
Em seguida, o sistema chama explicarRegras para explicar como funciona nosso jogo. Em seguida, usamos a função srand(time(NULL)) para gerarmos uma nova raiz de números pseudo-aleatórios (já vimos isso anteriormente, lembra?) e, usando

palavraEscolhida=palavras[rand()%((sizeof(palavras)/TAMANHO_PALAVRAS)-1)];

escolhemos uma das palavras.
Aqui tem uma pegadinha de programação: se você se lembra bem, mencionamos anteriormente que para o C, matrizes e ponteiros são estruturas similares. Isso porque em C uma matriz nada mais é que uma quantidade de memória alocada pelo sistema e declarada em ponteiro. Isso vale também para matrizes quaisquer dimensão. Podemos, desse modo, utilizar uma técnica para apontar diretamente a matriz de maneira “correta” (na realidade, existe um pequeno bug aqui, que vou deixar como uma “brincadeira” para você, leitor, identificar. A pista é: tem a ver com o tamanho. Você vai entender a seguir). No caso, o que fazemos é fazer com que o sistema escolha uma posição no começo de uma palavra. Para isso, existe alguma matemática com a função rand, usando o tamanho da matriz palavras e o TAMANHO_PALAVRAS de modo que rand retorne um valor entre 0 e o número de palavras-1, devolvendo o endereço do início dessa palavra para palavraEscolhida. Você pode se perguntar “e porque não copiar a palavra escolhida”. Isso é possível, mas é mais complexo e consome memória e tempo de processamento. Novamente: não pense em programação apenas para PCs, com seus gigas de memória e processamento. Pense, por exemplo, em um celular com alguns KB de espaço útil e poucos megahertz de processamento. Aproveitar a palavra que JÁ ESTÁ NA MEMÓRIA é uma idéia melhor. Nós veremos como usar essa informação de maneira segura já já.
Em seguida usamos malloc para alocar um espaço de tamanho igual ao tamanho da palavra escolhida. Fazemos uma checagem para ver se a alocação foi bem sucedida (NUNCA SE ESQUEÇA DISSO!) e usamos a função memset para preencher a memória alocada com terminadores nulos e, em seguida, com hifens. Essa segunda variável irá armazenar aonde você já acertou na escolha das palavras.
Entramos então no loop principal. Declaramos um caracter e adicionamos um a cada tentativa. O caracter é só declarado, sem ser inicializado. Aqui podemos “quebrar” a boa prática de C de inicializar uma variável assim que declará-la, pois aqui não iremos fazer mais nada que ler um caracter. Não há problema imediato nesse caso, pois é uma leitura rápida e o endereço usado será sempre o mesmo (não estamos alocando dinamicamente esse caracter). O jogador recebe uma informação sobre o número de tentativas que ele fez e quantas ele já errou (o laço compara o valor de tentativasErradas com o de NUM_TENTATIVAS). Foi colocado um getchar() para “limpar” qualquer caracter espúrio que tenha sobrado (em especial o ENTER).
E agora entramos em nossa primeira checagem: o jogador escolheu uma letra certa?
Para isso, temos a função:

int checaLetra (const char* palavraEscolhida, const char* letrasCertas, char letra);

Que recebe dois ponteiros e a letra a ser comparada.
Aqui existe um detalhe: os dois ponteiros são declarados como const.
Por que? Não queremos manipular o conteúdo deles?”, você deve estar se perguntando.

Aqui é importante clarificar uma coisa: quando você manda um ponteiro para uma função, ela tem, digamos assim, poder total sobre ele. Isso inclui trocar o endereço de memória para o qual o ponteiro aponta. Se você lembrar de quando usamos ponteiros para executar a função de Fibonacci, utilizamos dois ponteiros apontando para o mesmo lugar, sendo que um deles é que “se deslocaria” através do local alocado para armazenarmos os valores.
Para garantirmos que um ponteiro que estamos passando não vai ter seu endereço alterado, usamos a palavra-chave const antes do char*, para indicarmos ao sistema que esse ponteiro é constante (ou seja, irá apontar SEMPRE para o mesmo lugar enquanto em uso), o que oferecerá uma garantia de que, mesmo que o CONTEÚDO do endereço apontado seja alterado, o ENDEREÇO não o seja. Por segurança, a não ser que você precise manipular os endereços apontados, sempre coloque const quando usar ponteiros em parâmetros de função.
Indo para a função checaLetra, encontramos o seguinte código:

int checaLetra (const char* palavra, const char* letrasCertas, char letra)
{
  // Acertos obtidos (0 indica que nao tem a letra em questao na palavra)
  int acertos=0;

  // Ponteiros a serem inicializados
  char *palavraAnalisada, *certas;
 
  // Inicialidando os ponteiros com os valores recebidos
  palavraAnalisada=palavra;
  certas=letrasCertas;

  char letraComparada=((letra>=’A’)&&(letra<=’Z’))?letra-‘A’+’a’:letra;

  while (*palavraAnalisada)
  {
    if (*palavraAnalisada==letraComparada)
      {
    acertos++;
    *certas=letraComparada;
      }
    palavraAnalisada++;
    certas++;
  }

  return acertos;
}

Vamos analisar essa função. Para começar, temos a declaração de três variáveis: um int onde guardaremos a quantidade de vezes que a letra escolhida “casou” com alguma na palavra (0 se nenhuma), dois ponteiros de caracter (char*) que serão quem realmente fará o trabalho sujo, e um char que irá conter uma versão em minúscula do caracter digitado (isso deve-se ao fato de todas as palavras estarem em minúsculas, o que provocaria problemas na comparação entre os caracteres em caso de maiúsculas). A função não tem muito mistério: iremos correr os ponteiros de palavraAnalisada (que recebe a palavra que o sistema escolheu) e certas (onde serão gravados os acertos). Se o caracter que for apontado por palavraAnalisada “casar” com a letra a ser comparadas, será somado um ao número de acertos e a letra será gravada na posição apontada em certas. Como ambas se movem ao mesmo tempo (no fim do loop de comparação), a posição de certas onde a letra será gravada corresponderá à posição em que a letra fica em palavraAnalisada. Parece estranho no while não haver nenhuma condição de saída aparente, mas lembre-se que para C, qualquer valor 0 ou equivalente é considerado falso e provoca a saída do loop. No caso de strings, o terminador nulo da string é considerado um valor equivalente a 0 e portanto falso. Por isso, não precisamos especificar uma condição de saída. Esse tipo de construção é comum em programas C de alto nível e faz parte do “modo C de pensar” não declarar uma condição de saída explícita aqui. Após o loop, a função retornará quantos acertos ocorreram.
Vejamos agora uma coisa: é necessário criar os dois ponteiros de char?
SIM!
Usando const, garantimos que os endereços armazenados nos ponteiros que passaram os mesmos para o sistema não terão seus valores alterados. Porém, pela própria lógica que usamos, precisaremos de ponteiros que “se movam” através das strings, comparando os caracteres um a um e obtendo os acertos. Não teríamos como fazer isso usando o ponteiro const que passamos, pois ele é constante e portanto não pode ser manipulado. Porém, ao declararmos dois ponteiros e copiarmos os endereços de memória apropriados para eles, podemos “deslocar” esses segundo ponteiros, sem interferir nos ponteiros originais (constantes). Lembre-se que const nesse caso apenas impede que o endereço do ponteiro seja trocado, não seu conteúdo. Por meio dos ponteiros que declaramos dentro da função, podemos modificar os conteúdos das strings apontadas pelos mesmos e deslocar-nos através dos locais apontados, operando por meio de aritmética de ponteiros.
OK, acho que devemos estar claros quanto a isso. Voltemos ao nosso programa.
Lembra que dissemos que a função checaLetra retorna o número de acertos e 0 se nenhum? Isso tem um motivo:

      if (!checaLetra(palavraEscolhida,letrasCertas,minhaLetra))

Esse if explica nossa escolha. Assim como no caso do while em checaLetra, o if aqui é uma “boa prática” de C: ao invés de retornarmos um número arbitrário como falso, usamos o próprio mecanismo de “booleanos” em C (0 ou equivalente igual a falso, qualquer outro valor verdadeiro). Desse modo, o programa fica mais enxuto e rápido por dispensar comparações que seriam “desnecessárias”. Mas atenção: BOAS PRÁTICAS NÃO SÃO OBRIGAÇÕES! Se em algum código você precisar fazer comparações exijam o valor 0 como um valor verdadeiro, não se exime de usar o 0 como verdadeiro e comparar valores com operadores relacionais normalmente. O fato de você se ater a boas práticas ajuda na legibilidade e manutenção do código, mas não deve ser um grilhão para você.
Esse if irá apenas exibir uma mensagem de que o jogador errou e adicionará um ao número de tentativas, e iniciará uma nova interação do while pelo comando continue.
Caso o jogador tenha acertado a letra, ele verá a palavra com as letras que ele já acertou e com traços nas posições onde ainda têm letras que o jogador ainda não acertou. Em seguida chamamos outra função que usa parâmetros por referência, que é a contaFaltantes:

int contaFaltantes(const char* letrasCertas)
{
  char *letras=letrasCertas;
  int faltantes=0;

  while (*letras) if (*(letras++)==’-‘) faltantes++;

  return faltantes;
}

Basicamente ela conta quantos traços ainda existem em letrasCertas (ou seja, quantas letras ainda precisam ser descobertas). Novamente declaramos um int para armazenar o total de faltantes (com 0 caso não haja nenhuma faltante) e um char* para ser usado para “nos deslocarmos” através da string e contarmos os caracteres faltantes. O while dessa função é estranho:

  while (*letras) if (*(letras++)==’-‘) faltantes++;

O que queremos dizer aqui é que: enquanto *letras não apontar um terminador nulo, se o valor apontado por letras no momento do if for um traço, adicione um aos faltantes. Após a comparação, mova uma posição adiante o ponteiro letras (perceba a pós-fixação do ponteiro). Esse é outro tipo de construção característica que o C permite e muitas vezes ela é vista em programas profissionais C.
Depois de contadas as faltantes, comparamos esse total com a metade das letras da palavra escolhida. Se isso for verdade, uma nova contagem é feita para ver se todas as letras foram acertadas:

      if((contaFaltantes(letrasCertas)==0)||testaCerto(palavraEscolhida,&tentativasErradas))

Caso contrário, será chamada a função testaCerto. No caso, o C garante que, caso contaFaltantes retorne 0, testaCerto não será chamada (é a short-fuse logic – lógica de curto-circuito, que faz com que, uma vez uma comparação lógica seja irrefutavelmente verdadeira ou falsa, o resultado seja considerado sem necessidade de realizar os demais processamentos, poupando tempo). Perceba que aqui usamos a comparação contaFaltantes(letrasCertas)==0. Embora aqui pudessemos usar !contaFaltantes(letrasCertas), aqui a boa prática não contribuiria, e sim atrapalharia a legibilidade. É importante ter isso em mente ao escrever seus códigos.
Vejamos agora a função testaCerto, que é chamada no caso de ainda haver letras a serem descobertas em uma palavra com mais de metade de seus caracteres descobertos:

int testaCerto (const char* palavraEscolhida, int *tentativasErradas)
{
  char *minhaResposta;

  minhaResposta=(char*)malloc(strlen(palavraEscolhida)+1);
  memset(minhaResposta,”, strlen(palavraEscolhida)+1);
     
  if (!minhaResposta)
    {
      printf(“Nao consegui alocar memoria!\n”);
      return(1);
    }

  char *conversao=minhaResposta;
  printf(“Qual palavra voce acha que e essa? “);
  fgets(minhaResposta,strlen(palavraEscolhida)+1,stdin);

  // Convertendo para minusculas e eliminando os caracteres de nova linha – \n
  while (*conversao)
    {
      *conversao=((*conversao>=’A’)&&(*conversao<=’Z’))?*conversao-‘A’+’a’:*conversao;
      conversao++;
    }

  minhaResposta[strlen(minhaResposta)]=”;
 
  if(strncmp(minhaResposta,palavraEscolhida,strlen(palavraEscolhida))==0)
    return(1);
  else
    {
      printf(“Que pena, voce errou!\n”);
      (*tentativasErradas)++;
      return(0);
    }
}

Quando chamamos testaCerto, passamos a palavra em um ponteiro const char* e um int*.
Por que esse int não é const?“.
O int aqui não é const pois, vamos dizer assim, o original não era um ponteiro. A validade do que dissemos sobre a alteração potencial do endereço só vale para quando passamos ponteiro-para-ponteiro. Quando derreferenciamos uma variável comum e passamos sua referência, mesmo que haja uma alteração no endereço dentro do código da função, não haverá outros impactos na variável de retorno (além de possíveis crashes de sistema e, na melhor das hipóteses, erros de lógica). Colocar um const como precaução a mais é errado? Não. Mas como o código aqui não vai alterar essa posição (apenas o conteúdo do endereço), não há tanta necessidade assim de colocar const nesse momento.
É imporante aqui dizer que o retorno é 1 para o caso de você acertar a palavra e 0 se errar. A passagem por referência de tentativasErradas é importante pois precisaremos, no caso de erro aqui, adicionar 1 ao número de tentativas erradas (não haveria como fazer uma segunda chamada, uma vez que a entrada do usuário é feita nessa função, e armazenar o valor de retorno dessa função antes da comparação no if que a chama no programa principal, ainda que possível, é complexo e propenso a erros). Inicialmente alocamos o mesmo tanto de memória que usamos na palavraEscolhida para o usuário entrar sua tentativa usando malloc e colocamos um segundo ponteiro chamado conversao para esse endereço. Usamos fgets para aceitar a resposta dada pelo usuário e colocamos o terminador nulo ao fim da entrada do usuário. Usando o ponteiro conversao, convertemos a palavra para minúscula (uma conversão que fazemos usando o fato de que variáveis char podem ser entendidas como int e podemos subtrair um char de outro – mais exatamente, o valor ASCII de um do valor ASCII de outro – e somá-los com um terceiro char).
Em seguida, usamos a função strncmp da biblioteca string.h, para comparar a resposta dada com a palavraEscolhida. Essa função compara um determinado trecho de uma string com o trecho de mesmo tamanho da outra string. Para evitar que o jogador use um cheat e digite apenas a parte que ele sabe da palavra, determinamos que o tamanho a ser usado é o de palavraEscolhida, não da tentativa do jogador. Isso porque strncmp devolve 0 se os dois pedaços forem exatamente iguais, -1 se o primeiro for alfabeticamente anterior ao segundo e 1 se o primeiro for alfabeticamente posterior ao segundo (e aqui você deve ter entendido porque disse que o jogador poderia cheatar aqui). Se o resultado for 0, ou seja, as duas palavras forem iguais, a função retorna 1, o que vai fazer o if no código principal ser verdadeiro. Caso contrário, retornará 0, tornando o if falso (lembrando que pelo short-fuse logic, a primeira função deve ser falsa – senão essa nem seria executadas). Antes de devolver o 0 no caso de erro, porém, ele mostra uma mensagem de erro e soma um ao número de tentativasErradas (atente que o ++ fora do parênteses em (*tentativasErradas)++ indica que o valor a ser alterado é o que está armazenado no endereço apontado por *tentativasErradas, não o próprio *tentativasErradas.).
Se você notar, caso o if seja falso, ele acaba no final do loop. Uma nova iteração será aberta obviamente, se o número de tentativasErradas ainda for menor que o NUM_TENTATIVAS. Caso contrário, o programa sairá do loop e se encerrará.
Algumas “brincadeiras”:

  1. Descubra o bug na instrução que escolhe as palavras;
  2. Aumente o número de palavras e veja se isso altera alguma coisa no funcionamento do código e se ele não chega a escolher alguma das palavras novas ou antigas. Coloque palavras grandes e veja o que acontece. Modifique o código para corrigir tal circunstância;
  3. Uma coisa que não foi considerada propositalmente nesse código: no jogo normal, você não pode pedir uma letra já pedida. Além disso, a qualquer momento você pode pedir uma lista das letras já usadas. Crie uma lógica que trate dessas condições;
  4. Em algumas versões desse jogo, uma tentativa errada de informar a palavra completa acaba com o jogo de uma vez. Tente criar uma lógica assim;
  5. Em algumas versões, existe uma regra que, caso um determinado número de tentativas erradas consecutivas seja alcançada, o jogo acaba. Tente adaptar o jogo para essa regra;

Bem, com esse tópico, encerramos (ao menos por agora) o tópico Matrizes e Ponteiros. Ainda existe um tema dentro dela que falta, que é o de ponteiros de ponteiros, mas veremos ele no futuro, quando tratarmos de estruturas de dados complexas em C. Por agora é só e tenho só que agradecer por vocês terem aguentado tanto. 😛
Saiba mais

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!

Matrizes e Ponteiros – Parte 1

Olá todos! Espero que teham ido bem de Festas!

Hoje começaremos talvez o tópico mais importante de programação C. Esse tópico é importantíssimo e com certeza provocará dúvidas, portanto lembro que os comentários deverão ser usados para tirar dúvidas. Não deixem nenhuma dúvida passar nesse momento, pois isso poderá depois complicar o aprendizado de outros tópicos avançados.

O tema de hoje, e de mais alguns posts é Matrizes e Ponteiros.

Antes, porém, de vermos alguma programação, precisamos de alguma teoria:

Memória e Ponteiros:

Quando vimos a criação de variáveis, ficou uma espécie de “aberto”. Lá foi dito que “em C, as variáveis representam espaços de memória que o compilador irá preparar para determinadas funções para uso do programa.” Na verdade, isso tá certo, mas não totalmente. Quando declaramos uma varíavel, indicamos ao computador que precisamos que uma derminada posição de memória seja separada para uso do programa e que, toda vez que o compilador achar o nome da variável, ele aponte o local em questão para onde a variável foi encontrada. Desse modo podemos dizer que o nome de uma variável é a “representação” do endereço onde fica o conteúdo da mesma.

Porém, 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.

O C nos oferece um mecanismo muito importante para apontar-se para um local de memória previamente desconhecido, ao qual chamamos de ponteiro.

Uma variável de ponteiro (que chamaremos de ponteiro, ou pointer em inglês) é uma variável que armazena o endereço de memória onde o conteúdo que desejamos está. Ela em si não é o conteúdo (embora possamos manipular o ponteiro de modo a mudar o local de memória indicado conforme a necessidade), mas indica onde esse conteúdo tá armazenado. Usando esse ponteiro, podemos chegar ao conteúdo e o trabalhar.

Imagine o ponteiro como a agência de correio. Ela não é as pessoas para quem são entregues as mercadorias, mas ele sabe de alguma forma onde elas moram. O ponteiro funciona de maneira similar.

Matrizes, strings e ponteiros

OK… Mas o que os ponteiros têm a ver com matrizes e strings (que, como vimos lá no Hello World, é uma matriz de caracteres). Bem. na realidade, podemos dizer que uma matriz é um ponteiro.

‘Como assim Bial?”

Quando você declara, por exemplo char nome[80], você está alocando 80 caracteres em uma matriz e apontando para o primeiro deles, de 0 a 79. (Veremos isso mais para frente quando falarmos de aritmética de ponteiros).

Um programa exemplo com ponteiros

OK. Vamos dar um tempo na teoria. Hora de colocar a mão na massa. Digite e compile o programa abaixo:

#include <stdio.h>

int main (void)
{
  char palavra[80]=”Hello World!”;
  char *palavra2;

  palavra2=palavra;

  printf(“O texto %s esta armazenado no endereco %p\n”,palavra,palavra);
  while (*palavra2)
    {
      printf(“O caracter %c da palavra %s esta no endereco %p\n”,*palavra2,palavra,palavra2);
      palavra2++;
    }

  return(0);

}

Bem, o início cansamos de ver, mas vamos ver as declarações que temos coisas interessantes nelas:

  char palavra[80]=”Hello World!”;

  char *palavra2;

Na primeira linha, declaramos uma matriz de 80 caracteres na qual armazenaremos a string “Hello World!”. O C possui uma convenção bastante prática para strings: sempre que você coloca uma string entre aspas duplas (“), o compilador já sabe que deverá colocar ao final da string em questão o caracter terminador nulo (\ 0 – já vimos ele lá atrás, lembra?). No momento em que o programa é carregado, o próprio sistema aloca o equivalente a 80 caracteres e dentro deles coloca a string “Hello World!”. Em seguida, declaramos uma varíavel ponteiro de caracter (char * – ocasionalmente lendo-se char pointer) chamada palavra2. O asterisco é o símbolo que indica que a variável em questão é um ponteiro para o tipo de dado desejado, não o próprio dado. Embora todos os ponteiros tenham o mesmo tamanho, é importante indicar o tipo de dados ao qual aquele ponteiro aponta, pois o uso de um ponteiro de um tipo de dados errado pode acarretar problemas seríssimos (a má interpretação e uso dos dados pelo programa sendo o menor deles). 

A linha seguinte:

  palavra2=palavra;

Tem que ser pensada calmamente. No caso de matrizes , existe uma coisa a ser mencionada: quando você utiliza o nome da variável sem um [] (indicador de posição a referenciar), o C entende que você deseja utilizar ou manipular a o endereço que indica o priemiro item da matriz. Ao mesmo tempo, quando utilizamos apenas o nome da varíavel ponteiro sem o indicador *, indica a mesma coisa. No caso, essa linha pode ser traduzida como:

“Pegue o endereço do primeiro item da matriz palavra e coloque-o como o endereço a ser apontado por palavra2

Ponteiros em C podem ter a posição à qual eles apontam modificadas em tempo de execução. Na realidade, os ponteiros são feitos justamente para terem esse comportamento: veremos a importância desse comportamento mais adiante, quando falarmos de alocação dinâmica de memória.

Em seguida temos um printf:

  printf(“O texto %s esta armazenado no endereco %p\n”,palavra,palavra);

A ideia aqui é mostra onde é que está, na memória, a string “Hello World!”. Em seguida temos um loop:

  while (*palavra2)

    {

      printf(“O caracter %c da palavra %s esta no endereco %p\n”,*palavra2,palavra,palavra2);

      palavra2++;


    }


Que irá deslocar o ponteiro palavra2 e mostrar em que lugar da memória cada um dos elementos de palavra2 está armazenado. Para isso, em ambos os caso, utilizamos o modificador de formato %p que faz com que o printf imprima na tela o local na memória onde o ponteiro está apontando, e não o seu valor.
Agora, uma coisa pode ficar confusa no printf dentro do loop: por que quando queremos mostrar o caracter, temos que usar o símbolo * e quando queremos mostrar o string e o endereço apontado não? Isso acontece porque, tanto o modificador %s quanto o %p esperam um endereço de memória, enquanto o modificador %c (para caracteres) espera um conteúdo “real” (no caso, um caracter). Basicamente essa é a diferença e é uma diferença importante em C: em muitos casos, o C espera conteúdos “reais”, discretos, como um número ou um caracter. Nos demais casos, normalmente se trabalhará com ponteiros. Não existem em C conceitos como “strings”, “filas” e “listas” como o de linguagens de maior nível, como PHP, Java ou Python. Na realidade, o C oferece mecanismos para criar-se e manipular-se esses tipos de dados, em especial por meio dos ponteiros, mas a linguagem em si não possui tratativa para tais estruturas de dados mais amplas. Veremos no futuro como criar algumas dessas estruturas de dados.
Vamos destrinchar um pouco mais esse while, pois ele nos oferece dicas interessantes e maiores informações sobre como lidar com ponteiros em geral e com uma string em C em particular. Primeiro, vamos ver a “condição de saída” do while:

  while (*palavra2)

Ou seja, enquanto o valor apontado por palavra2 (*palavra2) for considerado verdadeiro, o laço segue adiante. Agora, a pergunta que deve estar passando na cabeça é: “como ele vai saber se o valor apontado é verdadeiro?” Quem ficou atento ao que dissemos quando falamos sobre os operadores lógicos deve ter se lembrado de que falamos que para o C qualquer valor é verdadeiro, à exceção do número 0, do caracter terminador nulo \ 0 e do valor pré-definido null. Se lembrarmos como as strings são compostas em C, elas são seqüências de caracteres terminadas com o caracter terminador nulo \ 0. Portanto, uma vez que o deslocamento do ponteiro leve-o para o caracter terminador nulo, o valor do ponteiro será falso e o programa sairá do loop que criamos.
Mas como fazemos o ponteiro avançar nos da string?“. Para isso, usamos um pouco de aritmética de ponteiro. No C, se usarmos os operandos aritméticos mais rudimentares + e , além do incremento e decremento unitários ++ e , podemos fazer o ponteiro avançar ou recuar, ou então indicar elementos adiante e anteriores à posição indicada pelo ponteiro. No nosso caso, utilizamos o incremento unitário:

      palavra2++;

No endereço em palavra2 (perceba a ausência do asterisco). Nesse caso, estamos indicando que queremos passar para o próximo elemento de palavra2. Quando o C executar essa operação, ele irá somar o equivalente ao número de bytes do tipo apontado por palavra2 ao valor de palavra2. Essa No nosso exemplo, não muda muita coisa, pois em quase todas as plataformas, um char tem o tamanho de 1 byte, mas esse é um conceito que é importante ficar claro: quando usamos operadores aritméticos para modificar o endereço apontado por um ponteiro, ele sempre trabalha somando ou subtraindo em bytes o número de elementos do mesmo tipo vezes o tamanho do tipo. Isso ficará mais claro no próximo post, quando iremos usar ponteiros para valores inteiros.
Isso nos dá como o programa funcionará conceitualmente, mas para melhor entendermos o que aconteceu, vamos analisar a saída do mesmo:

Analisando a saída do programa

OK, e como será a saída disso tudo?
O que iremos ter de saída do programa aparentará ser algo como a seguir:

O texto Hello World! esta armazenado no endereco 0xbffc5eb0
O caracter H da palavra Hello World! esta no endereco 0xbffc5eb0
O caracter e da palavra Hello World! esta no endereco 0xbffc5eb1
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb2
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb3
O caracter o da palavra Hello World! esta no endereco 0xbffc5eb4
O caracter   da palavra Hello World! esta no endereco 0xbffc5eb5
O caracter W da palavra Hello World! esta no endereco 0xbffc5eb6
O caracter o da palavra Hello World! esta no endereco 0xbffc5eb7
O caracter r da palavra Hello World! esta no endereco 0xbffc5eb8
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb9
O caracter d da palavra Hello World! esta no endereco 0xbffc5eba
O caracter ! da palavra Hello World! esta no endereco 0xbffc5ebb

Os valores ao final de cada linha são os endereços onde estão armazenados os conteúdos em questão. Com certeze eles irão aparecer diferentes para você no momento em que você executar o programa, mas o importante é entendê-lo.
A primeira coisa é que todos os endereços são indicados no formato numérico de base 16, chamado hexadecimal. Essa é uma convenção antiga adotada no mundo da informática para indicar endereços de memória. Não importa para nós as posições em questão, pois esses valores poderão (e provavelmente irão) mudar de execução em execução.
Na primeira linha, é indicado o endereço da string “Hello World!”, pego pelo endereço do início da matriz que armazena o vetor de dados. Repare bem que o endereço de “Hello World!” e do caracter “H” são os mesmos, e depois o endereço onde ficam armazenados cara caracter aumenta de um em um byte (que é o tamanho do tipo char).
Como “brincadeira”, uma sugestão é tentar fazer com que a string seja “corrida” ao contrário. Isso pode ser feito utilizando o decremento unitário na aritmética de ponteiros e usando os endereços para comparar o momento em que o ponteiro palavra2 chegar no início da string palavra (lembre-se que deverá fazer comparações com o endereço armazenado em ambos os casos, e não com seus conteúdos). É algo mais difícil, e portanto ressalto que qualquer dúvida os comentários estão abertos para que elas sejam tiradas.
Bem, esse é apenas o início do caminho nos ponteiros em C. Na próxima “aula”, veremos mais sobre a aritmética de ponteiros e alocação dinâmica de memória, um tópico muito importante em C.
Até lá, bom estudo!