Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

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.

About these ads

Uma resposta para “Sobre arquivos de código-fonte, arquivos de cabeçalhos e projetos

  1. Pingback: Utilitários Make, Makefiles e sua importância « Aulas de C

Deixe um comentário

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

Logotipo do WordPress.com

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

Imagem do Twitter

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

Foto do Facebook

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

Foto do Google+

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

Conectando a %s

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.

%d blogueiros gostam disto: