Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos da Categoria: Básico

Sobre arquivos de código-fonte, arquivos de cabeçalhos e projetos

Olá!
Hoje não iremos apresentar nenhum código.
Putzgrila!” você deve estar dizendo.
Na realidade eu estou dizendo apenas “meia-verdade”, pois não apresentaremos nenhum código, digamos assim, novo.
É que chegamos em um momento do nosso “curso” onde precisaremos de alguma teoria antes de seguir em frente.
Até agora, temos visto programas que podemos compilar e editar em apenas um arquivo de código-fonte e algumas dezenas de linhas. Talvez o “jogo de roletrando” que fizemos na última aula tenha sido o maior código que escrevemos, com em torno de 170 linhas.
Isso pode parecer grande coisa, mas um programa assim  é extremamente incomum.

Como assim?“, você deve estar pensando.
Se pararmos para pensar e compararmos com programas como os que usamos no dia a dia, o nosso “roletrando” é extremamente tosco e rudimentar. Não precisamos pegar leve, pois essa é a verdade. Programas comuns do dia a dia, como um navegador web, o próprio sistema operacional ou um compilador, são programas muito mais complexos e, portanto, com muito mais código. Por exemplo: o Windows XP, segundo a Wikipedia, possui em torno de 45 milhões (sim, MI-LHÕ-ES) de linhas de código. No mesmo artigo, mostra-se que o kernel (a parte mais fundamental do sistema operacional) do Linux 2.6.35 possui 13,5 milhões de linhas de código. Em Julho de 2011, quando esse artigo foi escrito, segundo o site Ohloh.net, o LibreOffice (uma suite de produtividade livre) possuia mais de 7,5 milhões de linhas de código. Pelo mesmo site, e na mesma época, constava que o Mozilla Firefox (um popular navegador Web) possuia em torno de 5 milhões de linhas de código.
Usar tal estatística é problemático, uma vez que o número de linhas de código para descrever-se um programa pode mudar de linguagem de programaçÂão para linguagem de programaçÂão: as linguagens podem incorporar em sua infraestrutura uma maior ou menor quantidade de bibliotecas e funções utilitárias, permitindo que o programador desenvolva programas mais “enxutos” ao utilizar-se de funcionalidades já previstas na linguagem de programaçao. Porém, o caso aqui não é comparar a complexidade no desenvolvimento ou coisas do gênero. Isso não vêem ao caso, pois meu objetivo aqui não é tentar determinar a linguagem que permite produzir mais programa com menos código. Meu objetivo aqui é fazer você pensar: imagine ter que carregar em um editor todos os, por exemplo, 5 milhões de linhas de código do Mozilla Firefox para uma manutenção. Tériamos sérios problemas para localizar o que desejamos corrigir, editar e compilar esse código novo, além do tempo gasto para tais procedimentos.
Entretanto, esses programas existem. “Como?”, você deve se perguntar?
No caso, graças à possibilidade de compilação parcial.
Quando falamos, lá no início do curso, que um compilador compila todo o código e gera o código binário, eu disse apenas meia-verdade. Realmente, o compilador gera UM código binário a partir do código fonte. Pòrém, quem realmente cria o binário esxecutável baseado no código binário gerado pelo compilador é um programa ou rotina do compilador chamado linkeditor (ou linker).
Por que isso é necessário?
O tempo todo, desde o início do curso, temos citado funções. Temos visto funções o tempo todo, desde os printf() até nossas funções personalizadas. Porém, no momento em que o sistema operaciona executa um programa, ele vai apenas lendo e lendo códigos até chegar ao fim do mesmo. Ele pode ter execuções condicionais, baseadas em if, while, etc… (na verdade, tudo vai a um nível ainda mais baixo no fim das contas, mas não vamos entrar nesses detalhes), mas na verdade mesmo com esses saltos cedo ou tarde o programa volta para onde ele estava, até chegar ao fim dele.

Cada função, não importa qual, representa, digamos assim, um “salto”. Como dissemos no passado:

Quando uma função é chamada, o programa principal passa o controle da execução para outro ponto completamente arbitrário dentro do espaço que o programa ocupa na memória e executa os comandos informados na função. Uma vez que termine, ele tem que devolver o controle para que o programa principal volte a ser executado, o que em C indicamos com return, e assim sucessivamente até que o programa chegue ao fim do programa principal e seja encerrado.

A função do linker é justamente calcular as posições de cada função na memória (lembra que falamos que as funções ocupam memória, quando falamos dos ponteiros de função?) e colocar no código binário as posições corretas, de modo que o sistema operacional consiga fazer todos os procedimentos de carga do código e execução de maneira adequada. Além disso, o linker também liga (em inglês, to link significa ligar) determinadas funções e códigos que não fazem parte do código binário que estamos usando para gerar o executável, sendo que esses últimos podem incluir tanto códigos binários de bibliotecas padrãao quanto códigos que estão em outros arquivos passados para o linker.

Atualmente a separação entre compilador e linker está cada vez menor, pois muitos compiladores trazem o linker como uma rotina embutida em si, e não como um programa em separado, mas a função do linker e sua utilidade ainda existe.

OK… Toda essa teoria para que?

Se pensarmos agora, com o que sabemos, é muito mais interessante separarmos o código fonte em pequenos arquivos englobando uma pequena parte do código-fonte, pois:

  1. O programador que for fazer manutenção em determinada parte do código terá uma chance muito menor de, inadvertivamente, mexer em código fonte indevido e provocar erros e bugs, pois cada parcela do código estará contido em um arquivo de código-fonte;
  2. A compilação será mais rápida “com o tempo”. É claro que muito provavelmente a compilação da primeira vez ainda será demorada, pois cada arquivo de código binário terá que ser gerado e, após todos os arquivos serem gerados, eles deverão ser alimentados ao linker (ou ao compilador atualmente), para que o mesmo seja transformado em código executável final. Porém, conforme as manutenções forem se fazendo necessárias, a compilação será mais rápida pois poderemos compilar apenas as parcelas de código (ou seja, os arquviso de código-fonte) que foram alterados e alimentar os códigos binários modificados junto com os demais ao linker. Desse modo, ao invés de compilar, por exemplo, alguns milhões de linhas de código, podemos restringir a compilação a algumas dezenas, ou até menos. De fato, muitos desses programas de grande porte, como o Firefox, utilizam ferramentas que permitem automatizar o processo de compilação de modo que elas detectam quando um ou mais arquivos foram modificados e geram novamente o código binário e o executável final;
  3. Podemos “fechar o código”. Essa expressão quer dizer que, ao oferecermos um código para alguém, podemos oferecê-lo na forma de um binárioo, ao invés de oferecê-lo na forma de código fonte. Isso pode ser interessante quando a pessoa ou empresa está desenvolvendo um código cujo funcionamento interno não deve ser divulgado por razões comerciais ou legais. Desse modo, pode-se oferecer o código binário ao usuário do mesmo que irá incorporá-lo ao seu código por meio do linker;
  4. Fica mais fácil aprender o que o programa faz “com o tempo”: ao invés de obrigar o desenvolvedor a ler todo o código de uma só vez e tentar entender o que ele faz, o desenvolvedor pode se restringir a ler apenas determinadas partes do código. Como a leitura de um código-fonte escritto por um outro desenvolvedor não é um procedimento exatamente simples, ao reduzir a necessidade do desenvolvedor em se “aprofundar” no estudo do código para resolver um problema (que podde ser imediato), ganha-se tempo no desenvolvimento de soluções;

OK… E como “dividimos” um programa e o compilamos?

Primeiro, temos que criar em nossa mente a estrutura dos arquivos de código fonte. É claro que podemos ter arquivos individuais de código fonte para cada função que formos criar, mas isso não é uma boa ideia: se um arquivo único de código fonte é algo ruim, uma grande quantidade de arquivos de código fonte também é ruim, pois dificultaria a localização do arquivo que nos interessa em meio ao mar de arquivos gerado no processo. Em geral, todo bom projeto de programação (chama-se projeto todo o conjunto de arquivos cujo fim é resultar em um programa binário executável e/ou em bibliotecas de funções úteis ao desenvolvedor) tem seus arquivos fontes organizados por função.

Para exemplificarmos esse processo, vamos pegar nosso programa do “jogo do Roletrando” e transformá-lo em um projeto. Organizaremos nosso “projeto Roletrando” em três arquivos:

  • main.c – nele está a lógica principal do jogo. Existe o hábito entre desenvolvedores C de chamar-se o arquivo de código fonte principal do programa de main.c, mas essa é uma decisão arbitrária, uma boa prática que não precisa necessariamente ser seguida (é uma boa prática, não uma obrigação). Estou seguindo essa boa prática como uma forma de facilitarr a vida de quemm for ler o código, mas isso não é obrigatório;
  • regras.c – nesse arquivo, colocaremos as duas funções que lidam com as regras do jogo: explicarRegras(), que é mostrada no início do jogo para que o jogador conheça o funcionamento do jogo, e testaCerto() que checa se o jogador conseguiu acertar a palavra em questão;
  • letras.c – colocaremos aqui as funções que validam o acerto ou erro na tentativa de acerto das letras: checaLetra(), que indica se a letra existe ou não na palavra, e contaFaltantes(), uma função utilizada para saber quantas letras ainda falta (pois a partir da metade de letras acertadas, o jogo passa a pedir que o jogador tente descobrir qual a palabra em questão);

Há um porém: como os códigos de cada arquivo farão referências a funções contidas nos demais arquivos, seria necessário adicionar os protótipos de função de TODAS as funções do programa em TODOS os arquivos. Isso tornaria o programa complexo e suscetível a erros: pense, por exemplo, que você modificou uma função, mas não seus protótipos. O linker não conseguiria ligar corretamente as funções, provocando o erro. O correto seria que o compilador provocasse um erro ou alerta indicando que os códigos antigos não foram adaptados para aquela função nova. Para que isso não aconteça, criaremos o nosso próprio arquivo de cabeçalhos. Quando falamos dos protótipos de função dissemos que:

Funções ? Parte 2 « Aulas de C

O que o compilador normalmente precisa é saber como trabalhar com uma função, ou seja, os valores que ele precisa passar para a mesma como parâmetros e o tipo de retorno da mesma. O código em si não precisa sequer ser descrito como parte do seu programa, podendo estar (o código em si) em qualquer outro lugar. (lembram das bibliotecas, como stdio.h, string.h e stdlib.h? Na realidade eles são úteis para o compilador saber como usar as funções que eles ?representam?. Os códigos estão armazenados em outras bibliotecas e arquivos dentro do sistema operacional ou do compilador).

Em C, chamamos esses arquivos que contêm os protótipos de função de “arquivos de cabeçalho” ou “header files” (por isso a extensão deles é tipicamente .h – de header). Como esses arquivos são incluídos pelo compilador (por meio das diretivas #include) ao código fonte no momento da compilação, esses arquivos são úteis para definir valores padrão e protótipos de funções que serão usadas em várias partes do código fonte, de modo a “isolar” funções desnecessárias ou com nomes similares e assim não provocar erros, além de ajudar na documentação e manutenção do código. No nosso caso, criariamos um arquivo roletrando.h com os cabeçalhos e definições necessárias. Uma outra grande vantagem dos arquivos de cabeçalhos é que o compilador consegue trazer arquivos de cabeçalho que estejam incluídos em arquivos de cabeçalho até um determinado nível, em geral suficiente para que um arquivo de cabeçalho do projeto nosso inclua os arquivos de cabeçalho padrão do C que precisamos.

Sem muito papo, vamos ver como ficou disposto o código no nosso projeto e explicar pequenas alterações no mesmo. O arquivo main.c ficará como abaixo:

/*
 * main.c
 */

#include “roletrando.h”

int main (void)
{

  // Declarando variaveis

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

  // 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<NUM_TENTATIVAS)
    {
      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! Voce acertou!\n”);
          free(palavraEscolhida);
          free(letrasCertas);
          return(0);
        }
    }
    }
}

Primeira coisa: se você comparar o código que estamos colocando aqui com o que colocamos originalmente, verá que, além de curto, ele não traz as funções como checaLetra e afins. Isso é normal e é exatamente o que queremos: isolar o código “principal” em um arquivo e os demais em outros.

Além disso, veja que, ao invés dos #includes, #defines e protótipos de função da versão “antiga”, colocamos apenas um #include, que é levemente diferente do que os #includes que vimos anteriormente:

#include “roletrando.h”

Esse #include por trazer o nome do arquivo de inclusão entre aspas duplas, ao invés de cercada por sinais de maior e menor (como em #include <stdio.h>). “Qual é a diferença?”, você deve se perguntar.
A diferença está relacionada com a idéia de “biblioteca padrão”:
Toda linguagem de programação (e C não é exceção), tem sua biblioteca padrão de funções. Essa biblioteca representa conjuntos de funções que o programador pode esperar que um ambiente de desenvolvimento inclua, embora isso não seja obrigatório (em ambientes de pouca memória ou com usos específicos, pode-se implementar C sem várias das bibliotecas padrão). Somada à biblioteca padrão, toda linguagem de programação permite que o desenvolvedor a expanda, incluindo bibliotecas para as mais diversas finalidades.
Em C, quando um arquivo de cabeçalho no #include vem cercado de sinais de maior e menor, o compilador procura o arquivo com o nome em questão não apenas nos diretórios do projeto em questão, mas em diretórios adicionais normalmente definidos em uma variável de ambiente chamada INCLUDE (isso não é obrigatório, mas tem se tornado um “padrão de fato” devido ao intensivo uso desse procedimento). Esses diretórios contêm diversos arquivos de cabeçalho para diversas bibliotecas, tanto padrão do C quanto para as bibliotecas adicionadas pelo desenvolvedor. Além disso, o próprio compilador tem certa “inteligência” para localizar bibliotecas padrão. Por exemplo, no Linux e na grande maioria dos ambientes Unix padronizados, os arquivos de cabeçalho da biblioteca padrão estão em /usr/include, e caminhos como /usr/local/include são reservados para bibliotecas adicionais.
Quando, porém, no C, a diretiva de compilação #include é cercada por aspas duplas, o C compreende que o cabeçalho em questão é parte específica desse projeto e não utiliza os caminhos em INCLUDE para procurar por ele, procurando-o dentro do diretório no qual o projeto está sendo compilado.
Essa é a diferença básica nesses dois casos: existem mais detalhes, mas iremos nos restringir no momento a essa explicação básica.
Bem, dito isso, o resto do código não possui mistérios: continua sendo o main() do “jogo de roletrando” que fizemos na última aula,  sem modificação alguma no código. Porém, se você tentar compilar ele “agora”, você receberá mensagens de erro como as a seguir (no caso, se você usar GCC):

main.c:5:24: error: roletrando.h: Arquivo ou diretório não encontrado
main.c: In function ‘main’:
main.c:13: error: ‘TAMANHO_PALAVRAS’ undeclared (first use in this function)
main.c:13: error: (Each undeclared identifier is reported only once
main.c:13: error: for each function it appears in.)
main.c:26: error: ‘NULL’ undeclared (first use in this function)
main.c:30: warning: incompatible implicit declaration of built-in function ‘malloc’
main.c:30: warning: incompatible implicit declaration of built-in function ‘strlen’
main.c:34: warning: incompatible implicit declaration of built-in function ‘printf’
main.c:38: warning: incompatible implicit declaration of built-in function ‘memset’
main.c:42: error: ‘NUM_TENTATIVAS’ undeclared (first use in this function)
main.c:47: warning: incompatible implicit declaration of built-in function ‘printf’
main.c:48: warning: incompatible implicit declaration of built-in function ‘scanf’

A primeira linha já dá a pista do que fizemos de errado:

main.c:5:24: error: roletrando.h: Arquivo ou diretório não encontrado

Sem o arquivo roletrando.h, o compilador não sabe uma grande quantidade de coisas, como, por exemplo, qual o valor de TAMANHO_PALAVRAS. Em alguns compiladores modernos, o compilador é de certa forma “inteligente” a ponto de conseguir evitar erros mais sérios, como em funções básicas como malloc e printf, mas ainda assim o sistema possui problemas para determinar tudo o que o programa precisa saber para compilar. Portanto, vamos criar o arquivo roletrando.h:

/*
 * roletrando.h
 */

#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);

Aqui, é interessante comparar o header file com o nosso “programa original”: perceba que é como se tivéssemo pego o código do início da nossa “versão original” e extraído apenas as primeiras linhas, até o início da função main() e colocado ele no arquivo de cabeçalho.
Então eu posso colocar qualquer código C em um arquivo de cabeçalho?
Sim, você pode, mas NÃO, você não deveria.
Ao colocar código convencional no arquivo de cabeçalho, você está jogando fora todas as vantagens relacionadas à separação de código em arquivos individuais E adicionando problemas adicionais. Por exemplo: imagine que você tenha adicionado uma função real (não um protótipo) a um arquivo de cabeçalho que será usado em vários arquivos de código fonte. Ao compilar o código, o compilador irá “adicionar” ao código fonte original o arquivo de cabeçalho e o compilará. Aqui parece OK. Mas, quando o linker ou o compilador forem fazer as ligações do código, haverá VÁRIAS cópias da mesma função em forma de código binário para o linker usar. Como ele não saberá qual usar, ele provocará mensagens de erro.
Como boa prática, podemos considerar que um arquivo de cabeçalho deve restringir-se a:

  1. Diretivas de pré-processador, como #include ou #define, que serão usadas por todos os arquivos que incluirem esse arquivo de cabeçalho, e;
  2. Protótipos de função;

Obviamente você pode incluir qualquer coisa válida em um programa C “normal”, mas isso aumentaria a complexidade do código, jogaria fora todas as vantagens do isolamento de código e colocaria potenciais erros e bugs.
OK, podemos agora compilar o nosso código fonte sem aquele monte de mensagens de erro. Porém, ao tentarmos compilar o código em main.c, teremos a seguinte mensagem de erro:

/tmp/ccrd9zr4.o: In function `main’:
main.c:(.text+0x32c): undefined reference to `explicarRegras’
main.c:(.text+0x4c7): undefined reference to `checaLetra’
main.c:(.text+0x509): undefined reference to `contaFaltantes’
main.c:(.text+0x53f): undefined reference to `contaFaltantes’
main.c:(.text+0x558): undefined reference to `testaCerto’
collect2: ld returned 1 exit status

Se você parar para pensar, vai notar que realmente não implementamos nenhuma das funções mostradas na mensagem de erro e, embora o compilador “saiba da existência” das mesmas (devido aos arquivos de cabeçalho), não sabe onde elas estão (elas não estão aí mesmo) e, portanto, não consegue gerar o código fonte em questão.
Portanto, vamos implementar as funções que faltam por meio dos dois arquivos que faltam, que são regras.c:

/*
 * regras.c
 */

#include “roletrando.h”

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 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)
    {
       free(minhaResposta);
       return(1);
    }
  else
    {
      printf(“Que pena, voce errou!\n”);
      (*tentativasErradas)++;
      free(minhaResposta);
      return(0);
    }
}

E letras.c:

/*
 * letras.c
 */

#include “roletrando.h”

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=(char*)palavra;
  certas=(char*)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=(char*)letrasCertas;
  int faltantes=0;

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

  return faltantes;
}

OK, você deve estar pensando…
E agora, como compilar esse programa?
Se você tentar compilar apenas um dos dois últimos códigos, você receberá uma mensagem de erro como a seguinte:

/usr/lib/gcc/i386-redhat-linux/4.1.2/../../../crt1.o: In function `_start’:
(.text+0x18): undefined reference to `main’
collect2: ld returned 1 exit status

Isso quer dizer que o compilador compilou corretamente o código, mas o linker não encontrou nenhuma função main(). Portanto, o comando padrão de compilação que usamos desde o começo do curso, que vimos lá atrás:
O primeiro programa: HelloWorld.c « Aulas de C

Entre no mesmo diretório onde você gravou o seu HelloWorld.c e digite o seguinte comando:
gcc -o HelloWorld HelloWorld.c

Já não serve mais.
Existem duas formas de compilar-se o código fonte com arquivos individuais e obter-se o binário. A primeira é utilizar-se o comando:

gcc -o roletrando main.c regras.c letras.c

Perceba que a única diferença aqui entre isso e o que temos feito desde sempre é que adicionamos os arquivos .c adicionais após o main(). Entretanto esse método não oferece grandes vantagens: você ainda gastará muito tempo compilando código-fonte pois, embora o código esteja separado, o compilador precisará reuní-lo antes de compilar e ligar o código. Desse modo, ao menos um dos benefícios da separação dos códigos em arquivos individuais terá sido perdida (você ainda gastará muito tempo compilando todo o código, inclusive os trecho que não mudaram).
O método que nos permite os melhores benefícios é aquele em dois passos, onde (1) o compilador gera cada um dos arquivos binários de cada código fonte e (2) o linker gera o binário final a partir do código binário gerado pelo compilador previamente. Cada compilador tem sua metodologia para gerar esse processo, e você deve ler cuidadosamente a documentação do seu compilador.
No GCC, primeiro iremos gerar o código binário (que chamaremos de código-objeto para distingüir do binário final, ou executável), por meio do comando:

gcc -o nome_do_codigo_objeto.o -c meu_arquivo_fonte.c

A opção -c do compilador GCC apenas faz a compilação do código, sem tentar fazer o link, conforme dito na documentação do mesmo:

-c  Compile or assemble the source files, but do not link.  The linking stage simply is not done.  The ultimate output is in the form of an object file for each source file.
By default, the object file name for a source file is made by replacing the suffix .c, .i, .s, etc., with .o.
Unrecognized input files, not requiring compilation or assembly, are ignored.

Ou seja, ele não gerará o o executável.

No nosso caso, utilizaremos primeiro os comandos:

gcc -o letras.o -c letras.c
gcc -o regras.o -c regras.c
gcc -o main.o -c main.c

Para gerarmos os arquivos .o (os códigos-objeto) de cada um dos código-fonte e, após isso, utilizaremos o comando:

gcc -o roletrando letras.o regras.o main.o

Para fazermos o link das funções e obtermos o executável.

É interessante notar que, no caso específico do GCC, ele possui alguma inteligência e é capaz de distinguir código-fonte de código-objeto. Portanto, a última linha do nosso primeiro passo:

gcc -o main.o -c main.c

Poderia ser substituída por:

gcc -o roletrando letras.o regras.o main.c

Sem problemas. Normalmente, porém, fazemos o passo da geração de arquivos objetos para cada arquivo-fonte.
E onde está o ganho? Até agora, gastei mais tempo lançando comandos!” você deve estar se perguntando.
Imagine que você está traduzindo o jogo do roletrando para um outro idioma e tem que, portanto, mexer na função explicarRegras(), em regras.c. Se você tiver com todos os códigos-objetos gerados, tudo o que você precisará fazer é gerar novamente o código-objeto de regras.c (regras.o) e refazer o executável com o comando para fazer o link das funções.
Existem muitas outras vantagens em usarmos esse método, mas não vem ao caso agora. Falaremos sobre outras vantagens no futuro.
Como “brincadeira”, tente visualizar os programas que criamos até agora e que usam funções, e tente recriá-los, separando as funções em arquivos fontes individuais e criando arquivos de cabeçalho para esses códigos. Esse processo é algo padrão no desenvolvimento de software e é algo muito comum.
Na nossa próxima “aula”, continuaremos esse assunto, aprofundando um pouco na automação da construção de binários a partir de arquivos de código fonte individuas e outras estratégias para organização de projetos.

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!

Funções – Parte 2

Olá a todos!
Bem, primeiramente desculpem a demora, pois tive muitas atividades de trabalho que me “frearam” um pouco. Mas não perdi a vontade de passar o que sei de C. E vocês, ainda estão aqui aprendendo?
Bem, então vamos continuar o tópico da aula anterior: Funções.
Na aula anterior, vimos como construir uma função, porque devemos usá-las, e como passar parâmetros e receber seus retornos. Com isso, podemos dizer que sabemos construir funções. Porém, ainda não sabemos como aproveitar ao máximo as funções, uma vez que vimos regras que “amarram” a construção de funções, tornando-as complexas. Em especial a regra de criar-se a função antes do uso (ou seja, colocar o código da função antes de qualquer chamada que seja feita a ela) é muito estranha. Na aula de hoje, iremos ver como escapar dessa “amarra” de programação. Também veremos uma característica das funções em C chamada recursividade, que é a capacidade de uma função chamar a sí própria, o que torna mais simples construir-se determinados algoritmos e programas.
Bem, vamos ao que interessa:

Protótipos de Função:

Bem, vamos começar com o primeiro tópico, que é o de protótipos. No caso, teremos um programa em cada tópico. Para o nosso tópico, usaremos o programa abaixo:

#include <stdio.h>

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

void main(void)
{
  /**
   * No GCC, esse cabecalho para main retorna o seguinte warning:
   *
   * warning: return type of ‘main’ is not ‘int’
   */

  int val1=0, val2=0, opt=0, res=0;

  do
    {
      printf(“Digite um valor:”);
      scanf(“%d”,&val1);

      printf(“Digite outro valor:”);
      scanf(“%d”,&val2);

      printf(“Escolha a operação a ser realizada…\n\n”);
      printf(“1 – Soma\n”);
      printf(“2 – Subtracao\n”);
      printf(“3 – Multiplicacao\n”);
      printf(“4 – Divisao”);
      printf(“0 – Sair do Programa\n\nDigite os operadores e a operacao separados por espaco:”);

      scanf(“%d”,&opt);

     switch(opt)
      {
      case 1:
        res=soma(val1,val2);
        break;
      case 2:
        res=subtracao(val1,val2);
        break;
      case 3:
        res=multiplicacao(val1,val2);
        break;
      case 4:
        res=divisao(val1,val2);
        break;
      case 0:
        break;
      default:
        printf(“Opcao invalida\n”);
        continue;
      }

      if (opt!=0) printf (“O resultado da sua operacao para %d e %d e %d\n\n”,val1,val2,res);
    } while (opt!=0);

  /**
   * No GCC, a linha abaixo provoca o seguinte warning:
   *
   * warning: ‘return’ with a value, in function returning void
   */

  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)
{
  return a/b;
}

Primeira coisa: perceba que usamos novamente um outro tipo de protótipo para main(), void main(void). Como dissemos nos comentários do programa, o uso desse protótipo não é padrão e irá retornar alertas (Warnings) pelo compilador. Se preferir fazer um programapedante” (ou seja, sem erros ou alertas), você pode trocar o protótipo do main() de volta para o velho e bom int main(int argc, char** argv). De qualquer forma, apenas fizemos isso para demonstrar o que acontece quando tenta-se utilizar um desses protótipos não-padrão.

Agora, vamos a algo importante antes de entrarmos no nosso tópico. Olhe o código em verde:

    switch(opt)
    {

      case 1:

        res=soma(val1,val2);

        break;

      case 2:

        res=subtracao(val1,val2);

        break;

      case 3:

        res=multiplicacao(val1,val2);

        break;

      case 4:

        res=divisao(val1,val2);

        break;

      case 0:

        break;

      default:

        printf(“Opcao invalida\n”);

        continue;

    }

Esse comando de controle de fluxo, o switch, é muito usado como substituto para cadeias monstruosas de if…elseif…else, em especial quando existem códigos que serão usados em uma ou mais opções. O switch…case compara o valor da variável dada como entrada (no nosso caso, opt) com o valor inserido em cada uma das linhas case. Caso o valor da variável em questão seja igual ao valor de um dos case, o programa irá seguir a execução desse ponto até o final do bloco switch…case ou até encontrar um comando break, o que acontecer primeiro. No caso de nenhum dos case case com o valor da variável a ser comparada, nada será feito, a não ser que exista uma cláusula default estipulada no bloco. Nesse caso, o programa irá continuar a execução a partir desse ponto, valendo as mesmas regras para os demais case. No nosso caso, por exemplo, se opt for igual a 7, o default será executado e exibirá na tela Opcao Invalida, e retornará ao início do laço do…while. Caso, por exemplo, opt estivesse em 2, o resultado da função subtracao(val1,val2) seria associado à variavel res e em seguida o switch…case seria interrompido.
Bem, agora que falamos desse comando de controle de fluxo que “passou batido” até agora, vamos falar sobre o nosso tópico atual.
Como vimos na aula anterior, as funções devem, na teoria, vir antes de serem usadas por um programa. Na realidade, isso não é bem verdade. O que o compilador normalmente precisa é saber como trabalhar com uma função, ou seja, os valores que ele precisa passar para a mesma como parâmetros e o tipo de retorno da mesma. O código em si não precisa sequer ser descrito como parte do seu programa, podendo estar (o código em si) em qualquer outro lugar. (lembram das bibliotecas, como stdio.h, string.h e stdlib.h? Na realidade eles são úteis para o compilador saber como usar as funções que eles “representam”. Os códigos estão armazenados em outras bibliotecas e arquivos dentro do sistema operacional ou do compilador). Como exemplo, podemos pensar em um conector para celular. Cada celular usa um conector específico e, desde que o conector siga o tipo de conexão que o celular exige, pode ser usado conectores de quaisquer marcas (vide a quantidade de carregadores “genéricos” que existem por aí) e com qualquer tipo de entrada de energia (não importa se é de tomada ou de carro, por exemplo).
No C, chamamos esses “padrões” de protótipos de função. Um protótipo de função nada mais é que um informativo que o compilador usa para saber como “chamar” uma função. A idéia é que os protótipos representam a seqüência de parâmetros e o tipo de retorno das funções a serem usadas no nosso código. Lembram-se de quando falamos que existe a idéia de “caixa preta” no código? Os protótipos são o que permitem a existência dessa “caixa preta”: o que o programador e o compilador precisa saber é o que a função precisa de entrada e o que ela devolve como resultado. O programador não precisa como saber como a função foi construída (imaginando que não tenha sido ele que a construiu) e o compilador só precisa saber se as chamadas de função são “encaixáveis” corretamente ao código.
Quando o programa é compilado, as funções que não fazem parte do programa e que estão em biblotecas são “encaixadas” ao programa de várias formas em um passo chamado de linkedição (em alguns livros mais atuais, usa-se o termo ligação). Um binário sem ser linkeditado é chamado ocasionalmente de código-objeto e, embora não seja útil para ser executado, eles são muito úteis (veremos no futuro compilação de programas com múltiplos fontes), inclusive podendo conter funções que possam ser ligadas a posteriori a outros programas (nessa situação, o código-objeto é chamado também de biblioteca). Uma vez que o ou os programas-fontes sejam compilados e linkeditados, o binário executável está pronto.
Voltando ao assunto, é por meio dos protótipos que o compilador sabe como “encaixar” cada função nas partes onde as funções são chamadas (na realidade, são colocados endereços para onde o programa vai e segue a execução). Além disso, por meio dos protótipos que o compilador, até certo ponto, consegue “perceber” se o código está corretamente construído, pois ele tem todas as informações da entrada de dados (parâmetros) e da saída (retorno).
Para o protótipo da função, a construção é igual ao do nome da função no início da mesma (que alguns chamam de cabeçalhos), com a diferença do ; no fim do protótipo. Na realidade, para o protótipo, você não precisa colocar nenhum nome de variável. uma vez que nesse momento, o que ele precisa saber é quais os tipos de parâmetro a serem recebidos, e não seu nome. No caso, embora tenhamos usado:

int soma(int a, int b);

para uma maior legibilidade do código, poderíamos usar simplesmente:

int soma(int, int);

que seria tão útil quanto o protótipo que o colocamos. De qualquer forma, aconselho que mantenha as declarações com “nomes de variáveis” como uma boa prática, para aumentar a legibilidade sobre quais são os parâmetros a serem passadas.
Não existe o que falar mais: uma vez que o protótipo tenha sido colocado de alguma forma à disposição, o compilador pode buscar funções em qualquer lugar, seja dentro do código objeto equivalente ao fonte compilado, em um arquivo de biblioteca ou no próprio sistema operacional e “encaixá-las” ou “ligá-las” ao programa do usuário.
De resto, existe pouco o que falar desse programa, pois ele não tem mistérios quanto ao que cada função faz. Para as “brincadeiras”, sugerimos:

  1. Tente remover os protótipos (comentando-os, por exemplo) e compilar os programas. Alguns compiladores irão dar alertas mas irão compilar o seu fonte, enquanto outros simplesmente se recusarão a compilar o fonte;
  2. Para comprovar que o “nome de variável” no protótipo não faz diferença, modifique o “nome de variável” de alguma das funções no protótipo, mas não na função;
  3. Para entender bem a idéia dos protótipos de função, uma boa forma é ler a documentação da biblioteca-padrão do C. Existem muitas funções interessantes nela, em especial em bibliotecas como stdlib.h, string.h, stdio.h, time.h e math.h. Nesse link você encontra a documentação completa das bibliotecas-padrão (em inglês). Procure ler com calma e tentar entender o que cada função faz. Obviamente você não compreendará tudo no presente momento, pois muitas funções lidam com conceitos avançados que ainda falaremos, mas com calma você verá algumas funções interessantes. Se possível, tente construir seus próprios programas e funções a partir dos códigos que mencionamos no momento;

Bem, com isso terminamos a parte de protótipos de função. Vamos a um tópico mais interessante: recursividade.

Recursividade – Chamando a si próprio:

Existem certos algoritmos (formas de descrever-se algo para um computador) que são mais facilmente representáveis quando eles usam de algum modo a si próprio. Dois exemplos clássicos são os cálculos de Fatorial e do número Fibonacci. Para relembrar, um número N fatorial (representado N!) é representado pela multiplicação de todos os inteiros até N (sendo que os fatoriais de 0 e 1 são definidos como 1). No caso, esse é o algoritmo que iremos ver em C, pois pe um exercício clássico de programação recursiva.

#include <stdio.h>

unsigned int fatorial (unsigned int a)
{
  if ((a==0) || (a==1))
    return 1;
  else
    return a*fatorial(a-1);
}

int main(void)
{
  unsigned int numeroFatorial=0;

  printf(“Digite o numero ao qual deseja-se obter fatorial (apenas positivos): “);
  scanf(“%d”, &numeroFatorial);
 
  printf(“%d! = %d\n”,numeroFatorial,fatorial(numeroFatorial));

  return(0);
}

Esse código é bem básico e tem pouco mistérios. O importante é atentar ao código da função fatorial:

unsigned int fatorial (unsigned int a)
{

  if ((a==0) || (a==1))
    return 1;

  else

    return a*fatorial(a-1);

}

A primeira coisa que ele define é que, caso o valor passado na execução da função seja 0 ou 1, o valor a ser devolvido pela execução é 1. Caso contrário, ele irá devolver o valor passado vezes o valor devolvido pela execução da mesma função com um valor igual ao valor passado-1.
Como funcionaria então, por exemplo, para o fatorial 5? Vejamos em um teste de mesa:

  • main começa executando fatorial(5);
  • Como 5 não é igual a 0 ou 1, ele deveria retornar 5*o resultado de fatorial(5-1), ou seja, fatorial(4). Como não sabe o valor de fatorial(4), ele executa fatorial(4);
  • Como 4 também não é igual a 0 ou 1, ele deveria retornar 4*o resultado de fatorial(4-1), ou seja, fatorial(3). Como não sabe o valor de fatorial(3), ele executa fatorial(3);
  • Como 3 também não é igual a 0 ou 1, ele deveria retornar 3*o resultado de fatorial(3-1), ou seja, fatorial(2). Como não sabe o valor de fatorial(2), ele executa fatorial(2);
  • Como 2 também não é igual a 0 ou 1, ele deveria retornar 2*o resultado de fatorial(2-1), ou seja, fatorial(1). Como não sabe o valor de fatorial(1), ele executa fatorial(1);
  • Como 1 é igual a 1, a função fatorial retorna 1;
  • Agora ele volta para a execução de fatorial(2), pois obteve o valor de fatorial(1), que ele precisava. Ele faz 2*fatorial(1), ou seja, 2*1, retornando 2;
  • Em seguida, retoma a execução de fatorial(3), pois obteve o valor de fatorial(2), que ele precisava. Ele faz 3*fatorial(2), ou seja, 3*2, retornando 6;
  • Em seguida, retoma a execução de fatorial(4), pois obteve o valor de fatorial(3), que ele precisava. Ele faz 4*fatorial(3), ou seja, 4*6, retornando 24;
  • Por fim, retoma a execução de fatorial(5), pois obteve o valor de fatorial(4), que ele precisava. Ele faz 5*fatorial(4), ou seja, 5*24, retornando 120 para main;

Atenção para a questão de:

  if ((a==0) || (a==1))
    return 1;

Todo algoritmo recursivo deve ter uma situação de “escape”, caso contrário provocará um loop infinito. No caso do fatorial, é o fato que os fatoriais de 0 e 1 são definidos por padrão como 1 (no link da Wikipedia mostrado anteriormente há uma explicação dos motivos desses valores serem pré-definidos). Somando-se isso e o fato de que a chamada recursiva é sempre equivalente ao valor da chamada atual-1, o resultado é que cedo ou tarde, o valor vai ser 0 ou 1 (valores negativos são negados já na tipagem unsigned int), ou seja, a “escada” de chamadas irá ser desfeita, com cada chamada devolvendo os valores esperados pela anterior. Caso isso não ocorra, haverá um loop “infinito” que se encerrará com um estouro de memória (uma vez que cada chamada de função armazena localmente valores e portanto precisa de espaço de memória).
Bem, não existe mais o que se falar sobre recursividade. Como “brincadeiras” quanto recursividade, sugiro:

  1. Uma circunstância a ser levada em consideração ao se construir algoritmos com recursividade é sobre o uso dos operadores unários de incremento e decremento (++ e ). Para observar seu impacto, no return a*fatorial(a-1), tente substituir por return a*(a–) e verifique o que acontece. Lembre-se que os operadores unários de incremento e decremento atuam como operadores de incremento/decremento e atribuição;
  2. Edite o código da chamada recursiva e elimine a “condição de escape” da recursão. Coloque algum código que permita você visualizar os valores recebidos a cada chamada recursiva e seus impactos e analise o resultado final;
  3. Tente implementar o algoritmo para determinar-se um número Fibonacci. Lembrando que um número de Fibonacci equivale à soma de todos os números naturais antecessores a ele, predefinido que o 0° Fibonacci é 0 e o 1° Fibonacci é 1. Se você reparar bem, não é muito diferente do cálculo de um número Fatorial;

Com isso, acabamos o básico de Funções. Ainda existem tópicos a serem cobertos. Em especial, um tópico importante que estamos deixando para trás é o de tipos de passagem de parâmetro, um tópico importante que cobriremos quando falarmos de ponteiros, nosso próximo assunto.
Vamos ter algum tempo até começarmos o assunto de ponteiros. Enquanto isso, existem muitos sites e apostilas na internet com exercícios de programação em C que poderão ajudar você a fixar o conteúdo que vimos até agora. Enquanto a mim, vou ficar um tempo sem uma Internet de boa qualidade, mas prometo que, assim que voltar estarei postando o início do tópico de ponteiros, com a parte de matrizes, ponteiros e a correlação entre os dois. Esse será um tópico bastante complexo, mas que se lido com calma irá ser bem fixado.
Então, nos vemos em 2011, pessoal. Até lá, boas festas e muita programação C para todo mundo! 😛

Funções – Parte 1

Olá a todos!
Bem, agora já estamos começando a pensar em programas nós mesmos, não é?

Agora, vamos pensar um pouco no programas que fizemos lá atrás, quando falamos de Entrada de Dados e Variáveis. Aquele foi um programa razoavelmente grande. Agora imagine que você crie um programa realmente complexo, que realize atividades similares em diversos pontos do mesmo. Se você escrever esse programa como criamos o programa de exemplo de Entrada de Dados e Variáveis, você teria um grande programa com vários pontos repetidos. Desse modo, caso precisasse alterar o modo como essas atividades similares seriam executadas, você teria que mexer em vários pontos similiares, o que mesmo o melhor dos programadores não conseguirá com facilidade e sem a possibilidade de provocar erros.
Por isso, o C (como toda boa linguagem de programação) prevê formas de dividir o programa em “pedaços” que executem a mesma tarefa. Chamamos esses pedaços de funções.
Na realidade, já usamos muitas funções até aqui. Todo comando que mostramos até agora, à exceção de palavras chaves como if ou do…while, são funções. A vantagem de dividir-se o programa em funções é que podemos isolar determinadas atividades nelas, o que permite:

  1. Programas escritos de maneira mais legível;
  2. Melhor manutenção do código, em especial em projetos complexos; você foca só no que está dando errado e uma vez que tudo esteja OK as melhorias se refletem apenas no que está dando errado;
  3. Reutilização de código: por meio das funções podemos criar bibliotecas de funções (lembra do que falamos anteriormente sobre isso?) que englobem funções que usamos constantemente em um (ou mesmo em vários programas) e com isso reaproveitar esse código em muitos casos;

Claro que uma função deve ser criada para ser suficientemente genérica, mas feito isso ela pode ser aproveitada nos mais diversos momentos.
Bem, dito essa teoria, vamos ao nosso programa exemplo: um programa de médias escolares.

#include <stdio.h>

/**
* Essa função de média irá acrescentar os valores adicionados à média
* anterior e devolverá a média no momento
*/
float media(float nota)
{
   static float mediaAtual=0.0;
   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;
   return mediaAtual;
}

int main(void)
{
    float nota=0.0;
    int notas=0;

    do
    {
        printf(“Digite a próxima nota ou -1 para sair:”);
        scanf(“%f”,&nota);

        if (nota!=-1)
        {
             notas++;
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
        }

    } while (nota!=-1);

    return 0;
}

Na parte do main() fica a ressalva de que mudamos um pouco o início: de int main (int argc, char** argv), estamos usando int main(void). Como dissemos no Hello World, essa construção (sobre a qual aproveitaremos para falar a seguir) pode ser mudada, embora o compilador possa gerar um aviso de que você está fugindo do padrão do C. No nosso caso, colocamos void nos parâmetros para indicar que não receberemos parâmetros (void é uma palavra reservada do C que indica algo “vazio” – veremos mais sobre isso adiante), ou melhor, que não utilizaremos parâmetros que sejam passados. De resto, o nosso main() engloba coisas que já vimos nas últimas semanas e que você deve estar afiado caso tenha seguido as sugestões que fizemos para mexer no código e compreendido o que fizemos até agora. Na verdade, tem algo que iremos falar, mas apenas depois que vermos nossa função:

float media(float nota)
{

   static float mediaAtual=0.0;

   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;

   return mediaAtual;

}

Já falamos anteriormente sobre o conceito de blocos de código. Basicamente, um bloco de código é uma parte do programa que é isolada logicamente e considerada como um comando único. Para “isolar-se” um bloco de código em C, usa-se as chaves ({}). De maneira “rápida e suja”, podemos definir uma função como um “bloco de código com nome”. Na realidade, uma função pode estar em um outro ponto do programa ou até mesmo em um arquivo totalmente isolado. A única regra para uma função é que ela tem que vir de alguma forma “antes” de qualquer ponto do programa onde ele seja usado (na próxima semana, quando encerrarmos o assunto Funções, veremos que não é bem assim e existem técnicas simples que permitem ao programador colocar sua função onde deseejar).
Uma função na realidade é caracterizada por realizar alguma tarefa e retornar algum valor. Para facilitar a vida do programador e desobrigá-lo de saber o que a função realmente faz, toda linguagem de programação parte do princípio de que uma função é uma “caixa preta”: você coloca determinados parâmetros na entrada da mesma, ele realiza algum processamento (que o programador não precisa realmente saber do que se trata) e devolve ao usuário alguma saída. Porém, embora seja uma “caixa preta”, é sempre necessário a uma função indicar o que ela espera receber de parâmetros para trabalhar e o que o usuário irá receber de volta. No C isso é feito no momento em que se nomeia a função.
Como dissemos acima, podemos pensar em uma função como um “bloco de código com nome”. Em C, chamamos o “nome da função” de protótipo ou assinatura (esse último é mais usado em documentos focando C++ e tem a ver com certas propriedades da mesma). O protótipo de uma função costuma seguir o formato:

retorno nome (tipoPar1 par1[=init1],tipoPar2 par2[=init2],…,tipoParN parN[=initN])

Onde:

  • retorno indica o tipo de varíavel que a função retorna. Esse tipo pode ser qualquer tipo básico do C, ponteiros (veremos isso quando entrarmos nesse assunto) ou void: void pode ser entendido como um nulo, ou seja, quando C executar uma função cujo retorno seja void ele não deve esperar nenhum retorno do mesmo (na realidade, alguns compiladores costumam provocar erros de compilação);
  • nome é o nome pelo qual a função é chamada. Os nomes de função seguem as mesmas regras que vimos quanto aos nomes de variável que vimos quando falamos de Entrada de Dados e Variáveis.  Além disso, não podem ter o mesmo nome de funções que tenham sido importadas por meio de #includes. Uma coisa: mesmo para as funções da biblioteca padrão vale a mesma regra. Por exemplo: se eu não importar a stdio.h, posso incluir minha própria versão de printf sem problemas. Veremos o motivo disso adiante;
  • Dentro dos parênteses incluímos uma série de indicações sobre os parâmetros da função, tipoPar1 par1[=init1],tipoPar2 par2[=init2],…,tipoParN parN[=initN]. Elas são distrinchadas assim: tipoPar é o tipo da variável em questão, que pode ser de qualquer tipo básico do C, tipos do usuário ou ponteiros (veremos os dois últimos no futuro). A ele pode ser agregado um modificador const, que indica que, não importa o que aconteça, esse valor não deve ser modificado (esse modificador só é útil quando usamos ponteiros – explicaremos o porque quando alcançarmos esse tópico); par é o nome do parâmetro. Opcionalmente, você tem init, que permite que você defina um valor default para inicialização, que será colocado caso esse parâmetro não seja passado (isso é feito ao deixar o espaço desse parâmetro vazio, sem nenhuma váriavel ou valor, mesmo null – para o compilador, passar null é passar um valor, ainda que nulo).
  • Uma ressalva sobre parâmetros: se você não esperar nenhum parâmetro em uma função, é uma boa prática inserir void dentro dos parênteses, ainda que parênteses em branco (()) seja igualmente suficiente para indicar uma função sem parâmetros. Essa boa prática ajuda na leitura do que a função faz e em muitas documentações você verá ela sendo adotada;

Bom, após vermos como é nomeada uma função, vamos ver o nome de nossa função e destrinchá-la:

float media(float nota)
  • Primeiro, indicamos que ela é uma função que irá devolver um valor de ponto flutuante (float);
  • Depois, informamos que o seu nome é media;
  • E na parte de parâmetros, indicamos que ela recebe apenas um parâmetro, do tipo flutuante e chamado nota (float nota). Também, pela ausência de um igual, indicamos que ela não tem um valor default. Portanto, a ausência desse parâmetro irá provocar um erro. Se tivéssemos indicado um default e não passássemos um valor, o compilador poderia devolver um alerta, mas ainda assim o programa iria compilar normalmente;

Escopo de varável e o modificador static:

Dito isso, vamos falar sobre o código. Na nossa primeira linha, temos uma declaração especial:
   static float mediaAtual=0.0;
Essa única linha vai nos levar a todo um tópico de explicações. Na realidade, para entendermos ela totalmente, precisaremos falar sobre escopo de variável e sobre como funciona a declaração de variáveis dentro de uma função.
Como já fizemos no main() quando falamos de Entrada de Dados e Variáveis, podemos declarar variáveis internas em uma função  Na realidade, você pode declarar variáveis dentro de qualquer bloco de código. Como uma função é um “bloco de código nomeado”, podemos definir variáveis dentro delas. Além dos parâmetros (que são variáveis dentro da função), podemos declarar quantas variáveis que acharmos necessárias. No caso, da mesma forma que uma variável no main() representa o local onde um conteúdo fica dentro do programa principal, uma variável dentro de uma função representa o local onde um conteúdo ficará armazenado dentro da função. Importante, porém, é notar que uma variável dentro de uma função pode ter um nome que já esteja sendo usado fora da função. Isso é possível pois, embora os nomes das variáveis sejam iguais, seus escopos são diferentes: uma vale para a função main() e outra para a função que o usuário criou, e o compilador, ao gerar o programa, tratará tais variáveis como variáveis diferentes e portanto com locais em memória diferentes. Existem algumas formas de mudar esse comportamento que veremos adiante.
Normalmente, ao chamar-se uma função, todas as suas variáveis são reinicializadas com os valores que o usuário definiu (ou com valores arbitrários, caso não o tenha feito), ou seja, podemos dizer que a cada chamada de uma função o valor de suas variáveis é resetado. Esse é o comportamento esperado, pois parte-se do princípio que cada chamada de uma função irá processaar valores não exatamente iguais.
Porém, existem casos onde podemos precisar que um ou mais valores permaneçam “salvos” entre chamadas de uma mesma função. Para garantir que isso aconteça, utilizamos um modificador na declaração da variável chamado static (estático). O que ele faz é garantir que a variável em questão tenha seu valor mantido entre as várias chamadas à função. Ou seja, após a função terminar sua execução, seu valor não é resetado como normalmente acontece. Uma situação onde isso pode ser útil é quando, por exemplo, desejamos saber o número de pessoas que executou uma determinada transação bancária: uma forma bruta seria colocar uma variável somando o número de requisições feitas à função de transação e depois definir uma forma de obter-se esse valor. (Embora normalmente só um valor possa ser retornado por função, existem “truques” que permitem obter-se mais de um valor – veremos tais “truques” quando falarmos sobre passagem de valor para funções, no próximo post).
No caso, voltando ao nosso programa, o que fazemos é declarar nossa variável mediaAtual como tipo flutuante (float) e estática (static), inicializando ela como 0.0 (zero flutuante). Essa inicialização será feita apenas na primeira chamada à função dentro do programa. Uma vez que essa chamada tenha se encerrado, na entrada seguinte o sistema irá manter o valor com o qual a variável encerrou a chamada anterior. Ou seja, caso o valor final de  mediaAtual seja 2, esse será o valor de mediaAtual na chamada seguinte.
Você deve estar se perguntando agora: “qual a diferença entre usar static e const em uma função?”. Aparentemente seria nenhuma, mas na verdade é ENORME:

  • const é usado quando você não quer que, durante a execução da função, o valor da varíavel seja alterado. Porém, uma vez que uma const é uma variável como outra qualquer até ser inicializada, ela também tem valores arbitrários preenchidos nela até ser inicializada (lembre-se que uma const só pode ser inicializada, ou seja, receber um valor, UMA ÚNICA VEZ). No caso de const, após o término da execução da função, o valor definido na inicialização é perdido e poderá receber um valor completamente diferente na próxima execução;
  • static deve ser usado quando você não quer que, entre execuções da função, o valor da variável em questão seja perdido. Dentro da função, durante a execução da mesma, você poderá alterar normalmente, como qualquer outra variável. Porém, seu valor não será “resetado” após o término da execução da função.
  • Aqui cabe dizer ainda que é possível criar-se uma variável static const. Esse “pequeno monstrinho” seria uma variável dentro de uma função cujo valor permaneceria sempre o mesmo entre todas as chamadas da mesma, definido na inicialização da variável na primeira chamada. CUIDADO: esse tipo de “monstrinho” pode gerar dor de cabeça séria na programação e normalmente não fará nada de mais interessante que não possa ser feito, por exemplo, com símbolos #define;

Bem, não temos muito o que dizer aqui mais sobre o escopo. Veremos um pouco mais sobre escopo de variáveis no futuro. Agora, vamos continuar analizando nosso código.

Retorno – a palavra chave return:

Continuando nosso código, após a declaração de variável, temos o seguinte código:

   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;
   return mediaAtual;

A primeira linha representa uma atribuição condicional que vimos na última “aula” ao aprofundarmos operadores e lógica. No caso, quando mediaAtual for 0 (no caso, na primeira chamada de função), ele receberá o valor de nota (parâmetro passado pelo usuário). Caso contrário, irá manter em mediaAtual o valor médio entre mediaAtual e nota. Não há muito mistério aqui e, embora a construção possa ser confusa, é só ler com atenção que fica claro o que estamos fazendo. Em seguida, usamos a palavra chave return para devolver a main (que chamou essa função) o valor calculado.
Se lembrarmos bem, temos visto return desde nosso primeiro “Hello World”. Isso porque, como dissemos na época, mesmo main() é uma função para o C, ainda que especial. E a última coisa que qualquer função precisa fazer é devolver o controle da execução para a função que a chamou. Para isso, utilizamos return para indicar que terminamos de processar tudo o que devíamos e que o processador pode voltar para onde ele tinha parado antes de começar a executar nossa função.
“Como assim?”, você deve se perguntar. Bem, ao darmos início ao programa, o mesmo é carregado na memória por um processo de todo sistema operacional chamado loader e sua execução é iniciada. Conforme os comandos são executados, o sistema vai respondendo adequadamente, modificando espaços de memória (representados no programa pelas variáveis) e seguindo adiante de maneira sequencial (isso também considerando os comandos de controle de fluxo). Quando uma função é chamada, o programa principal passa o controle da execução para outro ponto completamente arbitrário dentro do espaço que o programa ocupa na memória e executa os comandos informados na função. Uma vez que termine, ele tem que devolver o controle para que o programa principal volte a ser executado, o que em C indicamos com return, e assim sucessivamente até que o programa chegue ao fim do programa principal e seja encerrado.
O que precisamos saber então sobre o funcionamento de uma função:

  1. Um programa pode requisitar a qualquer momento a execução de uma função;
  2. Funções são usadas para tornar o programa mais reutilizável, mais fácil de manter-se e mais legível;
  3. Ao requisitar a execução de uma função, o programa pode precisar passar parâmetros em uma ordem determinada, indicando o que a função precisa ter de entrada para trabalhar;
  4. A função irá trabalhar como foi estipulado pelo criador da função: para o programa, uma função atua como uma “caixa-preta”;
  5. A função devolve o controle de execução ao programa principal ao encerrar-se, devolvendo valores determinados pelo tipo de retorno da função como saída;

Uma coisa antes de encerrarmos: ao devolver dados para o programa principal, devolvemos ele segundo o tipo estipulado lá no protótipo da função. Se for necessário por qualquer motivo, a função pode fazer typecast do valor devolvido antes de o retornar. Porém, é adequado que não seja usado tal expediente: use uma varíavel do mesmo tipo de retorno estipulado no protótipo e faça as operações necessárias a usando e use ela como valor para return. Assim evitará dores de cabeças e bugs difíceis de depurar-se.
Bem, não temos mais o que falar sobre a função em questão, então vamos voltar ao programa principal.
No programa principal não há mistérios:

int main(void)
{
    float nota=0.0;
    int notas=0;

    do
    {
        printf(“Digite a próxima nota ou -1 para sair:”);
        scanf(“%f”,&nota);

        if (nota!=-1)
        {
             notas++;
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
        }
    } while (nota!=-1);

    printf(“Você inseriu %d notas.\n”, notas);
    return 0;
}

Inicializamos duas variáveis, uma float chamada nota (que receberá a nota a ser “adicionada” à média) e uma inteira chamada notas (que usamos como um contador do número de notas “adicionadas). Um laço do…while é usado para que novas notas sejam inseridas uma após a outra até que o usuário entre com -1 (que é considerado valor de saída).
A única coisa que precisamos aqui é a linha:
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
A pergunta é ‘essa é uma entrada válida’? A resposta é: SIM.
No nosso caso, o printf espera um valor de tipo flutuante (repare no formato %9.2f), o que é oferecido por nossa função media (que retorna um tipo flutuante). ‘E se o programa esperasse, por exemplo, um int, ou recebesse um int?’ No caso, ocorreriam typecasts, mas o C sempre fará o possível para oferecer um retorno ao usuário, não importa se os valores saiam espúrios (ele parte do princípio de que o programador mantenha seus dados de maneira correta, não fazendo muitas checagens).
Bem… Vamos encerrar por agora nessa semana. Na próxima, iremos falar mais sobre funções, incluindo uma aprofundada nos tipos de escopo de variáveis e algumas dicas úteis (e IMPORTANTÍSSIMAS) sobre funções e seus protótipos.
Para essa semana, umas brincadeiras:

  1. Renomeie mediaAtual para outros nomes de variáveis que ocorram e veja o que irá acontecer;
  2. Remova static da declaração de variável da função media;
  3. Tente reescrever media de modo que você não precise usar uma variável static nela;
  4. Escolha um segundo valor (por exemplo, -2) para “resetar” os valores de media;
  5. Tente imprimir o valor retornado por media como inteiro e veja o que acontecerá;
  6. Tente deslocar o código da função media para abaixo da função main e veja as mensagens de erro. Procure entender os motivos das mensagens;

Bem, até semana que vem, quando veremos mais sobre funções, praticamente “fechando” o assunto.

Uma aprofundada em operadores e lógica em C

OK.
Vamos dar uma pequena pausa para aprofudar uma teoria que é necessária para seguirmos em frente: os operadores.

Na nossa última “aula”, vimos um pouco sobre os operadores, em especial operadores matemáticos, lógicos e relacionais. Mas na verdade o C é composto por uma enorme gama de operadores, capazes de realizar muitas atividades. Aprender bem como usar operadores é algo importantíssimo para construir bons programas em C. Portanto, vamos dar uma pausa e reforçar essa teoria antes de seguirmos em frente.
A primeira coisa a entender é que temos vários tipos de operadores em C, que podemos dividir  em “grupos” para facilitar a compreenção:

  • Operadores de atribuição – são operadores usados para atribuir-se valores a variáveis;
  • Operadores aritméticos – com ele realizamos operações matemáticas simples usando dois valores, os operandos;
  • Operadores relacionais – comparam os valores de dois operandos, verificando se eles representam verdades ou falsidades lógicas naquele momento;
  • Operadores lógicos – comparam os valores de dois operandos em termos lógicos, ou seja, comparações baseadas na lógica booleana (E, OU e NÃO);
  • Operadores de bits – Permitem realizar operações diretamente nos bits de um determinado valor (em geral, inteiro);
  • Operadores compostos – são operadores complexos que podem combinar as funções de dois operadores do tipos acima;
  • Operadores especiais -são operadores usados no desenvolvimento da aplicação;

Além disso, existe uma ordem de prioridade dos operadores que iremos mostrar no final desse post. Você não precisa decorar essa tabela: você pode utilizar esse post como referência ou então facilmente pegar uma “cola” dessa tabela na Internet em vários sites de referência.
Bem, vamos começar então com o Operador de Atribuição.

Operador de atribuição – =

O operador de atribuição = serve para indicar ao C que desejamos armazenar (ou atribuir) a uma determinada variável um valor. Já vimos nos programas anteriores vários exemplos de atribuições, em especial quando inicializamos variáveis como fizemos no programa de Entrada de Dados. Uma coisa, porém, que é importante dizer é que o C permite fazer-se múltiplas atribuições, desde que os tipos sejam compatíveis. Por exemplo, o trecho de código abaixo:

int a,c;
float b,d;
a = b = c = d = 0;

É válido, uma vez que b e d, ainda que sejam do tipo float, podem receber valores inteiros (por causa do autocast – lembra que vimos quando mostramos o programa de Entrada de Dados?). Basicamente, esse comando faz a seguinte salada na atribuição:

  • Atribui 0 a d – como d é float, dá o autocast do 0 (que nesse caso é tratado como inteiro) para float;
  • Em seguida, atribui a c o valor de dd está inicializado como 0.0 (0 no tipo float). Ao receber o valor para c, o sistema faz o autocast do valor, truncando-o para 0 (inteiro);
  • Após isso, atribui a b o valor de c – repete-se o caso da atribuição para d;
  • E por fim, atribui a a o valor de b – repetindo o que aconteceu quando atribui-se a c o valor de d;

Importante notar que os autocasts são sempre executados no momento da atribuição do valor a uma variável. Por isso que, se dividimos dois inteiros e precisamos de um valor float, mesmo atribuindo o resultado, precisamos forçar o cast de um dos inteiros para float antes: se usarmos a divisão normalmente, ele irá tratá-la como divisão entre dois inteiros e dará um resultado inteiro, que será convertido depois da operação em float. Ao forçar um deles como float, indicamos que precisamos de um resultado em ponto flutuante e, dando o autocast no outro operando, o programa realizará uma divisão em ponto flutuante e retornará um valor float.
Aproveitando que entramos no assunto typecast, existe uma prioridade nos autocasts: isso é feito com o objetivo de impedir que os resultados percam valor (por exemplo, ao converter-se um valor de um tipo para um outro tipo cujo tamanho seja menor em bits e, portanto, capaz de representar uma gama inferior de valores). Tenha isso em mente ao recorrer ao autocast: prefira, se possível, fazer você mesmo o typecast, pois isso irá garantir que você sabe qual será o tipo resultante.
Dito isso, vamos para o próximo conjunto de operadores.

Operadores aritméticos

Já falamos sobre eles antes: são operadores que realizam contas matemáticas simples entre dois operandos (valores), “devolvendo” um deles como valor da expressão aritmética (em C, uma expressão aritmética tem o mesmo significado que na matemática, e o termo expressão é extrapolado a partir daí como a representação de uma operação lógico-matemática que será resolvida em um determinado momento). Esse valor pode ser usado em uma atribuição ou em qualquer lugar onde exija-se um valor. Isso é interessante pois pode-se executar operações matemáticas, por exemplo, antes de operações lógicas, obviamente respeitando as prioridades determinadas pelo C (veremos abaixo as prioridades e como alterar a ordem de execução das operações).
Basicamente, as operações matemáticas são:

Operador

*
/
%

+
Operação

Multiplicação
Divisão
Resto da Divisão Inteira (mod)
Subtração
Soma

Prioridade

1
1
1
2
2

Na coluna Prioridade, fazemos uma referência a como o C prioriza as operações aritméticas entre si. No caso, primeiro o C executa divisões, multiplicações e resto de divisões conforme apareçam da esquerda para direita (como é feito na matemática). Em seguida, o C executa somas e subtrações (também da esquerda para a direita, conforme a matemática). Existem formas de alterar a prioridade da execução de fórmulas complexas, que veremos mais abaixo. Mas, basicamente, essas são as operações matemáticas possíveis de serem feitas com o C. Uma ressalva: diferentemente de outras linguagens de programação, C não possui um operador matemático para potenciação.
Em C, a divisão é palco de uma controversa. Normalmente em C, é feita divisão inteira entre valores se ambos forem do tipo int, mesmo que a atribuição do resultado seja feita para uma variável de valor flutuante. Isso deve-se ao fato de que o C considera dois momentos: (1) quando a operação de divisão é feita e (2) quando o valor resultante é atribuido à variável de ponto flutuante. Porém, se no momento da execução da divisão, um dos valores for de tipo flutuante (seja por ser uma varíavel float ou por um cast), o C irá pegar o termo restante, forçar o cast (autocasting) do mesmo para um tipo flutuante e retornar o resultado da operação com um valor de tipo flutuante. Isso é algo a manter-se em mente.
O operando % (resto da divisão inteira) é um caso a parte: ele vai transformar ambos os termos em inteiros, uma vez que o conceito de resto só existe em matemática quando falamos de número inteiros (mesmo nos casos de dízimas periódicas ou não periódicas, a matemática parte do princípio que, cedo ou tarde podemos descobrir o “último dígito” de um valor). Essa conversão é feita truncando-se o valor decimal do número com tipo flutuante, tornando-o inteiro. Porém, isso pode provocar inexatidão em resultados, podendo afetar os resultados finais do programa. Para isso, o C oferece uma biblioteca especializada em funções matemáticas, math.h, sobre a qual ainda iremos falar de maneira mais aprofundada.
Bem, dito isso, vamos seguir falando dos operadores. No caso, vamos falar dos operadores relacionais.

Operadores relacionais

O nome meio que diz tudo: operadores relacionais envolvem a relação entre dois valores específicos de maneira lógica, comparando-os entre si e vendo se a relação em questão é verdadeira ou não. Basicamente eles devolvem 1 caso a relação seja verdadeira, e 0 caso a mesma seja falsa. (Lembrando que, como vimos na nossa última “aula” não existe no C um tipo booleano, então ele adota 0, “” (string vazia) ou o símbolo null como convenções para falso e quaisquer outros valores como verdadeiro). Para ter uma noção do funcionamento do mesmo, rode o seguinte programa (retirado do material do Curso de C da UFMG):

#include <stdio.h>
int main()
{
    int i, j;

    printf("\nEntre com dois numeros inteiros: ");
    scanf("%d%d", &i, &j);

    printf("\n%d == %d é %d\n", i, j, i==j);
    printf("\n%d != %d é %d\n", i, j, i!=j);
    printf("\n%d <= %d é %d\n", i, j, i<=j);
    printf("\n%d >= %d é %d\n", i, j, i>=j);
    printf("\n%d < %d é %d\n", i, j, i<j);
    printf("\n%d > %d é %d\n", i, j, i>j);
    return(0);
}

Não existe muito a ser dito sobre novos comandos como fizemos em outros casos. O importante aqui é estudar o comportamento dos operadores relacionais, que estamos listando abaixo, já indicando também sua prioridade entre si:

Operador


>= 

<=
== 
!=
Operação

Maior do que 
Maior ou igual a 
Menor do que 
Menor ou igual a 
Igual a 
Diferente de

Prioridade

1
1
1
1
2
2

Aqui voltaremos a enfatizar:

NÃO EXISTE TIPO BOOLEANO EM C! EM C, QUALQUER VALOR DIFERENTE DE 0 OU DA STRING VAZIA “” OU DE NULL É CONSIDERADO VERDADEIRO!
Além disso, vamos reenfatizar outra coisa: nunca confunda o operador relacional de igualdade (==) com o de atribuição (=). Isso irá provocar erros de lógica de aplicação que são de difícil depuração.
Pois bem, esses três primeiros tópicos, falando de operadores de atribuição, aritméticos e relacionais são apenas uma revisão do que vimos anteriormente. A partir de agora iremos ver operadores que vimos por alto ou não vimos anteriormente, a começar pelos…

Operadores lógicos

Operadores lógicos são basicamente operadores que realizam comparações. De certa forma, são um subgrupo dos operadores relacionais, mas que trabalham apenas com valores segundo a lógica booleana. De uma forma rápida, podemos dizer que eles podem comparar se dois valores são verdadeiros ao mesmo tempo (E/AND), se pelo menos um entre dois valores pode ser considerado verdadeiro (OU/OR) e se um determinado valor é falso naquele momento (NÃO/NOT). A vantagem de usar-se operadores lógicos é que, combinando-os com os operadores relacionais, podemos construir condições complexas, em especial em situações de controle de fluxo. Por exemplo, vamos relembrar um trecho de código de nossa última “aula”:
      do
        {
          printf(“Quantas tentativas você acha que precisa para descobrir ele? “);
          scanf(“%d”,&maximoTentativas);

          if (maximoTentativas<1)
             printf(“Você precisa tentar ao menos uma vez! 😛 \n”);
          if (maximoTentativas>limiteTentativas)
             printf(“%d tentativas? Tá querendo demais também! 😛 \n”,maximoTentativas);
        } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

Relembrando: o do…while executa enquanto a condição indicada no final do bloco de código for considerada verdadeira. No caso, nossa intenção foi garantir que o usuário não entrasse com nenhum número negativo e nem fosse além de um limite de tentativas estipulado previamente. Para isso, utilizamos duas condições relacionais (maximoTentativas<1 e maximoTentativas>limiteTentativas), cada uma atuando em uma das situações que determinamos. Para “unirmos” as duas situações, usamos o operador lógico OU (|| em C) que indica que, enquanto pelo menos uma dessas condições relacionais for verdadeira, a condição lógica será verdadeira e, portanto, o programa permanecerá executando esse bloco de código.
C implementa os principais operadores relacionais, que são o E (AND), OU (OR), e NÃO (NOT), conforme representado abaixo:

Operador

!
&&
||
Operação

NÃO Lógico
E Lógico
OU Lógico

Prioridade

1
2
3

O programa abaixo, também retirado do material do Curso de C da UFMG poderá ilustrar melhor o comportamento de E, OU e NÃO:

#include <stdio.h> <br />int main()<br />{<br />   int i, j;<br />   printf("informe dois números(cada um sendo 0 ou 1): ");<br />   scanf("%d%d", &i, &j);<br />   printf("%d AND %d é %d\n", i, j, i && j);<br />   printf("%d OR %d é %d\n", i, j, i || j);<br />   printf("NOT %d é %d\n", i, !i);<br />}<br />

Na verdade, com o tempo esse conceito ficará bem arraigado na cabeça. Então vamos para os próximos operadores.
Antes, um parênteses: em vários momentos, e em especial quando falamos dos tipos de dados em C, mencionamos que em C, as strings de caracteres possuem um comportamento diferenciado em relação ao que vemos em outras linguagens de programação. Por isso, você NÃO DEVE utilizar os operadores lógicos e relacionais apresentados anteriormente com strings. Isso irá provocar certamente resultados espúrios. Para comparações envolvendo strings, a biblioteca padrão string.h oferece uma série de funções úteis, sobre as quais falaremos no futuro.

Operadores de bit (bitwise)

O C oferece um conjunto de operadores lógicos voltados exclusivamente para o “acesso direto” aos bits. Mais exatamente, para realizar operações lógicas com bits. Chamamos esses operadores de operadores de bit, bit-a-bit ou bitwise, dependendo do autor do livro.
C foi uma linguagem originalmente projetada para facilitar o desenvolvimento de aplicações em baixo nível, como sistemas operacionais, firmware (software de baixo nível para dispositivos eletrônicos, como celulares e players de DVD), compiladores, com o objetivo de substituir o Assembler e oferecer um mínimo de portabilidade de código, ainda que mantendo os benefícios do Assembler de acesso direto ao hardware. Como o software de um appliance, o firmware, se comunica com o hardware por pulsos eletrônicos interpretados pela CPU como bits 0s e 1s específicos em determinados locais de memória, é interessante que C possua comandos capazes de trabalhar com esses bits, que chamamos em programação de flags. Para isso, utiliza-se os operadores de bit.
Os operadores de bit lembram um pouco os operadores aritméticos e um pouco os operadores lógicos, pois eles irão realizar a operação lógica em cada bit de um determinado número, usando outro, e retornando um terceiro. Por exemplo, imaginemos que precisamos obter de um conjunto de flags (normalmente representados por um int) se o 5° bit dessa conjunto de flags está ativo. Uma forma é aplicar o bitwise AND (E bit-a-bit) contra o número 8 (representado 00001000). Esse valor (que alguns autores chamam de bitmask – máscara de bits) atua de tal forma que zera todos os outros bits do int em questão: lembrando que ambos os bit tem que ser 1 – verdadeiro – para que o bit na saída seja 1. Como os demais bits no bitmask são 0, o valor de tais bits é zerado. Se o bit na flag estiver zerado, o resultado será 0, caso contrário o valor de saída será 8.
Existem também operadores de movimentação de bits: algumas operações matemáticas e lógicas são resolvidas facilmente quando utilizamos essa movimentação de bits, “empurrando” os bits à esquerda ou direita, com isso aumentando e diminuindo seus valores. Como exemplo: uma forma “rápida e suja” de fazer-se a potência de 2 em um número é “empurrando” seus bits uma casa para a direita. Como em numeração binária um bit é sempre uma potência de 2 acima do bit à direita ao seu, ao empurrar-se os bits à direita temos o efeito da potência de 2. (PS: esse método não faz as checagens matemáticas de casos especiais como potências de 0 e 1 e nem verificam estouros do tamanho de variável). Ao empurrar-se os bits, o último bit na direção para a qual os bits foram “empurrados” é eliminado e o primeiro bit da direção oposta é preenchido com 0.
Os operadores de bit são:

Operador

!
<<
>>
&
^
|
Operação

NÃO por bit a bit
Deslocamento de bits à esquerda
Deslocamento de bits à direita
E bit a bit
OU Exclusivo (um e apena um bit)
OU bit a bit

Prioridade

1
2
2
3
4
5

Uma coisa a salientar de diferente entre os operadores bit-a-bit e os operadores lógicos é a existência do OU-Exclusivo (eXclusive OR, XOR) bit-a-bit. Nessa situação, ao comparar-se os dois números bit a bit, um bit na saída só será um se UM E APENAS UM dos bits comparados for 1. Caso contrário (ambos 0 ou ambos 1) o bit será zero.
Bem, esse é um tema complexo e pouco útil para nós. Vamos falar agora de…

Operadores Compostos

Operadores Compostos englobam operadores que atuam ao mesmo tempo como dois tipos diferentes de operadores, em geral uma operação bit-a-bit ou aritmética e uma atribuição. Além desses, existe o operador de atribuição condicional ?.
Vamos englobar em três subgrupos os operadores compostos: os de atribuição “aditiva”, os de incremento e decremento em um e o de atribuição condicional.
Os de atribuição “aditiva” são operadores que realizam uma determinada operação envolvendo um parâmetro com uma variável e atribuem à mesma variável o resultado da operação. Por exemplo, a expressão a+=4 é equivalente em termos de programação a a=a+4. Esses operadores existem para ambientes onde o espaço para a leitura do código fonte é pequeno e determinadas redundâncias de digitação comprometeriam a compilação. Alguns autores consideram que o uso desse tipo de operador torna o programa mais legível, enquanto outros defendem que esse tipo de operador provoca a ofuscação do código (ou seja, torna a leitura do código complexa). A lista dos operandos compostos segue abaixo:

Operador

/=
*=
%=
+=
-=
<<
>>
&
^
|
Operação

Divisão com atribuição
Multiplicação com atribuição
Resto da divisão inteira com atribuição
Soma com atribuição
Subtração com atribuição
Deslocamento de bits à esquerda com atribuição
Deslocamento de bits à direita com atribuição
E bit a bit com atribuição
OU Exclusivo (um e apena um bit) com atribuição
OU bit a bit com atribuição

Prioridade

1
1
1
2
2
3
3
4
4
4

Esse é um subtipo complexo, e o próximo também demanda atenção, que são os operadores de incremento e decremento em um. Na verdade, esses operadores são indicados por ++ e e indicam que a variável onde eles estão sendo usados terá seu valor somado ou subtraído em 1. Porém, existe um comportamento que deve ser levado em conta ao usar-se tais operadores, que é o de prefixação e pós-fixação. Para usar-se os operadores desse tipo, você pode colocar o operador antes ou depois da varíavel. A posição onde o colocar, porém, irá afetar o comportamento sobre qual será o valor realmente usado.
Ao prefixar o operador antes da variável (ou seja, colocar ++ ou antes da variável), o valor será modificando antes de qualquer uso. Por exemplo, se você usar a=++b, e b for 4, acontecerá o seguinte: (1) b será incrementada em 1, para 5 e (2) a receberá o novo valor de b, 5. Ao pós-fixar o operador, acontecerá o contrário: o valor será usada para qualquer outro fim antes de ser modificado. Mantendo o exemplo anterior, imagine que você, ao invés de usar a=++b, utilizou a=b++. Nesse caso (1) a receberá o valor atual de b, 4 e depois é que (2) b será incrementado em 1. Essa diferença é importante de ter-se em mente, pois ela pode provocar bugs (em especial quando utiliza-se esse operador em conjutnto com comparações relacionais), mas é uma ferramenta muito poderosa.
Por exemplo: nas “brincadeiras” da nossa última “aula” sugeri:

  • Da mesma forma que existe o operador ++, existe o operador , que subtrai um da variável que o antecede. Considerando isso e o funcionamento do laço for, tente reconstruir o laço para não ter uma condição no sentido exato da palavra. Dica: lembre-se que C trata 0 como falso;

A forma para fazer isso é lembrar que o for, na comparação, exige um “booleano” verdadeiro (ou seja, qualquer valor diferente de 0). Portanto, se usarmos o operador de decremento em 1 pós-fixado, podemos fazer com que o número de tentativas vá se reduzindo até que, na última tentativa, ele chegue na condição em 0 e, sendo “falso”, saia do laço. Portanto, basta substituir a linha:

for (i=1; i<=maximoTentativas;i++)

no programa da nossa última “aula” por:

for (i=1; maximoTentativas–; i++)

e teremos feito a mágica. Na realidade, se não usarmos a variável i para exibir o número da tentativa, podemos simplesmente reduzir o comando para:

for (; maximoTentativas–;)

e o código terá o mesmo comportamento, sem precisar de uma variável de controle.

Mas, ATENÇÃO: isso só funcionará bem se você usar o decremento pós-fixado. Se tentar prefixar o decremento, ele irá, na prática, tirar uma tentativa do usuário, ao ponto de, se o usuário pedir apenas uma tentativa o sistema nem pedir o número: no caso, ele irá subtrair 1 de maximoTentativas (que será 1), reduzindo-o para 0 e tornando a condição “falsa”.
Bem, aqui podemos dizer que terminamos de falar do incremento e decremento de um. Vamos falar, para acabar com os atributos compostos, da atribuição condicional.
Chamamos de atribuição condicional um operador que funciona como o seguinte tipo de lógica:

if (condicao)
   var=x;
else
   var=y;

O C oferece um operador que permite-nos fazer esse tipo de condição diretamente na atribuição, o operador ?. Esse é um operador ternário (ou seja, exige três operandos), sendo composto por <condicao>?<atr_verdadeiro>:<atr_falso>. Nesse caso, caso condicao seja verdadeira (diferente de 0), o operador irá devolver atr_verdadeiro, enquanto que, caso condicao seja falsa (igual a zero), ele irá devolver o valor de atr_falso. Por exemplo, imagine que você irá precisa do módulo de um número, ou seja, do valor sem sinal do mesmo. Ao invés de construir um if só para isso, você pode usar uma atribuição condicional:

modulo=(numero<0)?numero*-1:numero;

O que quer dizer que modulo irá receber o valor de numero vezes -1 caso numero seja menor que 0; caso contrário, ele irá receber o valor normal de numero.

Controle de Fluxo e Operandos Lógicos

Olá!
Vamos continuar então a programar em C.

Os dois primeiros programas, Hello World e o de Entrada de Dados, tinham como ponto comum o fato de serem, vamos dizer assim, diretos. Eles eram executados, realizavam cada instrução em sequencia e se encerravam.
Isso em si não é ruim: muitos programas simples e interessantes podem ser feitos assim. Porém, é perceptível que eles não possuem nenhuma “inteligência”: eles não conseguem executar instruções dependendo das entradas que lhe são oferecidas. Isso se deve ao fato de eles não possuírem nenhum controle de fluxo. Em programação, esse termo é adotado para definir situações onde o programa pode mudar a sequencia de execução do programa conforme condições estipuladas no momento do desenvolvimento da aplicação. Por exemplo: no programa de Entrada de Dados, poderíamos estipular valores limite para primeiro e segundo que não os limites do tipo int.
Pois bem, veremos agora um programa com um pouco mais de “inteligência”, pois iremos falar mais sobre os comandos de controle de fluxo em C e sobre os operadores lógicos, além de questões sobre o tipo booleano (ou melhor, sobre a ausência de um tipo booleano) em C.
No caso, vamos escrever um outro programa comum, o “adivinhe o número”. Mas vamos adicionar alguma “inteligência” a ele e tornar ele um pouco mais desafiador:

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

/* Definindo o número limite para a pessoa tentar descobrir */

#define LIMITE 10
#define ENCERRA printf(“Pressione qualquer tecla para continuar!\n”); while(!getchar());

int main (int argc, char** argv)
{
  const int
limiteTentativas=(LIMITE/2)-1;
  int numeroPensado=0, numeroDoUsuario=0, maximoTentativas=0,i;
 
  srand(time(NULL)); // Serve para modificar a tabela de números pseudo-aleatórios
  numeroPensado=rand()%LIMITE+1;

  printf(“OK! Pensei em um número entre 1 e %d e te desafio a achar ele!\n”, LIMITE);
  do
    {
      printf(“Quantas tentativas você acha que precisa para descobrir ele? “);
      scanf(“%d”,&maximoTentativas);

      if (maximoTentativas<1)
         printf(“Você precisa tentar ao menos uma vez! :P\n”);

      if (maximoTentativas>limiteTentativas)
         printf(“%d tentativas? Tá querendo demais também! :P\n”,maximoTentativas);
    } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

  for (i=1; i<=maximoTentativas;i++)
    {
      printf(“Vamos lá! %da. tentativa! “,i);
     
      scanf(“%d”,&numeroDoUsuario);

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
         else
         {
           printf(“Não pensei em %d. “, numeroDoUsuario);
           if (numeroDoUsuario<numeroPensado)
              printf(“Pensei em um número maior.\n”);
           if (numeroDoUsuario>numeroPensado)
              printf(“Pensei em um número menor.\n”);
         }
    }

    printf(“Que pena! Eu pensei em %d! Melhor sorte da próxima vez! \n”,numeroPensado);
    ENCERRA
    return(0);
}

Como de costume, iremos ignorar algumas coisas que já vimos anteriormente. Portanto, se você ficar com alguma dúvida, dê uma relida nos posts anteriores para fixar bem o conteúdo que já lidamos. No caso, vamos falar sobre os trechos de código que estão com destaque colorido no fonte àcima.

#define e os “números mágicos”

Logo de cara incluímos duas novas bibliotecas em C: stdlib.hSTanDard Library (biblioteca padrão), que contêm uma série de funções cotidianas em C – e time.h, que contem funções para lidar com tempo. Dessas, a stdlib.h em especial é muito importante, pois uma grande quantidade de comandos C muito usados está incluída nela. No caso, estamos muito interessados nas funções relacionadas à geração de números randômicos, mas existe muito mais nela e no futuro falaremos muito sobre essa biblioteca.
Logo após, vemos dois novos comandos do pré-processador (ou macros, como são chamados tecnicamente):

#define LIMITE 10
#define ENCERRA printf(“Pressione qualquer tecla para continuar!\n”); while(!getchar());

A macro #define permite ao programador definir um símbolo que o pré-processador irá substituir por uma informação qualquer no momento da compilação. No caso, definimos dois símbolos:

  • LIMITE – um número com valor 10;
  • ENCERRA – uma sequencia de comandos C;

No caso, você pode se perguntar sobre a utilidade de definir-se símbolos. Esse sistema de símbolos permite:

  1. Criar “comandos” simples (caso do ENCERRA);
  2. Criar situações de compilação condicional (um tópico mais avançado, veremos no futuro);
  3. Evitar os chamados “números mágicos”;

“Números mágicos?! Virou Hogwarts agora?”
Na verdade não. Em programação, chamamos de números mágicos determinados números ou expressões que são colocadas no código para realizar determinadas funções. No nosso caso, precisamos limitar o maior número no qual nosso programa irá “pensar”. Poderíamos simplesmente escrever ele no nosso programa de maneira direta, mas como esse número iria se repetir várias vezes em vários trechos de código aparentemente sem relação um com o outro, com certeza a leitura e análise de erros do programa (que em programação é chamada de depuração) seria complicada. Por isso chamamos tais números de números mágicos: uma vez que ele entra, temos dificuldades para entender o que eles fazem.
No caso, ao criarmos um símbolo (LIMITE), podemos usar ele no nosso código que o próprio compilador (através do pré-processador) irá substituir todas as entradas do símbolo LIMITE pelo valor desejado (no caso, 10). Isso torna o programa mais legível e mais simples de ser modificado.
O mesmo vale para ENCERRA. Com ENCERRA, criamos um “pseudo-comando”, um símbolo que será substituído por uma série de comandos C. No caso, o comando escrito em ENCERRA ajuda alguns usuários de IDEs (em especial no Windows) a visualizarem os resultados dos seus programas.
Existe mais sobre #define que ainda iremos ver, em especial na questão dos “pseudo-comandos”, mas esse básico deve ajudar a compreender melhor as coisas até termos uma oportunidade de nos aprofundarmos nele (em especial, quando falarmos no futuro de outras macros de pré-processador, quando revisitaremos #include e #define).

Variáveis Constantes:

Após as macros temos uma declaração de variáveis: logo de cara, temos uma que é um pouco diferente do que o que vimos quando falamos dos tipos de variáveis e como as declaramos:

  const int limiteTentativas=(LIMITE/2)-1;

A principal diferença está na palavra reservada const antes da declaração do tipo int da variável limiteTentativas. O que essa palavra faz é indicar ao compilador que essa variável na verdade é uma constante do tipo desejado, portanto não poderá mais ser modificada uma vez que seja inicializada, o que acontece em seguida. No caso, a variável constante limiteTentativas tem seu valor inicializado com o resultado de (LIMITE/2)-1, ou seja, na divisão do valor do nosso símbolo LIMITE por 2, menos 1 (no caso atual, o valor final é 4).
Você deve estar se perguntando: “usar um símbolo com #define não seria mais interessante?”
Bem, nesse caso não. Perceba que o limite de tentativas é determinado baseando-se no valor de LIMITE, e o #define não realiza per se contas ou qualquer processamento: tudo o que ele faz é dizer que (1) existe um símbolo e (2) qual seu valor. Se modificarmos o valor de LIMITE, teríamos que modificar o valor desse novo #define manualmente.
Você pode pensar então: “por que não tornar a fórmula um símbolo via #define?”. A ideia parece boa, mas ela cai em um problema: quando você substituir o símbolo, você obrigaria o sistema a repetir várias vezes uma determinada conta. Podemos pensar em desktops com gigas de poder de processamento e isso aparentemente ser uma boa ideia, mas pense em um sistema embarcado e você verá que uma situação como essa iria provocar redução na velocidade do programa.
Já com a variável constante, temos uma situação onde a variável não poderá ser modificada mas ainda assim será calculada apenas uma vez, apenas exigindo uma recompilação no caso de uma mudança de LIMITE (o que iria acontecer de qualquer maneira), pois ao compilar, LIMITE seria substituído pelo seu valor.
“Eu tenho que OBRIGATORIAMENTE inicializar uma variável constante no momento em que a declaro?”, você deve estar se perguntando. Na realidade, da mesma forma que ocorre com as variáveis comuns, as variáveis constantes PODEM ser inicializadas após a declaração. O que é impedido é que uma variável constante receba OUTROS VALORES após a inicialização. Como constantes são normalmente usadas para delimitar de alguma forma o comportamento do programa (no nosso caso, utilizaremos limiteTentativas para delimitar o máximo de tentativas que uma pessoa terá de adivinhar o número “pensado” pelo computador), é uma boa prática inicializá-la antes de qualquer entrada de dados.
Bem, acho que fechamos aqui a questão das variáveis constantes, à exceção de uma coisa: por que logo abaixo vem uma segunda linha de declarações int?
Quando você coloca várias declarações e/ou inicializações de variáveis em uma mesma linha, o compilador irá entender que todas elas tem o mesmo comportamento. Se colocássemos as int em questão junto com a declaração const int de limiteTentativas, essas variáveis também seriam declaradas constantes, o que não é o comportamento desejado. Nesse caso, a regra é clara: declarações de constantes devem ser separadas das declarações de variáveis comuns.
Bem, agora que vimos o bastante sobre constantes para entendermos o programa, sigamos em frente.

rand(), srand() e o gerador de números pseudo-aleatórios:

Vamos dar uma olhada nesse bloco de código:

srand(time(NULL)); // Serve para modificar a tabela de números pseudo-aleatórios
numeroPensado=rand()%LIMITE+1;

A primeira linha utiliza duas funções: a primeira é srand(), da stdlib.h, que recebe um long int como entrada. Essa função serve para modificar a tabela de números pseudo-aleatórios do sistema, usando o long int de entrada como parâmetro para essa modificação. No caso, usamos a função time(NULL), da biblioteca time.h, que devolve o horário do sistema atual em UNIX TimeStamp (o número de segundos passados desde as 0:00 do dia 01/01/1970 até o momento atual). Na verdade, ela retorna o epoch time (outra forma pela qual pode ser chamado o UNIX TimeStamp) para qualquer data passada de maneira correta (não entraremos nesse detalhe aqui).
Por que chamamos os números do sistema de pseudo-aleatórios?
Porque o computador não consegue, pela própria natureza lógica, gerar um padrão 100% aleatório. O que pode ser feito é utilizar-se algoritmos que gerem números, considerando-se a probabilidade de sua ocorrência em sequencia, que se aproximem da aleatoriedade e montar uma tabela com ela. O problema é que, ao se “pegar” um número nessa tabela é que, caso ela não seja “inicializada” de maneiras diferentes entre cada execução, o valor deixará de ser aleatório, ainda que continue sendo não-determinístico (ou seja, você não consegue, a partir das sequencias de números, determinar com precisão as fórmulas matemáticas que as geraram). Para evitar esse problema, utilizamos um comando, o srand(), que irá modificar a semente da tabela de números pseudo-aleatórios, o que irá garantir que, a cada execução, como o Timestamp será diferente, a semente da tabela será diferente e, por sua vez, a saída da mesma será diferente (se quiser saber mais sobre a idéia de geração de números pseudo-aleatórios, dê uma olhada nesse artigo da Wikipedia). De qualquer modo, sabemos que o srand() irá garantir que os números “aleatórios” tenham esse comportamento o mais próximo possível da realidade.
Na linha seguinte, utilizamos rand() para obter um número aleatório. Porém, esse comando não nos permite definir qual o valor máximo a ser obtido, normalmente obtendo qualquer valor entre 0 e uma definição chamada RAND_MAX (normalmente o limite de um unsigned int). A opção padrão é obter o resto da divisão inteira entre o maior número que desejamos e o valor retornado. Nesse caso, obtemos um número que vai de 0 ao limite estipulado menos 1. No nosso caso, queremos um número entre 1 e o LIMITE estipulado, por isso adicionamos 1 ao valor obtido (lembre-se da prioridade de execução da operação de resto em relação à soma).
Bem, acho que já falamos demais sobre rand(), até porque não tem como aprofundar nesse caso sem entrar uma teoria extremamente complexa e totalmente fora do nosso escopo atual (e muito provavelmente futuro). Tudo que é preciso saber é que precisaos usar srand() antes de rand() para garantir o comportamento aleatório e que precisamos utilizar a operação resto para “limitar” o valor de rand().

Laço de repetição condicional – do {…} while e operações relacionais e lógicas:

Em seguida, vemos que ele irá apresentar o desafio ao usuário e entrará no bloco de código abaixo:

  do
    {
      printf(“Quantas tentativas você acha que precisa para descobrir ele? “);
      scanf(“%d”,&maximoTentativas);

      if (maximoTentativas<1)
         printf(“Você precisa tentar ao menos uma vez! :P\n”);
      if (maximoTentativas>limiteTentativas)
         printf(“%d tentativas? Tá querendo demais também! :P\n”,maximoTentativas);
    } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

No nosso caso, nos focaremos nos comandos em ciano. Eles representam um tipo de controle de fluxo que chamamos de repetição (ou iteração, em alguns lugares) condicional. O nome pomposo apenas quer dizer que o laço em questão vai ser executado enquanto uma determinada condição for cumprida. No nosso caso, esse comando funciona assim:

  do
    {
       <…>
    } while (<condicao>);

O que fazemos aqui é indicar que, enquanto a condicao estipulada for verdadeira, o programa continuará no laço.

Mas como construir uma condição lógica? Para isso, C nos fornece os operadores lógicos, que comparam valores e retornam “verdadeiro” ou “falso” conforme a situação.
Aqui cabe um parenteses antes de entrarmos nos operadores: o C não possui um tipo booleano (ou seja, verdadeiro ou falso) específico. Para o C, um valor 0, NULL (que é definido por padrão como 0) ou “” (string vazia) como falso e qualquer outro valor como verdadeiro. Isso é importante pois existe um bug em C muito comum que iremos discutir adiante.
Bem, dito isso, os operadores lógicos em C padrão são: 

Operador  

>= 

<= 
== 
!=
&&
||
!
Ação  
Maior do que
Maior ou igual a
Menor do que 
Menor ou igual a 
Igual a 
Diferente de
E lógico
OU lógico
NÃO lógico

(fonte: curso de C da UFMG)
Os operadores lógicos são escritos de maneira similar aos matemáticos, sempre comparando um valor a outro. No caso, por exemplo, do comando acima, temos três expressões:

  • primeiro, uma comparação maximoTentativas<1;
  • depois, uma comparação maximoTentativas>limiteTentativas;
  • e por fim, uma operação lógica ou entre os dois;

Como isso é resolvido no C? Primeiro, o sistema irá analisar se o maximoTentativas<1. Caso seja, ele nem irá fazer a segunda comparação maximoTentativas>limiteTentativas, pois o fato de a comparação lógica ser OU vai fazer o programa aceitar como verdadeira a condição e continuará no laço (chama-se a isso short-circuit logical analysis – análise lógica de curto-circuito, e parte do princípio que uma vez que tenhamos garantido que uma condição é sabidamente verdadeira ou falsa, não há necessidade de continuar-se processando a lógica em questão). Caso contrário, ele irá tentar a segunda comparação, maximoTentativas>limiteTentativas, para verificar se ela é verdadeira ou falsa e, portanto, a condição o será.
Parece meio confuso, mas é só pensar com calma que você irá entender. Procure imaginar valores para maximoTentativas e pense em qual será a resposta (lembre-se: no caso atual, limiteTentativas é 4).
O que acontecerá se o valor não fizer nenhuma das duas condições estipuladas ser verdadeira? Simples: a expressão como um todo será falsa (0) e o laço do…while irá se encerrar normalmente, com o programa seguindo adiante. Ou seja, somente quanto o usuário entrar com um número máximo de tentativas maior que 1 e menor ou igual que o limite de tentativas estipulado (no caso, 4), o laço será interrompido.
Temos uns ifs engraçadinhos dentro desse laço, mas não falaremos sobre ele agora. Vamos então seguir em frente.
Para fecharmos esse tópico, existe uma versão do do…while chamada simplesmente while. Ela é representada assim:

while(<condicao>)
{
    <…>
}

e sua única diferença em relação ao seu “irmão” é que, caso ao chegar na entrada do laço a condição for satisfeita, o sistema sequer irá entrar no bloco de código em questão. O do…while irá executar ao menos uma vez, uma vez que a condição é testada no momento da saída do laço, diferentemente do while, que é executada quando da entrada no laço.

Iterações – o comando for:

Após nosso usuário ter determinado quantas tentativas ele quer ter para acertar o número mágico, vamos então dar a chance a ele.

  for (i=1; i<=maximoTentativas;i++)
    {
      printf(“Vamos lá! %da. tentativa! “,i);
     
      scanf(“%d”,&numeroDoUsuario);

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
         else
         {
           printf(“Não pensei em %d. “, numeroDoUsuario);
           if (numeroDoUsuario<numeroPensado)
              printf(“Pensei em um número maior.\n”);
           if (numeroDoUsuario>numeroPensado)
              printf(“Pensei em um número menor.\n”);
         }
    }

Aqui temos um número delimitado de tentativas por máximo de tentativas. Desse modo, podemos utilizar um outro comando de laço, o for, que é um outro caso de laço de repetição ou iteração. Diferentemente do do…while, porém, o for é uma iteração não-condicional. Na verdade, ele é condicional, mas ele é mais usado em situações na qual se espera que ele se execute um determinado número de vezes (pode-se usar ele até como um while diferente, mas não é boa prática e não falaremos sobre isso aqui).
O for tem como estrutura a seguinte:
  for (<inicializacao>; <condicao>; <iteracao>)
    {
       <…>
    }

No caso, ele é um pouquinho complexo, portanto vamos dar uma olhada no seu comportamento:

  • no momento em que o programa chega no for, a primeira coisa que é feita é executar os comandos em <inicializacao>. No nosso caso, ele inicializa a variável i em 1. Essa inicialização pode ser feita como desejado, mas restringindo-se a um comando (ou bloco de código);
  • Em seguida, o bloco de código do for será executado;
  • Ao terminar de executar-se o bloco de código do for, ele executa o comando que está em <iteracao>. No nosso caso, utilizamos um operador matemático ++, que é utilizado em C para adicionar-se um ao valor da variável ao qual ela sucede. Na verdade ele se comporta de uma maneira mais complexa, mas para o momento basta entender que i++ seria o equivalente a i=i+1;
  • Depois de executar a <iteracao>, o sistema irá verificar se a <condicao> colocada é verdadeira. Caso o seja, ele irá interromper o laço for da mesma forma que o while, caso contrário ele entrará e executará uma nova iteração. No nosso caso, testamos se i ainda é menor ou igual ao número de limiteTentativas. Lembrando que C não possui tipo booleano e que basta que o valor seja 0 ou “” para ser considerado falso e, portanto, o laço for seja interrompido;

Bem, acho que isso deve ter deixado claro como o for funciona. O primeiro printf do programa deve deixar claro que você vai passando por várias iterações até acertar o “número mágico” “pensado” pelo nosso programa (o do rand() do início do programa). Mas como o programa saberá quando fomos bem-sucedidos?
Para isso existe o nosso próximo comando.

Execução condicional – o comando if:

OK… Paramos falando sobre como o nosso programa saberá que fomos bem sucedido. Veja que temos um scanf lendo o que o personagem entrou naquela tentativa. Precisamos de um comando para decidir fomos bem sucedidos ou não em acertar a descoberta do número “mágico”  “pensado” pelo programa. Quem cuida disso é comando if do bloco abaixo:

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
      else
      {
         printf(“Não pensei em %d. “, numeroDoUsuario);
         if (numeroDoUsuario<numeroPensado)
            printf(“Pensei em um número maior.\n”);
         if (numeroDoUsuario>numeroPensado)
            printf(“Pensei em um número menor.\n”);
      }

O if é uma estrutura importante de programação, pois ele executa blocos de código caso a condição determinada seja verdadeira:

      if (<condicao>)
      {
        <…>
      }
      else if (<outra_condicao>)
      {
        <…>
      }
      else
      {
        <…>
      }

O if irá executar o bloco de código abaixo da instrução caso condicao seja verdadeira. Caso contrário ele pode:

  1. Testar outras condições. Para isso, utiliza-se else if, com uma nova condição e um bloco de código adequado;
  2. Executar um código para exceção. Para isso, utiliza-se else e o bloco de código adequado;
  3. Não fazer nada;
Agora, vamos tentar ler esse trecho de código. A primeira coisa que ele testa é se numeroDoUsuario==numeroPensado, ou seja, se o número que o usuário entrou é igual ao número “pensado” pelo sistema. Caso seja, ele indica que foi bem sucedido e encerra o mesmo (perceba que temos um return(0) dentro do if. Isso é permitido pelo C). Perceba que o operador de relação de igualdade é ==. NUNCA CONFUNDA COM O OPERADOR DE ATRIBUIÇÃO, =. Esse é um bug (falha de programação) muito comum que é provocado por falta de atenção na programação. No caso de colocar =, o valor de numeroDoUsuario seria substituído pelo de numeroPensado. A não ser que numeroPensado fosse 0 (o que é impossível, devido à lógica do programa) o programa acusaria verdadeiro independentemente do valor realmente ser (lembre-se, qualquer valor diferente de 0 ou “” ou NULL é considerado pelo C como verdadeiro em relações lógicas).
Bem, dito isso, vamos continuar a analisar nosso programa. Caso a condição acima não seja verdadeira, ele irá ignorar o bloco abaixo de if e irá ver que temos um else com um bloco de código. Ele então entrará nesse bloco de código. A primeira coisa que ele irá fazer é dizer que não pensou no numeroDoUsuario e testar se numeroDoUsuario<numeroPensado. Caso seja verdadeiro, ele irá imprimir na tela uma mensagem dizendo que pensou em um número maior que o que o usuário entrou. Caso contrário, irá verificar se numeroDoUsuario>numeroPensado. Se verdadeiro (provavelmente será, se tudo mais deu errado), irá imprimir que pensou em um número menor que o que o usuário pensou.
Os ifs dessas linhas possuem uma característica interessante que é o fato de terem comandos diretamente abaixo deles, e não dentro de blocos de código. Na realidade isso deve-se ao fato que if, while, for e afins, todos eles possuem uma característica de executar na realidade apenas um comando. A diferença é que, para o compilador C, um bloco de código (lembre-se: blocos de código são comandos isolados pelos colchetes {}) são considerados comandos isolados. Portanto, um bloco de código == um comando.
Lendo com calma o código você irá compreender muito bem o mesmo. Leia e releia o códigoo e faça exercícios mentais para entender o código do if.

Quando tudo dá errado – considerações:

OK… Mas e caso o usuário não acerte mesmo depois de ter tentado o número de vezes que ele desejou (maximoTentativas).
Isso ocorre quando a variável i, após a iteração do for, ficar com um valor maior que maximoTentativas, tornando a condição  i<=maximoTentativas falsa e saindo do laço for (ao atingir o limite de iterações estipulado). Para encerrar o programa, nós fazemos o sistema imprimir uma mensagem dizendo qual o número no qual ele pensou e saindo do mesmo.
Antes de irmos para as “brincadeiras” finais, algumas considerações:

  • Os operadores relacionais que mostramos anteriormente só servem para valores numéricos ou para o tipo char. Em especial para strings, a biblioteca string.h traz funções que nos permite testar igualdade entre textos. Veremos eles melhor no futuro;
  • Ainda não esgotamos o assunto controle de fluxo. Em especial falaremos ainda sobre um tipo de execução condicional para múltiplos valores e sobre como “quebrar” laços como o for e o while;
  • É possível aninhar ifs ccom múltiplos else if e afins. Porém, tome cuidado ao usá-los: além de tornar o código de difícil leitura, você pode ter problemas dependendo do compilador (normalmente o C exige no mínimo 8 “níveis” de ifs aninhados, mas esse valor não é obrigatório);

Bem, dito isso, vamos fazer algumas sugestões para “brincadeiras” com o nosso código:

  • Comente ou remova o srand() e veja o comportamento do gerador de números pseudo-aleatórios;
  • Tente reescrever o código para tornar maximoTentativas uma constante. Lembre-se que não é necessário inicializar o valor imediatamente, podendo ser inicializado em um momento futuro;
  • No caso de quando o usuário erra o número “pensado”, você pode ter percebido que os dois ifs são redundantes. Tente reescrever o código usando apenas um if;
  • Da mesma forma que existe o operador ++, existe o operador , que subtrai um da variável que o antecede. Considerando isso e o funcionamento do laço for, tente reconstruir o laço para não ter uma condição no sentido exato da palavra. Dica: lembre-se que C trata 0 como falso;
  • Para ter uma idéia do problema que pode ocorrer quando se confunde o operador de igualdade com o de atribuição, modifique o código removendo um dos sinais de igual de if (numeroDoUsuario==numeroPensado) e veja o que acontece com o programa. Para maior clareza, coloque um printf que apresente o número “pensado” antes do usuário entrar o seu número e digite outro completamente diferente. Coloque também um printf mostrando o numeroDoUsuario após o bug;
  • Para entender a questão das definições, altere o valor de LIMITE e veja como o programa se comporta. Como dica, o cálculo do limiteTentativas baseia-se na idéia de buscar-se o número sempre indo na metade do que é válido. Por exemplo: computador escolhe 6. Na primeira interação, tento 5, e ele me fala que pensou um maior. Tento 7 (metade do bloco “acima de cinco”) e ele me diz que pensou um menor. Tento 6 e acerto;

Bem, semana que vem iremos reeforçar a teoria dos operadores (lista completa e exemplos) e daremos uma terminada na questão dos controles de fluxo. Até lá, divirtam-se!

Tipos de Dados e Variáveis

Olá a todos!

Devem ter “brincado” bastante com o HelloWorld.c, não? Então vamos começar agora a estudar de maneira mais aprofundada algumas coisas que já vimos nele. No caso, falaremos sobre os tipos de dados e variáveis. Portanto, vamos ver um programa um pouco mais complexo que o HelloWorld.c. Novamente digite o programa abaixo como ele está apresentado:

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

/*
* (c) 2010 GPL
*/

int main (int argc, char** argv)
{
  int primeiro=0, segundo=0, soma=0, resto=0, divisaoInteira=0;
  float divisaoFlutuante=0;
  char nome[80];
  char palavra[20];
  /*
  * O comando abaixo inicializa as strings nome e palavra
  */
  memcpy(nome,”\0”,sizeof(nome));
  memcpy(palavra,”\0”,sizeof(palavra));

  printf(“Olá! Digite seu nome, por favor!\n”);
  fgets(nome,sizeof(nome),stdin);

  printf(“Digite uma palavra que devo exibir e dois números para eu fazer umas contas.\n”);
  scanf(“%s %d %d”,palavra,&primeiro,&segundo);

  getc(stdin);

  soma=primeiro+segundo;
  divisaoInteira=primeiro/segundo;
  resto=primeiro%segundo;
  divisaoFlutuante=(float)primeiro/(float)segundo;

  printf(“Olá, %s\n”,nome);
  printf(“Você me pediu para exibir %s e os números %d e %d\n”,palavra,primeiro,segundo);
  printf(“Algumas continhas!\n%d+%d=%d\n”,primeiro,segundo,soma);
  printf(“%d:%d=%d(Resto %d)”,primeiro,segundo,divisaoInteira,resto);
  printf(” ou %7.2f\n”,divisaoFlutuante);
  printf(“Divertido, não? Aperte qualquer tecla para continuar…”);
  getc(stdin);
  return(0);
}

Como fizemos no HelloWorld.c, vamos dar uma “dissecada” no código. Porém, diferentemente do HelloWorld.c, não iremos explicar algumas partes do código, pois elas serão similares ao que vimos no Hello World. Perceba que o código guarda similaridades como a função main e o #include , portanto o que dissemos lá continua valendo. Vamos às explicações “únicas”.

Variáveis – declarando e inicializando:

Como você deve ter reparado, adicionamos um novo #include. No caso, estaremos incluindo a biblioteca string.h, que também é parte da biblioteca-padrão do C. string.h é uma biblioteca que é voltada, como o nome diz, à manipulação de strings, ou seja, cadeias de caracteres. Porém, como veremos logo, as strings em C são bem diferentes das suas similares em outras linguagens de programação, e seu comportamento deve ser bem avaliado.
Logo após main(), temos um trecho de código que não vimos no Hello World

int primeiro=0, segundo=0, soma=0, resto=0, divisaoInteira=0;
float divisaoFlutuante=0;
char nome[80];
char palavra[20];

Essas linhas fazem a declaração de algumas variáveis. Como o assunto é extenso, vamos gastar algum tempo falando sobre ele aqui.
Em C, as variáveis representam espaços de memória que o compilador irá preparar para determinadas funções para uso do programa. Toda variável tem um tipo pré-determinado (ou estático) e só pode receber valores daquele tipo pré-determinado. No caso, em C, os principais tipos são:

Tipo Num de bits Intervalo
Inicio Fim
char 8 -128 127
unsigned char 8 0 255
signed char 8 -128 127
int 16 -32.768 32.767
unsigned int 16 0 65.535
signed int 16 -32.768 32.767
short int 16 -32.768 32.767
unsigned short int 16 0 65.535
signed short int 16 -32.768 32.767
long int 32 -2.147.483.648 2.147.483.647
signed long int 32 -2.147.483.648 2.147.483.647
unsigned long int 32 0 4.294.967.295
float 32 3,4E-38 3.4E+38
double 64 1,7E-308 1,7E+308
long double 80 3,4E-4932 3,4E+493
A bem da verdade, nessa tabela os números de bits são indicados conforme a padronização C exige. Porém, existem casos em que essa padronização não é seguida e isso pode comprometer o uso de determinados tipos . Veremos uma solução ainda ao estudarmos esse programa.
De qualquer modo, podemos dividir os tipos de variáveis em C em três grupos fundamentais: 

  • Caracteres;
  • Inteiros e;
  • Números de ponto flutuante;

Na realidade, os caracteres são entendidos como um subgrupo dos inteiros, mas em 99% das aplicações, não utiliza-se os caracteres como números, embora alguns truques interessantes apareçam daí (veremos algum deles em posts futuros, quando precisarmos lidar com condições lógicas em C).
Aqui é importante ser feita uma ressalva MUITO SÉRIA que não foi feita no Hello World porque não era exatamente o momento: a linguagem C é considerada CASE-SENSITIVE.
“Que diabo é isso?“, você deve estar se perguntando. Isso quer dizer que todo compilador C diferencia as letra maiúsculas das minúsculas. Isso quer dizer que para ele main e MAIN e Main são três coisas diferentes. Isso gera algumas coisas estranhas: por exemplo, o comando return, como vimos no Hello World, é uma palavra reservada e, portanto, não pode ter nenhum outro comando com esse nome. Porém, se você quiser criar uma função Return e outra RETURN, você PODE, uma vez que para o compilador C, cada uma dessas é diferente uma das outras e por sua vez são diferentes de return. Para facilitar a vida, existem certas convenções que são adotadas para evitar confusões, sendo que a principal é: variáveis e funções devem ser nomeadas em minúsculas, sendo que, caso sejam usadas duas palavras, as opções são (1) usar-se hifen () ou underscore (_) para separá-las ou (2) colocar a primeira letra de cada palavra à exceção da primeira em maiúsculo.
Dito isso, vamos a mais uma regrinha rápida: como criar um nome de variável válido. O C considera válido um nome de variável que obedeça às seguintes regras:

  1. Tenha no máximo 32 caracteres de tamanho: na realidade, para o código fonte o nome pode ser maior. Porém, apenas os primeiros 32 caracteres são considerados como nome da variável. Portanto, se o seu programa tiver umaVariavelCujoNomeEhGigantePorqueOProgramadorAchouLegal e tiver também umaVariavelCujoNomeEhGigantePorqueOProgramadorTambemAchouLegal, ou compilador não irá apresentar mensagens de erro, mas você terá erros de lógica, pois para o compilador o nome de ambas as variáveis será umaVariavelCujoNomeEhGigantePorq.
  2. Seja composto exclusivamente por:
  • Letras sem acentos ou caracteres específicos (a-zA-Z);
  • Números;
  • Underscore (_);
  1. Não pode ser iniciados por número, embora underscore seja aceito;
  2. Não seja nome de variável ou função previamente declarada ou ainda de palavra reservada;

Considerando-se essas regras, para o compilador qualquer nome é válido. Isso porque o nome da variável serve apenas para identificar ao programador o valor a ser manipulado (na realidade, o compilador faz uso dos nomes de variáveis para traduzir os nomes em endereços a serem vistos quando o programa for compilado e executado. Veremos mais sobre isso quando entramos no assunto de ponteiros). Porém, é interessante que o programador tome cuidado ao dar os nomes de variáveis de modo a poder compreender o que elas fazem: embora abc123 seja um nome de variável aceitável para o compilador, notaDoAluno é um nome tão válido quanto e mais claro para o programado, então deveria ser uma opção mais acertada em um programa que a opção anterior.
Dito isso, vamos voltar aos tipos de variáveis: como dissemos, existem dois tipos numéricos importantes, os inteiros e os números de ponto flutuante. No caso, os inteiros são sempre identificado como int (à exceção de char e byte, que são reconhecidos como ponteiros, ainda que normalmente não sejam usados como tal), embora possam receber modificadores. Esses têm a ver com o tamanho em bits do mesmo (o que também afeta o valor máximo que o mesmo pode representar) e como o fato de ter sinal ou não (o sinal é sempre representado por um bit, e portanto pode modificar o valor máximo que o mesmo pode representar). O int básico é signed (tem sinal) e short (em seu menor tamanho). Na tabela anterior perceba que int, short int, signed int e signed short int tem o mesmo valor em número de bits (16) e valores mínimos e máximos (de -32768 a 32767).
Comparemos primeiro o int básico com o unsigned int (inteiro sem sinal). O número de bits é o mesmo, mas o valor mínimo muda para 0 (ou seja, essa variável não aceita valores negativos). Se compararmos o int com o long int (inteiro grande), embora o valor mínimo seja negativo, em ambas as pontas ele é maior que o int normal por ter um maior número de bits. É importante ter isso em mente ao criar-se um programa: em certas situações (por exemplo, contagem de mercadorias) um long int pode ser mais útil que um int. Em outras aplicações (por exemplo, uma aplicação de votação), usar um unsigned int pode permitir a você usar com maior eficiência a memória do sistema (ao não ser obrigado, por exemplo, a usar um long int).
Na parte dos números de ponto flutuante, o C utiliza float, double e long double. Em todos os casos, o C tem seus limites definidos em potências de 10. Porém, devido às conversões de base e perdas de precisão, é importante ter em mente que valores de ponto flutuante perderão precisão conforme o tipo utilizado: quanto mais bits, mais preciso o valor, e portanto menor chance de erros de cálculo serão sentidas. Porém, é importante lembrar que existe consumo de memória, e que dependendo da aplicação isso deve ser levado em conta.
De qualquer forma, existe pouca coisa a comentar a mais sobre os tipos de variáveis. Voltemos agora ao nosso código de exemplo:

int primeiro=0, segundo=0, soma=0, resto=0, divisaoInteira=0;
float divisaoFlutuante=0;
char nome[80];
char palavra[20];

São declaradas cinco variáveis inteiras (int), com os nomes primeiro, segundo, soma, resto e divisaoInteira. Se você só declarar uma variável, o valor inicial dela será aleatório. Não é considerado uma boa prática, ainda que permitido pelo C, declarar-se uma variável sem inicializá-la, pois isso pode ter conseqüências estranhas. No caso, as variáveis inteiras em questão foram inicializadas em 0, ao colocar-se diante delas um sinal de igual. O sinal de igual em C é um operando de atribuição de valor, ou seja, ele indica que a varíavel colocada antes do sinal de igual irá receber o valor colocado depois do sinal de igual. É importante deixar claro por causa dos operadores lógicos, que veremos em outro exemplo, e como funciona as comparações lógicas em C.
Uma coisa que é importante dizer é que uma variável pode ser declarada em uma linha e inicializada em outra. Embora o código como mostrado seja considerado mais claro e seja uma melhor prática em C, o código abaixo:

int primeiro, segundo, soma, resto, divisaoInteira;
primeiro=0;
segundo=0;
soma=0;
resto=0;
divisaoInteira=0;

Embora menos “elegante” é tão correto quanto no caso da linha int primeiro=0, segundo=0, soma=0, resto=0, divisaoInteira=0;. Essa prática de inicializar variáveis no momento da declaração, porém, impede de cometer-se erros em outros pontos, em especial quando utilizamos as variáveis de ponteiro, que veremos futuramente.
Bom, vamos continuar adiante. Após declarar os cinco inteiros, é declarado uma váriavel de ponto flutuante float divisaoFlutuante=0. Alguns autores sugerem que crie-se a prática de inicializar variáveis de ponto flutuante com 0.0, pois essa é, digamos assim, a forma de indicar um 0 de ponto flutuante sem provocar typecast (veremos isso adiante). Particularmente, não acho essa uma boa opção. O ganho de desempenho não é tão alto e a legibilidade fica um pouco confusa. Mas tem aplicações, quando você possui sistemas com baixa potência (microprocessadores, por exemplo), isso pode te oferecer um ganho de performance.
Após isso, temos duas strings sendo declaradas, uma chamada nome de 80 caracteres e uma chamada palavra de 20 caracteres. Aqui na realidade não estamos declarando uma strings, e sim uma matriz de caracteres. O C lida com tipos discretos, não possuindo tipos “compostos”, como uma string. No C, uma string é composta pelos caracteres e um símbolo especial de terminação, chamado caracter nulo, ou null-character em inglês. Essa declaração apenas separa o espaço necessário para armazenar 80 caracteres (incluindo o terminador null-character). Veremos mais sobre isso em matrizes e ponteiros. Por enquanto, você sabe que essa é a forma de criar uma string em C e que você não consegue criar uma string “dinâmica” (ou seja, de tamanho ilimitado) em C.

memcpy, sizeof e cuidados com memória:

OK…. Até agora vimos a declaração de variáveis e sua inicialização, assim como a questão das strings em C. Vamos seguir adiante que esse programa ainda tem muita coisa a ser vista.
Após as declarações de variáveis, vemos um novo comando: 

/*
* O comando abaixo inicializa as strings nome e palavra
*/
memcpy(nome,”\”,sizeof(nome));
memcpy(palavra,”\”,sizeof(palavra));

O memcpy é um comando da biblioteca string.h que copia uma determinada string para outra um determinado número de vezes (memcpy vem de MEMory CoPY – Cópia de memória). Esse comando exige três parâmetros:

  • O primeiro é o nome da váriável para a qual serão copiadas as informações (na verdade a leitura é um tanto mais complexa que isso. Iremos nos aprofundar ao lidar com ponteiros);
  • O segundo é a string a ser copiada;
  • O terceiro é o número de vezes que ela será copiada;

Na primeira linha, o primeiro parâmetro é a variável nome. O segundo parâmetro é “\”, uma string de um caracter só. No caso, estamos utilizando um caracter de controle similar ao \n do printf (que vimos no Hello World). No caso, “” é o caracter de controle que representa o terminador nulo que indica o fim de uma string.
O terceiro parâmetro é interessante de ser avaliado com calma, uma vez que ele mostra uma nova palavra reservada do C, chamada sizeof. O comando sizeof recebe como parâmetro uma váriavel e devolve, em bytes, o tamanho da mesma. No caso, estamos usando sizeof para obter esse valor em bytes e usá-lo como número de vezes em que o caracter nulo deverá ser executado.
Você pode estar se perguntando: “qual a vantagem disso, se sabemos o tamanho da string palavra?” O problema é que o C, em seu padrão, apenas RECOMENDA tamanhos mínimos para os tipos de variável, não os OBRIGA. Desse modo, quando precisamos de ter certeza, como nesse caso, é interessante que utilizemos sizeof para que seu valor seja correto durante a execução, tornando-o mais portável e evitando “números mágicos” de difícil análise em caso de erro. Isso é ainda mais importante quando temos que usar, por exemplo, alocação de memória dinâmica (que veremos no futuro) com tipos numéricos cujo tamanho pode variar de máquina para máquina (máquinas para processamento científico podem usar tipagens numéricas com um grande número de bytes para alcançar níveis de precisão condizentes às necessidades científicas). Por agora, basta saber que o uso de sizeof para determinar tamanhos de variáveis é considerado uma boa prática de programação.

Entrada de dados – fgets,scanf, entrada padrão, ponteiros e o operando de endereçamento &:

Uma vez inicializadas as variáveis, estamos em “ambiente seguro” para seguirmos em frente. Como a maioria dos programas, precisamos entrar alguns dados. No caso faremos isso de algumas formas diferentes, usando comandos diferentes mostrados no trecho de código abaixo:

printf(“Olá! Digite seu nome, por favor!\n”);
fgets(nome,sizeof(nome),stdin);

A linha em roxo mostra uma forma de ler-se informação de uma entrada de dados. No caso, utilizamos o comando fgets, que faz parte da biblioteca stdio.h. Esse comando possui três parâmetros:

  • O primeiro parâmetro é o nome da variável que irá receber as informações (no nosso caso, nome);
  • O segundo é o número máximo de caracteres a serem lidos (no caso, novamente usamos sizeof para obter o número máximo de bytes);
  • O terceiro é um nome que indica qual a fonte dos dados a ser usada. No caso nosso, estamos usando o nome “stdin“.

fgets irá ler da fonte de dados indicada todos os caracteres possíveis até que (1) seja alcançado o limite de caracteres determinados no segundo parâmetro seja alcançado ou (2) um terminador nulo seja lido ou (3) seja lido um caracter de nova linha (\n). Na prática, essa última condição quer dizer que o ENTER do teclado tenha sido pressionado, sendo que o caracterdo ENTER (ASCII 13, ou \n) será armazenado.
O nome stdin, mostrado no terceiro parâmetro, é um símbolo padronizado do C, definido dentro do stdio.h. Esse símbolo representa a entrada padrão do programa C. Normalmente, a entrada padrão de um programa é o teclado, da mesma forma que a saída padrão é o monitor de vídeo. Porém, em muitos sistemas operacionais é possível utilizar-se estruturas e comandos que redirecionam a entrada e/ou a saída padrão para outras fontes (como a saída ou entrada de um programa anterior ou posterior, no que é chamado de piping). No nosso caso, estamos usado stdin para que o sistema leia o que vier do teclado do operador.
Você deve se perguntar: “Putz… que complicado… não dá para facilitar?”. Na realidade, até dá, mas aqui estamos adotando uma prática segura que é usar comandos em C que apenas utilizem a memória que foi determinada.
O C por natureza não controla o uso de memória. No caso das strings, isso é muito importante: embora declaremos um tamanho para a string, isso não quer dizer que esse tamanho será obedecido. Isso se deve ao fato de o C considerar que uma string é uma matriz ou um ponteiro de caracteres e o C não fazer nenhum controle de onde o ponteiro irá ir. Na realidade, entraremos mais aprofundadamente nesse conceito quando falarmos especificamente de ponteiros, mas aqui cabe o “parênteses”.
Existe uma função da biblioteca stdio.h, chamada gets. Ela exige apenas um parâmetro, o nome da variável que irá receber as informações, sendo muito mais simples e rápida que fgets. Porém, como ela não possui algo que diz a ela o tamanho dessa variável (na verdade, da matriz), ela irá escrever as informações onde der, inclusive sobrescrevendo qualquer coisa que esteja além da memória utilizada pela matriz uma vez que essa esteja “cheia”, não importa o que seja. Isso pode comprometer o sistema das mais diversas formas: como não há como saber o que está “imediatamente depois” da sua matriz na memória, você não tem como saber se, por exemplo, o seu código não está gravando sobre trechos de programas do sistema carregados em memória, dados de outras pessoas, etc… Por isso nossa opção por usar um comando mais complexo, mas que garanta que o programa funcionará como esperado.
Bem, acho que já demos a atenção devida ao fgets. Não se preocupe se não compreendeu totalmente o que dissemos agora: mais para frente voltaremos a esse assunto, quando tivermos com alguns conceitos mais bem detalhados, e aí as coisas ficarão mais claras.
Vamos à nossa segunda entrada:

printf(“Digite uma palavra que devo exibir e dois números para eu fazer umas contas.\n”);
scanf(“%s %d %d”,palavra,&primeiro,&segundo);

Aqui estamos usando outro comando de entrada de dados: no caso, estamos usando o scanf, que como os demais comandos de entrada de dados é parte da biblioteca stdio.h. scanf lê as informações da linha digitada e tenta encontrar coisas que casem com o formato desejado, e armazena essa informações nas variáveis desejadas. No caso, ele utiliza símbolos de formato similares aos de printf para “quebrar” a entrada da maneira adequada. No comando apresentado, ele lê uma string (%s) que irá acabar no primeiro espaço lido (importante notar isso), e em seguida lerá dois inteiros (%d), separados por espaço, e armazenará os valores lidos nas variáveisa palavra, primeiro e segundo.
Aqui, você deve ter reparado nos e-comerciais (&) na frente de primeiro e segundo. Isso se deve pelo fato de scanf precisar dos endereços das variáveis em questão, e para isso, precisamos passar ponteiros. Em C, chamamos de ponteiro uma variável que, ao invés de armazenar um conteúdo per se, armazena o endereço da memória onde esse conteúdo se encontra (na realidade, o conceito é mais complexo, mas não vamos aprofundar nele agora). Como primeiro e segundo são variáveis int comuns (ou seja, armazenam conteúdos, e não o endereço), precisamos obter o endereço de memória onde esse conteúdo está., ou seja, o endereço de memória das variáveis Para isso, usamos o operador & antes do nome da varíavel. O operador & é chamado de operador de derreferenciamento, e sua função é indicar que, naquele momento, utilizaremos o endereço da variável em questão, e não o seu conteúdo.
No caso de palavra, porém, não é necessário o operador &, pois toda matriz em C também pode ser usada como ponteiro. Na verdade, no memcpy e no fgets utilizamos os nomes das matrizes como ponteiros. Portanto, uma regrinha de C que acabamos de aprender:

EM C, TODA MATRIZ PODE SER USADA COMO UM PONTEIRO.

Iremos aprofundar essa regrinha quando falarmos de ponteiros mais profundamente. Agora, porém, deve ter ficado claro a utilidade do e-comercial (&). Se não ficou, tudo bem (por agora): ainda veremos muitas vezes o & e logo tudo isso ficará claro.
O scanf, assim como o printf, possui uma grande gama de símbolos de formato. Abaixo deixamos uma tabela com os símbolos de formato do scanf (mais adiante ofereceremos uma para printf):

Código Formato
%c Um único caracter (char)
%d Um número decimal (int)
%i Um número inteiro
%hi Um short int
%li Um long int
%e Um ponto flutuante
%f Um ponto flutuante
%lf Um double
%h Inteiro curto
%o Número octal
%s String
%x Número hexadecimal
Bem, vamos seguir em frente. Esse ponto ficará mais claro com o uso constante do comando scanf, que faremos no futuro.
Antes de avançar, falaremos sobre o comando getc(stdin). Basicamente ele lê um caracter de uma entra indicada (no caso stdin) e devolvê-lo para uma variável. Como não usamos nenhuma atribuição, o valor é simplesmente eliminado do stdin. Usamos esse comando para que qualquer caracter indesejado (em especial, ENTERs não lidos).

Operações matemáticas, operandos matemáticos e typecasting:

OK… Já obtivemos as entradas de nosso usuário, então é hora de fazemos algo com tudo isso. No caso, faremos uma brincadeira boba que é fazer algumas contas com os dois números que o usuário passou, primeiro e segundo:

soma=primeiro+segundo;
divisaoInteira=primeiro/segundo;
resto=primeiro%segundo;
divisaoFlutuante=(float)primeiro/(float)segundo;

Aqui temos um exemplo mais claro de atribuição: as variáveis soma, divisaoInteira, resto e divisaoFlutuante recebem os resultados de cada uma das contas que mandamos o C fazer. No caso, toda operação matemática em C é composta por:

atributo1 operação atributo2

Por exemplo: a varíavel soma irá receber o resultado de primeiro + segundo. Vejamos quais são os principais operadores matemáticos em C:

  • / , * e % = Divisão, multiplicação e resto da divisão inteira;
  • + e = Soma e subtração;
  • = = atribuição;

A seqüência foi propositamente colocada na ordem acima pois o C, assim como a matemática, possui uma ordem de prioridade nos operandos: os operandos / , * e % (Divisão, multiplicação e resto da divisão inteira) possuem uma prioridade mais alta que os operandos + e (Soma e subtração), portanto são executados primeiro em uma expressão complexa, com vários operandos executados ao mesmo tempo. É possível, com o uso de parênteses (), alterar a prioridade das expressões, gerando os valores corretos (de forma equivalente ao que acontece em equações matemáticas). Por exemplo: x * y + z é diferente de x * (y + z). Na primeira, o C (como na matemática) irá primeiro multiplicar x por y e depois somar o resultado a z. Na segunda expressão, primeiro é feita a soma de y e z e depois o resultado é multiplicado por x. Uma dica muito útil é: se você estiver em dúvida quanto à seqüência correta de operações, isole os termos com parênteses aos pares: embora afete um pouco a leitura do programa, é mais interessante que incorrer no risco de gerar resultados errados.
OK, vimos quais são os operandos matemáticos e como eles se comportam. Analisamos o código linha a linha, vemos que na primeira linha é executada uma soma entre os operandos primeiro e segundo e o resultado é atribuído a soma. Na segunda, é feita uma divisão entre os operandos primeiro e segundo. No caso, será feita uma divisão inteira, pois ambas são variáveis inteiras e a variável que que irá receber o resultado (divisaoInteira), também é um int. Em seguida, temos uma operação de resto entre primeiro e segundo que será armazenado em resto. Essa operação de resto (%) deve ser feito apenas com valores inteiros. Mas isso pode ser resolvido com um mecanismo que mostraremos abaixo.
A última linha apresenta uma estrutura importante que existe em C chamada typecast (mudança de tipo). Algumas vezes, precisamos fazer operações matemáticas (entre outras) com varíaveis cujo tipo originalmente não nos permite. Por exemplo, imagine que primeiro é 5 e segundo é 2. Embora indiquemos que divisaoFlutuante tem um tipo float, se não forçarmos o programa a enxergar ao menos um dos valores como float, o resultado armazenado em divisaoFlutuante será 2, ainda que armazenado em memória e com o mesmo comportamento de um número de ponto flutuante. Esse comportamento se deve ao fato de que o C pode converter operandos numéricos para outros tipos, mas apenas quando exista alguma diferença de armazenamento.  Se deixarmos os dois como inteiros, a divisão será feita como inteira (ou seja, dando o resultado 2) e só depois, na hora da atribuição é que o valor será convertido para float, o tipo correto a ser armazenado em divisaoFlutuante.
Para solucionarmos esse tipo de problema, podemos informar ao C que determinado valor, ainda que expressado de um tipo, deve ser tratado como outro naquele momento específico. A isso chama-se em programação de typecast. No caso da última linha, estamos dando typecast (ou, para resumir, cast) nas variável primeiro e segundo. Para dar-se um cast em um valor ou variável, basta anteceder o mesmo com o tipo que o sistema irá adotar naquele momento para o mesmo entre parênteses. No caso, para converter primeiro em float, utilizamos (float)primeiro. Importante dizer que não era necessário dar o cast em segundo, pois o programa, ao perceber que estaria tentando dividir um número de ponto flutuante por um inteiro não dividiria “laranjas com bananas”, por assim dizer, dando ele próprio o cast em segundo (o que é chamado em programação de autocast). Porém, para melhorar a leitura do programa, achei interessante dar o cast manualmente em segundo também. Muitas vezes, fazendo as coisas da maneira correta, você não precisará tanto de casts quanto aparente, pois o compilador irá ele próprio fazer ajustes para provocar autocast dependendo do caso. De qualquer modo, casts podem ajudar a leitura do programa, o que é bem importante.

printf, caracteres de controle e símbolos de formato:

Bem, agora que já fizemos continhas bobas, tá na hora de nosso programa mostrar o que ele fez:

printf(“Olá, %s\n”,nome);
printf(“Você me pediu para exibir %s e os números %d e %d\n”,palavra,primeiro,segundo);
printf(“Algumas continhas!\n%d+%d=%d\n”,primeiro,segundo,soma);
printf(“%d:%d=%d(Resto %d)”,primeiro,segundo,divisaoInteira,resto);
printf(” ou %7.2f\n”,divisaoFlutuante);
printf(“Divertido, não? Aperte qualquer tecla para continuar…”);
getc(stdin);
return(0);

OK, você deve estar se perguntando, temos um monte de printf que vimos lá no Hello World, e que faz saída formatada. E daí?
Bem… E daí que agora temos saídas que o sistema processou e que devemos mostrar ao usuário. Para isso, temos que indicar ao C o que vai ser publicado e como.
O printf, além dos caracteres de controle, aceita símbolos de formato. Lembra quando falamos acima, ao comentarmos o scanf, que ele exigia símbolos de formato para saber o que ler? Pois bem, o nosso amigo printf também aceita símbolos de formato para dizer como os dados deverão ser apresentados pelo sistema. Abaixo copiamos uma tabela de símbolos de formatos (site original – Curso de C da UFMG):

Código Formato
%c Um caracter (char)
%d Um número inteiro decimal (int)
%i O mesmo que %d
%e Número em notação científica com o “e”minúsculo
%E Número em notação científica com o “e”maiúsculo
%f Ponto flutuante decimal
%g Escolhe automaticamente o melhor entre %f e %e
%G Escolhe automaticamente o melhor entre %f e %E
%o Número octal
%s String
%u Decimal “unsigned” (sem sinal)
%x Hexadecimal com letras minúsculas
%X Hexadecimal com letras maiúsculas
%% Imprime um %
%p Ponteiro

Na verdade, a formatação completa é um pouco mais complexa, pois após o % você pode colocar:

  • -, indicando que o preenchimento deve alinhar-se à direta (e não à esquerda);
  • +, indicando que, caso o valor tenha sinal, os símbolos de sinal são mantidos (normalmente sinais de positivo são ignorados);
  • um número, indicando o tanto de caracteres a serem exibidos. No caso de numéricos, se esse número for precedido por 0, os caracteres que normalmente não seriam preenchidos serão preenchidos por 0;
  • uma seqüência x.y indicando (para ponto flutuante) que deve-se exibir o número com um número de caracteres x, sendo que desses y serão casas decimais ou (para inteiros) deve-se exibir no mínimo x números e no máximo y. Vale a regra do 0 mostrada acima;

Existem muito mais complexidades envolvendo a saída formatada. Caso queira saber mais, esse artigo da Wikipedia traz muito mais informações que, na pior nas hipóteses, serão interessantes a título de curiosidade.
A primeira linha, printf(“Olá, %s\n”,nome);, irá imprimir na tela “Olá”, seguido pelo conteúdo da variável nome, e irá pular uma linha. Porém, na execução, você irá perceber que irão ser puladas duas linhas. A segunda linha deve-se ao fato de na leitura de dados por fgets o caracter \n (ENTER) será lido e interpretado pelo printf como parte da varíavel nome. Em outros tópicos ensinaremos uma técnica simples para remover esse \n e deixar a saída mais elegante. Por enquanto, somos obrigados a tratar essa segunda nova linha como um bug do programa.
A segunda linha do trecho que estamos estudando mostra uma característica do printf. Veja a linha em questão:

printf(“Você me pediu para exibir %s e os números %d e %d\n”,palavra,primeiro,segundo);

Essa linha irá exibir a palavra e os números que você digitou anteriormente. Como isso é feito?

Perceba que a linha em questão possui três símbolos de formato, %s, %d e %d. Esses símbolos indicam que o programa espera uma string e dois inteiros para exibí-los na tela. Logo após a nossa string formatada (“Você me pediu para exibir %s e os números %d e %d\n”), colocamos as variáveis que armazenam esses valores na seqüência pedida pela string (no caso, primeiro palavra – a string, depois primeiro e segundo – os dois inteiros), separados por vírgulas entre si e da string a ser formatada. Essa é a construção padrão de um comando printf: uma string a ser formatada, contendo tantos caracteres de controles e símbolos de formato quanto necessário, seguido pelas variáveis a terem seus conteúdos apresentadas em cada posição, na seqüência das mesmas e com um tipo adequado ao símbolo de formato. Colocar menos ou mais variáveis e/ou variáveis de tipo inadequado irá ocasionar erros no momento da compilação (porém, no caso dos numéricos, é permitido fazer o cast do conteúdo da variável para um tipo de dados adequado antes de apresentar o valor em questão).
Em seguida a outra linha possui uma construção propositalmente confusa na string a ser formatada. Vamos vê-la: 

Algumas continhas!\n%d+%d=%d\n

Lembrando que os valores que irão substituir os três símbolos %d são os das variáveis primeiro, segundo e soma. Qual será a saída dessa string:

  • A primeira coisa será imprimir Algumas continhas!, seguido por uma nova linha(\n);
  • Em seguida, irá exibir o conteúdo de primeiro (%d – imaginemos que seja um 5)…
  • … seguido por um sinal de maior (+)…
  • e pelo valor de segundo (%dimaginemos que seja um 2) …
  • … seguido por um sinal de igual (=)…
  • que precede o valor de soma (%d – que, como vimos anteriormente, é primeiro+segundo – nesse caso 5+2, dando um 7)…
  • Terminando por um outro nova linha (\n);
  • O que nos leva à conclusão que, na tela, irá aparecer (considerando os valores acima):

Algumas continhas!
5+2=7

Esse tipo de construção é permitida? Sim! Desde que se siga-se a regra de que tem que haver tantas variáveis quanto símbolos de formato e nos tipos adequados, nada impede que uma mesma string formatada gere 2, 3, até 1000 linhas (na verdade, existe outra regra que falaremos ao mencionar mais claramente strings).
Bem, a linha seguinte, printf(“%d:%d=%d(Resto %d)”,primeiro,segundo,divisaoInteira,resto);, lembra a anterior, à exceção que não terminamos essa linha com um nova-linha (\n). Da mesma forma que uma string a ser formatada pelo printf não precisa ter nenhum símbolo de formato (lembra do printf(“Hello World!\n”);?), o printf pode ter uma linha sem caracteres de controle. Na realidade, nenhum dos dois é obrigatório no comando printf. No nosso caso, ele irá formatar a string e exibir o seu conteúdo, mas sem pular linha. Isso irá gerar uma linha cujo conteúdo será contatenado na saída em tela com o conteúdo da próxima instrução printf que tiver, ou então com uma linha de comando do sistema ou similar caso o programa se encerre antes. Isso é importante de ser entendido, pois o printf não pula automaticamente linhas! Parece bobagem dizer isso, mas caso não seja indicado explicitamente que o comando deve pular uma linha (com o uso de \n), ele não irá o fazer.

OK, a linha seguinte tem um exemplo que é interessante e importante:

printf(” ou %7.2f\n”,divisaoFlutuante);

Parece muito com os demais printf que vimos nesse longo exemplo. Porém, a diferença tá no símbolo de formato: %7.2f. O que isso quer dizer? Esse símbolo indica que, na posição em questão, serão exibidos no máximo 7 caracteres, com no mínimo 2 deles sendo casas decimais (somando o ponto decimal, sobrariam portanto 4 casas para a parte inteira) e que o número será exibido como um flutuante decimal (f). Esse é um ponto importante que podemos resumir criando uma espécie de “modelo” de símbolos de formatação:
%[-+0][tamanho.[casas-decimais]]tipo
Desses, apenas tipo é obrigatório, sendo que podem ser usados quaisquer um dos tipos anteriormente mostrados na tabela de símbolos. O tamanho pode ser usado em qualquer condição, e casas-decimais pode ser usado em qualquer numérico, assim como os caracteres especiais , + e 0 antes do tamanho. Alguns exemplos:
  • %30s – apresenta apenas os primeiro trinta caracteres de uma string;
  • %10.2f – apresenta um ponto flutante escrito com 10 caracteres, sendo dois deles casas decimais: no caso dos pontos flutuantes, uma vez que se defina um número de casas decimais a serem exibidas ele é respeitado SEMPRE – valores com mais casas decimais tem seus valores decimais arredondados até o número de casas adequado e valores com menos casas têm as casas restantes preenchidas com 0;
  • %4h – esse é um interessante, pois mostra o conteúdo da variável inteira associada convertido para hexadecimal.  A base hexadecimal (ou base-16) é uma base numérica muito usada em eletrônica e programação avançada, e por isso C prevê o uso dessa base como forma de facilitar a entrada e saída de valores;
  • %-30s – como o anterior, mas irá alinhar o texto à direita, preenchendo os caracteres que sobrem (caso aplicável), com espaços;
  • %+020.2f“COMO ASSIM BIAL?”, você deve estar pensando… Bem, esse é nosso exemplo mais complexo: ele vai (1) imprimir um ponto flutuante com (2) 20 caracteres sendo (3) duas casas decimais e (4) preenchendo os caracteres que sobrem (caso aplicável) com 0 e (5) mostrando sempre um símbolo de sinal, independente de o resultado ser positivo ou negativo. Esse pode ser um exemplo estranho, mas existem aplicações científicas onde a leitura do sinal na saída dos dados é importante (por exemplo, coordenadas geográficas e ou indicadores de sinais em astrofísica);
Bem creio que com essa última linha acabamos de explicar o importante do programa. Salve ele, compile-o (já vimos como compilar um programa no Hello World) e brinque um pouco, executando-o várias vezes e procurando pensar em como o programa se comporta com os diversos valores.

Como fizemos com o Hello World, aqui também vou sugerir algumas “brincadeiras” com o programa:

  • Pra começar, tente modificar os tipos de variáveis no momento da declaração para ver o comportamento do programa. Aqui muitas coisas poderão simplesmente não funcionar, mas o objetivo por incrível que pareça é esse mesmo: procurar entender o que funciona e o que não;
  • Para entender as mecânicas de typecasting e autocasting, remova um dos casts para float na divisaoFlutuante, e depois remova o outro. Veja o comportamento em ambas as situações, e procure reparar como o programa irá reagir em cada uma dessa situações. PS: para que o exemplo fique mais claro, escolha valores para primeiro e segundo cuja divisão não seja exata (por exemplo, 8 e 3 ou 5 e 2);
  • Tente modificar as formatações na saída do programa. Utilize tamanhos fixos e no caso dos números experimente acrescentar o 0 antes dos tamanhos do mesmo. Utilize para alinhar valores à direita;
  • No momento em que for digitar o nome e a palavra a serem exibidas, tente utilizar valores com tamanhos muito maiores que os que as variáveis suportam. Verifique qual será a saída resultante. Lembrando sempre que a maior palavra da Língua Portuguesa é anticonstitucionalissimamente e que um bom nome gigante é o de Dom Pedro II (Pedro de Alcântara João Carlos Leopoldo Salvador Bibiano Francisco Xavier de Paula Leocádio Miguel Gabriel Rafael Gonzaga) , o que os tornam ótimos valores para esses testes;
  • Após fazer esses testes, modifique o programa para que ele use gets ao invés de fgets e repita os testes. Verifique o que pode acontecer ao entrar valores que “estourem” o tamanho da variável nome e o comportamento no sistema operacional quando isso ocorrer;
  • Tente adicionar ao programa um comando que determine a área de um triângulo de base primeiro e altura segundo. Declare e inicialize uma variável para guardar esse valor e um comando printf para exibir o resultado final;
Bem, esse foi um exemplo bem mais longo, pois tivemos que tratar muitos assuntos aqui. Semana que vem a idéia é entrar em comandos de controle de fluxo. Então, até lá!

O primeiro programa: HelloWorld.c

Olá todos!
Bem, agora vamos para algo de programação.

OK… Na realidade não é nada muito fantástico, mas a ideia é permitir que você tenha uma noção da estrutura de um programa C típico e de como ele é composto. Depois iremos explicar tudo de maneira mais detalhada.
Existe uma tradição em programação que o primeiro programa de computador para qualquer linguagem deve apresentar apenas uma mensagem na tela, normalmente “Hello World!“. Na realidade, se você usar o Google para pesquisar o termo “Hello World” em programação, você verá sites com coletâneas enormes de HelloWorlds nas mais diversas linguagens de programação, antigas ou modernas.
De qualquer modo, vamos ver nosso Hello World em C. Abra um editor de texto (como o Notepad do Windows ou quaisquer um dos editores de texto do Linux) e digite o que está abaixo as-is. (iremos explicar melhor logo) Se estiver usando uma IDE, abra-a e crie um arquivo com o conteúdo abaixo:

#include <stdio.h>

/* Nosso Hello World! (c) 2010 GPL */

int main (int argc, char** argv)
{

    printf(“Hello World!\n”); // Trouxemos ele do stdio.h
    return (0);
}

Não se preocupe em copiar a formatação do código: apenas digite-o como ele está. As cores são apenas para algumas explicações que serão dadas sobre o programa em questão.
Vamos começar então a destrinchar os que digitamos.
Repare na primeira linhas:

#include <stdio.h>

Ela, por incrível que pareça, ainda não é código C… Na verdade não exatamente. Todo “comando” que comece com o # (sharp, tralha, cerquilha, lasanha, entre outras formas de dizer que já ouvi por aí) é um comando para o pré-processador, que indica que o compilador deve tomar alguma atitude especial. Nesse caso, o #include adiciona ao arquivo em questão o conteúdo de um outro arquivo, no caso o arquivo stdio.h. O fato de ele estar entre os sinais de maior-menor tem uma função especial: ele faz o compilador buscar esse arquivo entre os arquivos da biblioteca-padrão do C. A biblioteca-padrão do C representa comandos que normalmente espera-se que façam parte do compilador independentemente da plataforma, versão ou tipo do compilador específico e, embora não sejam necessariamente obrigatórios, eles são quase sempre encontrados em qualquer bom compilador. É possível programar-se em C sem as bibliotecas-padrão (na realidade, em alguns casos é desejável, como no caso de programar-se C para microprocessadores), mas em geral um bom compilador irá incluir as bibliotecas padrão do C. Em alguns casos, o maior-menor irá representar a inclusão de arquivos de outras bibliotecas que fazem parte dos includes do compilador. Falaremos mais sobre isso quando entrarmos na questão de programação com múltiplos objetos e afins.
O stdio.h vêm de STandarD I/O Headers (Cabeçalhos de Entrada e Saída Padrão). Ele faz referências a funções de entrada e saída padronizadas pelo C. No caso, os cabeçalhos indicam ao compilador que tipo de funções o programa irá usar e como ele irá se comportar nesse caso. Veremos mais no futuro, mas o que é importante saber é que, sem essa linha, esse programa (e qualquer outro que exija funções de entrada e saída padrão) irá dar erro no momento da sua geração (ou compilação).
Chega de falar dessa linha. Vamos falar sobre uma coisa muito importante em C: comentários.
Comentários são basicamente isso: comentários. Em geral, comentários são apenas texto informativo que é inserido em um programa para documentá-lo, sendo sumariamente ignorado pelo compilador (veremos mais sobre o compilador ainda nesse post). Em alguns casos, com o uso de ferramentas certas, comentários podem ser muito úteis, ajudando a documentar um programa de maneiras muito inteligentes. Uma utilidade também dos comentários é que eles podem ser usados como método de depuração (correção de problemas): basta comentar-se o trecho de código onde existam problemas e verificar se os mesmos persistem uma vez que o programa seja re-executado.
No C, existem dois tipos de comentários:

  • O padrão mais antigo, onde começa-se o comentário com /* e termina com */: nesse caso, é considerado comentário tudo que estiver entre esses dois símbolos. A vantagem desse tipo de comentário é que ele é multi-line, ou seja, você pode adicionar linhas e linhas de comentário, desde que todos os comentários estejam entre esses dois símbolos;
  • Um padrão mais recente (parte do padrão chamado C99), que é usar-se duas barras (//): na verdade, esse padrão já usado a um bom tempo, em especial em compiladores que compilem código de C++ além de C (veremos C++ em tópicos mais avançados). Ele permite comentários in-line, ou seja, comentários a partir de um determinado ponto de uma linha de código. Para o compilador, tudo entre as duas barras e o final da linha onde elas estão será considerado comentário;

Bem, não precisamos falar mais dos comentários, então vamos voltar à nossa  Repare nas linhas comentadas em azul:

int main (int argc, char** argv)
{
<…>
}

Aqui estamos utilizando uma função main para colocar o código principal do nosso programa. O C trabalha com o conceito de função para separar os trechos de códigos, sendo que uma função é basicamente um bloco de código (um trecho de código separado pelos colchetes { }) com um nome. Veremos mais sobre isso quando entramos em funções. O importante agora é saber que main é o nome que damos a essa função onde colocamos nosso código (o texto em preto no programa acima).
A função main, porém, é especial e exige um pouquinho de atenção: ela é considerada o ponto de entrada de um programa. Para ser exato, caso não exista uma função main dentro de um programa, o compilador irá dar erro ao compilar (existem situações em que você não usará main em um arquivo C. Veremos isso ao falarmos sobre a programação com múlltiplos fontes e bibliotecas). Ela também tem uma construção razoavelmente padronizada, que é int main (int argc, char** argv). Você pode criar seu main com outras definições (ou cabeçalhos… lembram lá de cima?), mas alguns compiladores poderão acusar um alerta (uma mensagem que não é de erro, mas indica detalhes que podem dar problema) de que o main em questão sai do padrão do C. Em geral, o mainmain será escrito dessa maneira como indicamos e, caso você use uma IDE, o arquivo principal do seu projeto quase sempre terá um como o acima (falaremos mais sobre esse conceito de projeto no futuro).
Toda função em C é composta de três fatores, que explicaremos por alto (voltaremos mais nesse assunto quando falarmos de variáveis e funções), e a main não escapa desse padrão:

  • Retorno: diz qual o valor final que a função irá devolver ao sistema uma vez que termine de executar;
  • Nome: como ela é identificada pelo programador (quando o programa for compilado, esse nome é substituído por informações que indicarão onde ela ficará na memória do computador no momento da execução);
  • Parâmetros: O que ela espera de informação de quem quer que a chame. Normalmente são representados por um tipo, que indica qual o tipo de valor que é esperado, e por um nome de parâmetro, que indica como esse parâmetro será usado pelo programador;

Na realidade, é possível especificar-se funções que não retornem nada e não tenham parâmetros. Veremos isso em funções e quando falarmos dos tipos de dados em C.
No caso específico do main, ela é do tipo int (que representa valores inteiros – sem casas decimais – tanto positivos quanto negativos), chama-se main e possui como parâmetros um int chamado argc e um char** chamado argv. Esses parâmetros, no caso de main representam:

  • argc: o número de argumentos passados pelo programa e;
  • argv: uma lista dos mesmos (na verdade, o argv é mais complexo que isso, mas não iremos aprofundar mais nele. Por enquanto, o importante é saber como ele trabalha);

OK… Até agora vimos que precisamos dizer ao C que funções de biblioteca iremos usar e qual é nossa função main, além de dizer a ele o que ele deve retornar ao sistema operacional e como ele deve receber parâmetros do sistema operacional. Agora vamos aos comandos reais.

    printf(“Hello World!\n”); // Trouxemos ele do stdio.h
    return (0);

Bem, esses dois comandos basicamente imprimem na tela o texto “Hello World!” na tela e encerram o programa. Mas esses comandos possuem detalhes que os diferenciam e que são importantes para entender o C:
O primeiro comando se chama printf (de PRINT Formatted, “imprimir com formatação”). Esse comando é parte das bibliotecas padrão do C, na verdade da stdio que já mencionamos anteriormente. Embora seja parte da biblioteca padrão, ele não é, vamos dizer assim, obrigatório para um compilador C. Na verdade, ele não é uma palavra reservada, ou seja, um comando que é obrigatório da existência em C e que não pode possuir outro igual. De certa forma, um desenvolvedor pode criar seu próprio comando printf e colocá-lo em uma biblioteca à parte, o que pode ser útil para, por exemplo, quando vai programar-se microprocessadores (que não possuem saídas visuais, portanto havendo necessidade de um printf). No caso, ele recebe um texto para imprimir na tela. Na verdade, esse texto pode possuir determinados conjuntos de caracteres especiais que indicam como deverá ser formatado o código em questão, aos quais chamamos de formatos e caracteres de controles (veremos mais adiante sobre isso). No caso, temos um caracter de controle (símbolos que representam caracteres que não são visualizáveis normalmente), o \n, que indica que deve-se pular uma linha ao terminar-se de imprimir esse texto. Portanto, o que printf vai fazer aqui é (1) escrever na tela “Hello World!” e (2) pular uma linha.
Quem leu até aqui com calma vai perceber que a “mágica” já está feita. Então o programa não acabaria aqui? Para que o segundo comando, o tal return (0)?
O comando return é parte das palavras reservadas do C. Uma palavra reservada normalmente representa um comando ou informação interna da linguagem de programação C cujo nome não pode ser usado para MAIS NADA a não ser pelo que é indicado. O padrão C determina um conjunto de palavras reservadas, como return, int ou float. Importante ver a diferença entre os dois: um programador poderia criar sua própria versão de printf, mas não poderia criar uma versão de return.
O return é um comando que normalmente encerra qualquer função, devolvendo o controle do programa para quem quer que tenha chamado a função. No caso de main, pode-se dizer que quem chama a função main (o programa principal) é o ssitema operacional, portanto é ele que vai receber de volta o controle quando o comando return for executado nesse caso, o que indica o final do programa. O 0 entre parênteses é um valor inteiro que o sistema pode usar para fazer testes (no caso de programas automatizados e afins). No nosso caso, como não existe, digamos assim, um “dar errado”, o valor 0 foi adotado mais por convenção, mas esse valor pode ser mudado se for necessário, embora não aprofundaremos esse tópico aqui. Importante a saber aqui é que (1) return é uma palavra reservada e não pode ter outro no C, (2) ele devolve o controle ao sistema operacional nessa situação e (3) ele pode devolver um valor que pode ser usado pelo sistema operacional. No momento, basta saber essa informação: nos aprofundaremos no funcionamento de return quando estudarmos funções.
OK… O programa está pronto. O que fazer com ele?
Diferentemente de algumas linguagens de programação mais atuais, como Python, PERL, PHP ou Ruby, o C é uma linguagem compilada. Isso quer dizer que precisamos usar um programa, chamado compilador, para traduzir as instruções do nosso programa (chamado de código fonte) para instruções que o computador compreenda e seja capaz de executar (chamado de código objeto ou binário). No caso, vamos utilizar o GCC para compilar o nosso programa: salve o código digitado como HelloWorld.c (na realidade, você pode escolher qualquer nome, mas é bom usar nomes claros, que identifiquem facilmente seu código) e abra um terminal (no Linux) ou um Prompt de Comando (no DOS/Windows). Entre no mesmo diretório onde você gravou o seu HelloWorld.c e digite o seguinte comando:

gcc -o HelloWorld HelloWorld.c

O comando em questão irá compilar seu código e irá gravar o binário em um arquivo. No caso do Linux especificamente, o padrão do GCC é gravar o binário em um arquivo de saída a.out. Utilizamos então a opção -o (output) pada gravar o código em um arquivo de saída chamado HelloWorld. Imaginando que tudo esteja correto, usamos o comando ./HelloWorld para executarmos nosso Hello World!
OK, você deve ter visto o Hello World! e se perguntado em algum momento “Não entendi!”. Algumas “brincadeiras” que você pode fazer para entender alguns conceitos que colocamos aqui:

  1. Comente ou remova a linha #include e veja o resultado ao compilar o programa;
  2. Modifique o nome main para alguma outra coisa e tente compilar o programa. Substitua o retorno int ou os parâmetros entre parênteses dele por void (sem retorno/parâmetro);
  3. Adicione mais linhas printf com o texto que você quiser e coloque ou não o caracter de controle \n e veja como o programa irá se comportar;

Essas “brincadeiras” irão permitir que você pegue alguns dos conceitos sobre os quais falamos até agora e irá divertir você. Caso tenha alguma dúvida sobre esse post, por favor, adicione nos comentários sua dúvida. Em especial, se a dúvida envolver erros no programa, copie a saída que deu na compilação e informe seu sistema operacional e compilador.
Semana que vem estarei postando mais coisas. A jornada de programação C apenas começou.