Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos da Categoria: Técnicas

Utilitários Make, Makefiles e sua importância

OK…
Então continuaremos sem programar C real. Essa é a má notícia da “aula” de hoje.

A boa é que terminaremos o tópico que começamos na “aula” passada, quando falamos sobre o conceito de projeto, separação de códigos fonte e compilação individual.
Como vimos, é possível dividir um programa em arquivos fontes individuais (que formam, em conjunto, um projeto) e compilar os mesmos individualmente, de modo que no caso de uma modificação pontual não seja necessário recompilar totalmente os fontes para obter-se o executável. Isso deve-se ao fato de os compiladores modernos na verdade executarem duas funções simultaneamente: a compilação (transformação de códigos fontes em códigos objetos) e a linkedição ou ligação (a união de vários códigos objetos em binários que possam ser executados pela máquina).
Até aqui nenhuma novidade.
Mas lembremos novamente do que mostramos na aula passada. Programas “reais”, como pacotes Office e navegadores possuem milhões de linhas de código, que, por sua vez, podem estar espalhadas em milhares de arquivos de código fonte. Mesmo com essa divisão, a tarefa de gerar um novo código-objeto para cada fonte alterado e ligar todos os objetos em um executável seria MUITO enfadonha e propensa a erros.
Para resolver esse problema, antigamente usavam-se scripts específicos para cada plataforma de desenvolvimento e uso. Porém, isso ainda assim era ineficiente, pois a adição ou remoção de novos arquivos e a mudança na estrutura do projeto demandava a total modificação dos scripts, sendo que os próprios scripts tinham que ser mantidos, e eram enfadonhos de se manter.
Em 1977, porém, Stuart Feldman criou o primeiro sistema de automação para a compilação de programas, o make. A função do make é, construir todas as dependências descritas em um arquivo especial chamado Makefile. Makefiles seguem um padrão razoavelmente simples de construção. Embora o formato Makefile original seja um “padrão de facto“, muitos compiladores trazem consigo o seu próprio make, e IDEs, como a Code::Blocks, o Eclipse e o Netbeans também possuem suas próprias regras e mecanismos, usando ou não e baseado ou não no make UNIX.
Para explicarmos o conceito geral e demonstrarmos o funcionamento, utilizaremos o GNU Make. GNU Make é parte dos utilitários incluídos no GCC (GNU Compiler Chain), que é incluído em quase todas as distribuições Linux e está disponível em várias plataformas, como Windows, MacOS/X, etc…

Um Makefile simples:

Sem muitas delongas, vamos mostrar como o Make trabalha e como criar um Makefile:
Basicamente, make funciona em um sistema de alvos e dependências. Ou seja, make precisa saber quais são os arquivos que ele irá processar (dependências) para realizar alguma tarefa e obter alguma outra coisa (alvo). Por exemplo, vamos fazer um Makefile simples.

all:
    echo “Hello, Make!”

Aqui dizemos que queremos obter “all“, ou seja, tudo (é o default do GNU Make. Se não encontrado, executa o primeiro alvo de cima para baixo dentro do Makefile). Depois dos dois-pontos (:) indicaríamos qualquer dependência que precisássemos. Porém, como não temos nenhuma, deixamos em branco mesmo.
Em seguida, colocamos a tarefa a ser executada. Ela pode ser quaisquer seqüências de comandos válidos para o sistema operacional em questão. No nosso caso, utilizamos um comando echo “Hello, Make!”, que é usado no Linux para emitir uma mensagem na tela (no Windows também funciona). Uma coisa importante: os comandos da tarefa devem ser espaçados do início da linha por uma tabulação (tecla TAB). Embora algumas ferramentas make modernas consiga reconhecer espaços no lugar do TAB para efeito de indicação da tarefa, é melhor manter o padrão para não incorrer em problemas em outras plataformas.
Bem, digitado esse arquivo, salve-o com o nome de Makefile. Esse nome é o nome default que o GNU make (e a maioria dos demais) irá procurar. Em várias ferramentas make, é possível que você defina, por meio de uma opção de linha de comando, qual o Makefile a ser usado. Consulte o manual de sua ferramenta Make para maiores informações.
Bem, voltando: uma vez salvo o mesmo (no momento não interessa onde você irá o jogar), digite o comando “make” na linha de comandos de seu sistema operacional, no diretório onde você salvou o seu arquivo Makefile. A saída resultante será algo mais ou menos como a seguinte:

$ make
echo “Hello, Make!”
Hello, Make!

Não muito útil, mas já mostra resultados. Uma vez que você disparou o comando, ele procurou um alvo-padrão (all) e verificou se suas dependências estavam resolvidas (no momento nenhuma, então ok). Estando tudo OK, ele executou a tarefa determinada (no caso, o comando echo “Hello, Make!”) e se encerrou.
Exemplo bobo…

Um Makefile útil

Bem, pelo menos sabemos como ele funciona. Agora vamos fazer algo realmente útil:
Vá ao diretório onde você guardou o seu projeto do Roletrando que mostramos na “aula” passada. Vamos criar um Makefile um pouco mais interessante:

#
# Primeira tentativa de Makefile
#

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

main.o: main.c roletrando.h
    gcc -o main.o -c main.c

regras.o: regras.c roletrando.h
    gcc -o regras.o -c regras.c

letras.o: letras.c roletrando.h
    gcc -o letras.o -c letras.c

Agora temos um Makefile realmente útil. Primeira coisa que você deve ter notado é que, de certa forma, colocamos todos os comandos que usaríamos em uma compilação parcial normal, como vimos anteriormente:

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

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.

Agora, precisamos entender o que estamos fazendo.
As primeiras linhas de nosso Makefile:

#
# Primeira tentativa de Makefile
#

São apenas comentários. No caso do make, os comentários utilizam o símbolo sustenido, ou sharp, ou qualquer outro nome que você já ouviu falar (vale até lasanha… :D). Como no C e em qualquer outra linguagem ou ferramenta, os comentários são simplesmente ignorados.

A linha seguinte:

all: roletrando

Indica que, para o make atingir o alvo all, ele tem como dependência que executar o alvo roletrando. Importante notar que, no make, uma dependência pode ser quaisquer arquivos E outros alvos. Isso é importante e veremos abaixo o por que.

Outra coisa: repare que esse alvo não possui tarefas. O que fizemos aqui é criar uma espécie de alvo “nulo”, ou phony. Simplesmente fizemos isso para “chamar a atenção” do make para cá. Normalmente ele executa o primeiro alvo de cima para baixo dentro do Makefile por padrão se ele não encontrar um alvo all. Em alguns casos, porém, você pode querer que o make gere vários alvos ao mesmo tempo (por exemplo, se você desenvolver um sistema composto por vários programas). Nesse caso, basta listar outros alvos como dependências em all.
Seguindo adiante, vemos uma entrada “completa” de um alvo (chamado também de regra):

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

Uma regra é composta pelo nome da regra (o alvo), uma ou mais dependências, e comandos que permita ao sistema obter essa dependência. O make precisa dessa informação para fazer suas tarefas. No caso acima:

  • O nome da regra (alvo) é roletrando;
  • As dependências são main.o regras.o letras.o;
  • O comando a ser executado é gcc -o roletrando main.o regras.o letras.o;

Perceba como a coisa está construída: quando usamos várias dependências, elas são separadas por espaço. Além disso, normalmente o nome da regra (ou alvo) é o nome do arquivo final a ser obtido por aquela regra (no nosso caso, o binário roletrando). O comando (ou comandos) devem seguir após a linha com a descrição do alvo e das dependências. Uma regra é separada da outra por uma linha em branco.
OK… Aqui vemos que o make sabe que, para obter o arquivo roletrando, ele precisa ter as dependência (ou seja, os arquivos) main.o, regras.o e letras.o. Além disso, ele sabe que, tendo esses arquivos, ele precisará executar o comando gcc -o roletrando main.o regras.o letras.o para obter o seu alvo e, portanto, cumprir a regra.
Mas até aqui, o Makefile não sabe como obter nenhuma das dependências em questão.
Para isso, incluímos as linhas seguintes:

main.o: main.c roletrando.h
    gcc -o main.o -c main.c

regras.o: regras.c roletrando.h
    gcc -o regras.o -c regras.c

letras.o: letras.c roletrando.h
    gcc -o letras.o -c letras.c

Aqui, vemos que colocamos as diversas regras para gerar cada um dos arquivos .o (objetos) necessários como dependência em roletrando. Perceba que as dependências são os nomes dos arquivos de fonte .c e (no nosso caso), o arquivo de cabeçalho roletrando.h. Isso visa garantir que o arquivo de cabeçalho exista e, no caso de ele ser modificado (por exemplo devido a uma função modificada) o código seja reconstruído de maneira adequada. Claramente isso vai depender de como funciona seu código fonte: haverá situações onde adicionar muitos arquivos fonte ou cabeçalhos gerará inconvenientes (como compilações desnecessárias). Mas isso não vem ao caso agora.
O make checará se (1) os arquivos indicados nas dependências existem ou se (2) eles são alvos resolvidos por outras regras. Se ambas as coisas não forem possíveis, ele irá dar uma mensagem de erro como a seguinte (No caso, renomeei o roletrando.h, exigido por main.o, para outro nome qualquer):

make: *** Sem regra para processar o alvo `roletrando.h’, necessário por `main.o’.  Pare.

Estando tudo OK, ao rodar-se o comando make (por via das dúvidas, antes apague todos os arquivos .o e o arquivo roletrando), ele irá mostrar algo como abaixo:

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

Vamos descrever o que aconteceu:

  1. make foi executado, procurou a regra default all e a encontrou. Viu que ela demandava um alvo chamado roletrando. Então foi para a regra roletrando;
  2. Em roletrando, ele viu que precisava dos arquivos (ou alvos) main.o, regras.o e letras.o. Antes de fazer qualquer coisa, ele irá verificar se ele tem uma regra que explique como obter esses arquivos.
  3. O make percebe que existe uma regra para main.o. Ela demanda os arquivos main.c e roletrando.h. Verificando que os dois existem, ele verifica se o arquivo indicado no alvo existe e é mais novo que os arquivos de dependência (o que indica que nenhum deles foi alterado). Caso contrário, ele irá executar as regras para obter-se uma nova versão do arquivo main.o (ou obtê-lo, caso não exista).
  4. Se houver alguma falha (por exemplo, um erro de sintaxe), o make irá abortar sua execução, não gerando nenhum binário e não criando o roletrando.
  5. Em caso de sucesso no passo 3 para main.o, o sistema repetirá o processo dos passos 3 e 4 para os demais arquivos de dependência regras.o e letras.o. Se algum deles exigisse um outro arquivo gerado por um outro alvo, os passos 3 e 4 seriam repetidos para tal arquivo e assim sucessivamente. make realiza uma pesquisa recursiva para ver se cada alvo necessáriao como dependência de cada outro alvo (incluindo all) foi obtido com sucesso.
  6. O passo 5 tendo sido realizado com sucesso para TODOS os alvos e suas dependências (incluindo outros alvos), o make irá executar os comandos indicados na regra roletrando para obter o arquivo roletrando, resolvendo a dependência all;
  7. Resolvida todas as dependências de all (roletrando e suas dependências), make irá se encerrar com sucesso;
Agora, após rodar make, você terá o arquivo roletrando. Se tentar rodar make novamente, verá que ele não fará absolutamente NADA, pois ele sabe que nenhuma das dependências de roletrando foi modificada (nehum dos .o), pois todas existem e são mais atuais que qualquer modificação em quaisquer um dos arquivos .c.
Vamos simular uma alteração em um dos arquivos .c (no caso, o arquivo regras.c). Para isso:

  • em Windows, abra o arquivo em um editor e o salve sem alterar nada;
  • no Linux, na linha de comando, dê o comando touch regras.c;

Execute então o comando make. Sua saída deve ser como a seguinte:

$ make
gcc -o regras.o -c regras.c

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

Por que isso aconteceu?
Make analisa o timestamp do arquivo (o registro de quando o mesmo foi alterado pela última vez) de todas as dependências necessárias e, caso o timestamp de uma dependência seja maior que o do arquivo do alvo (ou seja, a dependência em questão foi alterada), ele executa novamente a regra para ele e para todas as regras que tenha o alvo como dependência e assim sucessivamente. Portanto:

  1. O sistema identificou que a dependência regras.c de regras.o mudou e reconstruiu regras.o;
  2. Então o make percebeu que a dependência regras.o de roletrando mudou e, portanto, reconstruiu roletrando;

Ou seja, você não precisa se preocupar com re-executar manualmente os comandos em questão. Estando tudo OK, o próprio make irá executar e processar os arquivos adequados para gerar um binário.

Varíaveis e Macros no Makefile

Bem, agora temos um Makefile útil…
Mas agora pense em uma coisa…
Aqui colocamos regras, alvos e dependências para CADA arquivo fonte. Isso em sistemas pequenos é bem útil, mas conforme o sistema aumenta de tamanho e funcionalidades, obviamente aumenta o número de arquivos de código fonte. Ou seja, teríamos que fazer mais entradas de regras, mais dependências…
Além disso, imagine que você deixa de usar o gcc como compilador. Você provavelmente teria que modificar todos os alvos e regras para usar os padrões do novo compilador, o que seria um pesadelo!
O make, porém, salva a nossa vida, oferecendo variáveis e macros que nos ajudam a fazer com que o make realize tarefas “genéricas” (como gerar um objeto .o a partir de um fonte .c), além de embutir alguma inteligência para que ele descubra se existem novos arquivos .c no local e coisas do gênero. No caso, digite o seguinte Makefile e o salve no diretório do roletrando com o nome Makefile-1:

#
# Segunda tentativa de Makefile
#

C_COMP=gcc
FONTES=$(wildcard *.c)    # equivale a dizer FONTES=main.c regras.c letras.c
HEADERS=$(wildcard *.h)

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

Perceba que ele parece muito mais complexo do que o nosso Makefile anterior. Mas não se preocupe que iremos descrevê-lo aos poucos.
Primeiro vamos pegar as primeiras três linhas:

C_COMP=gcc

FONTES=$(wildcard *.c)    # “equivale” a dizer FONTES=main.c regras.c letras.c


HEADERS=$(wildcard *.h)

Aqui estamos criando três variáveis: C_COMP, FONTES e HEADERS. A primeira variável, C_COMP, indica o compilador que estamos usando. O nome poderia ser qualquer um (à exceção de alguns nomes que o make reserva para uso próprio, que não detalharemos aqui), e abaixo veremos o porque. No nosso caso atual, definimos que C_COMP=gcc, o que lembra uma atribuição em C. Estamos atribuindo à variável do Makefile C_COMP esse valor.
A variável seguinte, FONTES, é atribuída com $(wildcard *.c).
O que isso quer dizer, afinal de contas?“.
Quando você usa o símbolo de $, quer dizer que você está usando uma macro do make. As macros são certos recursos embutidos no make que estão disponíveis para facilitar sua vida na criação de um Makefile. Quando usamos $() cercando algo, estamos indicando uma macro de expansão, ou seja, vamos fazer com que o make entenda que o valor que está ali equivale ao de um comando ou variável previamente definido. No nosso caso, usamos o comando wildcard para pedir que o make realize uma busca no diretório onde o Makefile se encontra e localize todos os arquivos que seguem o padrão indicado. No nosso caso, $(wildcard *.c), pode ser lido como “procure todos os arquivos .c no diretório onde você está e os dê como valor onde você está”. Colocamos em seguida um comentário:

# “equivale” a dizer FONTES=main.c regras.c letras.c

para dizer o nosso resultante. No fim das contas, usar:


FONTES=$(wildcard *.c)

equivale a dizer, na situação atual

FONTES=main.c regras.c letras.c

Em seguida, criamos uma varíavel HEADERS, com o mesmo tipo de conteúdo de FONTES.
Uma coisa importante de dizer aqui, que não foi dita, é que as variáveis podem conter um valor só (como em C_COMP), ou vários valores separados entre si por espaço (como no caso da “expansão” de FONTES). Isso é importante em alguns casos, como veremos adiante.
Após estipularmos o alvo all, com dependência roletrando (igual ao nosso primeiro Makefile), definimos nosso alvo roletrando.
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

“Que coisa maluca é essa pelamordeus?!”
Calma. Aqui estamos usando várias macros para resumir nosso serviço.
Primeiro, vamos olhar a parte das dependências: $(FONTES:.c=.o)
O que isso quer dizer?
O make possui alguma inteligência para saber que determinados arquivos geram outros determinados arquivos. Por isso, ele é capaz de “traduzir” nomes por substituição simples se corretamente indicado. No caso, estamos usando o :.c=.o na frente de FONTES. Isso indica ao make para entender que, ao expandir FONTES ali, ele deve substituir todas as extensões .c para .o. Ou seja, ele será capaz de montar uma lista dos arquivos objetos necessários a partir da lista de arquivos fontes (que colocamos na variável FONTES, lembra).
Em seguida, temos a regra para obtermos os alvos: $(C_COMP) -o $@ $^
Temos a expansão de C_COMP, o que já deve ser claro, e um -o que é parâmetro do gcc (aqui fica uma sugestão: o ideal aqui é que outra variável contivesse todos os parâmetros da compilação, uma vez que eles podem mudar de compilador para compilador). Em seguida temos duas macros novas: $@ e $^. No caso, $@ deve ser lido como “o alvo a ser alcançado” e $^ indica “a lista de dependências passadas”.
Como tudo isso então se comporta, no fim das contas?

  1. make irá expandir a lista de dependências. Para isso, irá olhar o valor de FONTES, pegar qualquer valor listado em FONTES que termine com .c e modificará o valor dele nessa expansão para .o. Considerando que FONTES equivale, nesse momento a main.c regras.c letras.c, ele expadirá FONTES como main.o regras.o letras.o nas dependências de roletrando;
  2. Em seguida, ele irá montar a regra para atingir-se o alvo roletrando. Primeiro irá expandir a variável C_COMP (valendo gcc), colocará o -o na frente do mesmo (que, como não é uma macro ou variável é deixado como está), após isso inserindo o nome do alvo (no caso, roletrando) e a lista de dependências do mesmo! O valor final para a regra expandida será gcc -o roletrando main.o regras.o letras.o (ou valor similar: na realidade normalmente será gcc -o roletrando letras.o main.o regras.o, pois a expansão com wildcard em FONTES gera um lista em ordem alfabética dos arquivos cujo nome casem com o padrão desejado);

OK, então temos o comando para gerar o executável a partir dos objetos… Mas e quanto a geração dos objetos a partir dos fontes?
O make tem outra boa inteligência que é permitir que um alvo seja estabelecido a partir de um padrão, ou melhor, que haja uma regra padrão para alvos de um determinado tipo. Um exemplo de alvo assim está no final do nosso novo Makefile:

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

O símbolo de porcento (%) serve para indicar um padrão genérico que pode ser usado na regra como um todo. Colocando no alvo, ele diz ao make que ele sabe o que fazer para qualquer arquivo que obedeça o padrão em questão. No nosso caso, o alvo passa a ser “qualquer arquivo que termine em .o“. O bom é que esse valor passa a ser “salvo” e pode ser usado na dependência, como fizemos aqui: nossa dependência é %.c $(HEADERS), ou seja, “qualquer arquivo que tenha o mesmo nome do alvo, mas termine em .c” e o valor da variável HEADERS (que é um wildcard de arquivos .h).
A linha de regra tem algumas similaridades com o que temos na linha de regra para roletrando: primeiro expandimos C_COMP para gcc e temos um -c que é mantido como está (lembrando que -c apenas compila o arquivo oferecido como entrada, sem fazer link, gerando um arquivo objeto).
Em seguida, temos a macro $<. Essa macro pega o primeiro valor listado na lista de dependências e o usa como valor. Perceba que, se analisarmos a lista de dependências, teremos normalmente o arquivo .c e um ou mais header files. Por isso, usamos apenas o primeiro item da lista (é bom deixar o header file como dependência, para o caso de alteração, mas o que precisamos usar mesmo é o arquivo de fonte .c) como parâmetro aqui. Em seguida, temos a montagem do resto da regra, que é idêntica a como fizemos no alvo roletrando: -o como está e o $@ sendo expandido para o nome do alvo.
Como então se dá a execução passo a passo do Makefile desde que mandamos o comando make, nesse caso?
Ele irá expandir a regra roletrando e ver se todos os arquivos .o existem. Caso algum deles não exista ou seja mais antigo que o seu  .c, ele perceberá isso e, usando o alvo genérico %.o, irá gerar o .o necessário a partir do .c. Em seguida irá gerar o alvo roletrando.
Apague todos os arquivos .o e mande executar nosso Makefile com make -f Makefile-1. Sua saída provavelmente será algo como abaixo:

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

Ou seja, ele fará todo o serviço necessário sem você criar regras individuais e aumentar muito a complexidade do seu Makefile.
Um efeito colateral importante do uso de wildcards é que qualquer arquivo pego pelo padrão é usado como valor de variável (e como dependência, em geral). Isso pode acarretar problemas. Por exemplo, mantendo nosso Makefile como está, crie um arquivo .c em branco no diretório do roletrando. Não interessa o nome, apenas crie (para usuários de Linux, use o comando touch o_nome_do_meu_arquivo_aleatório.c), sem conteúdo nenhum. Após isso, execute o Makefile-1 como fizemos anteriormente. Você perceberá que ele irá achar o arquivo random.c na lista de fontes e colocará o arquivo random.o como parte das dependência de roletrando (devido à expansão), compilando random.c com a regra genérica.
Isso parece bobagem, uma vez que random.c está vazio… Mas tente, por exemplo, copiar um arquivo que tenha uma função main() para dentro de roletrando. Ao usar wildcards, você pode incorrer no risco de, cedo ou tarde, provocar erros de compilação e ligação devido à inclusão inadvertidade de um arquivo .c que contenha um código para uma função similar a uma que já exista em seu código. Tome muito cuidado quanto a isso.
Uma sugestão seria não utilizar as expansões com wildcards e apenas incluir os nomes de arquivo fontes, separados por espaço, em FONTES. Ainda demandaria alguma edição no Makefile após a inclusão de um novo arquivo, mas isso permitiria uma melhor administração dos fontes a serem compilados. Além disso, o resto das regras e macros do Makegile continuariam com sua utilidade mantida.
No caso de a pessoa tentar isso, ela não poderia normalmente dar ENTER e em seguida colocar o nome de um arquivo (considera-se que o valor da variável termina de ser lido em um caracter nova-linha). Porém, é possível “enganar” o sistema para que ele ache que um ENTER não é o iniício de uma nova linha. Para isso, coloque “\” antes de dar ENTER e siga em frente. O ENTER dado não será lido pelo make (será “escapado”) e o valor na linha de baixo será acrescentado aos demais normalmente. Por exemplo,

FONTES=fonte1.c fonte2.c fonte3.c
       fonte4.c

Faria com que você recebesse a mensagem de erro “Makefile:2: *** faltando o separador.  Pare.“. Mas:

FONTES=fonte1.c fonte2.c fonte3.c \
       fonte4.c

Seria correto e retornaria a expansão para fonte1.c fonte2.c fonte3.c fonte4.c.

Bem, estamos quase acabando esse assunto chato de Makefiles. Vamos apenas falar de um último tópico.

Phony targets (Alvos nulos)

Lá no início, quando falamos do alvo defaultall“, mencionamos o fato de ele ser um phony target, um alvo nulo…

Na verdade, phony targets são alvos que não vão gerar algum arquivo de saída. Um alvo assim tem como objetivo facilitar atividades que o desenvolvedor necessita fazer, como limpar os arquivos objeto antigos, empacotar um tarball (um arquivo compactado .tar.gz ou .tar.bz2), entre outros.
Uma coisa importante é que o mesmo não irá gerar arquivos, mas é bom por via das dúvidas indicar que esses alvos são Phony Targets para que o make não exija um arquivo com o nome do Phony Target ou para evitar que a existência de um arquivo com o mesmo nome do alvo faça com que a regra pare de funcionar. Para isso, utilizamos o alvo especial .PHONY para isolar todos os Phony Targets por meio dele. Caso contrário, podemos receber mensagens de erros como a seguinte, ao utilizar uma Phony Target clean, por exemplo:

make: *** Sem regra para processar o alvo `clean’.  Pare.

Abra seu arquivo Makefile-1 e adicione as seguintes linhas ao final do mesmo:

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Seu arquivo Makefile-1 deverá estar parecendo algo assim:

#
# Segunda tentativa de Makefile
#

C_COMP=gcc
FONTES=$(wildcard *.c)    # equivale a dizer FONTES=main.c regras.c letras.c
HEADERS=$(wildcard *.h)

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Perceba que ainda não definimos clean e tar como Phony Thargets. Agora, vamos definir nossas Phony Thargets. Antes de all, inclua a seguinte regra:

.PHONY: all clean tar

Perceba que agora definimos eles como Phony Targets, perceba que incluimos all. Essa é uma boa prática, incluir all como Phony Target para evitar problema maiores no caso da existência de uma arquivo all. Teste as regras clean e tar, lembrando de usar -f Makefile-1 para indicar o arquivo de Makefile desejado (se você colocar essas regras no arquivo Makefile padrão, não precisará do -f). Muitos softwares livres incluem em seus makefiles Phony Targets como clean, install, package, etc… Tente criar vários Phony Targets para fazer operações de instalação, remoção, limpeza dos objetos antigos, compactação, e por aí afora.
O nosso Makefile “genérico” deve ficar como o abaixo. Esse é um ótimo Makefile padrão para ser adotado no dia a dia:

C_COMP=gcc
FONTES=$(wildcard *.c) 
HEADERS=$(wildcard *.h)

.PHONY: all clean tar

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Para saber mais

Não pretendo falar por um bom tempo mais sobre Makefiles. Essa introdução deve ser o suficiente para o dia a dia e para o que precisaremos por um LONGO TEMPO no nosso “curso”. Se você desejar maiores informações, abaixo alguns links úteis:

Além dos seguintes tutoriais, onde me baseei para escrever esse artigo:

Bem, agora chega de falarmos de ferramentas. Prometo na próxima “aula” voltar à programação em C, com estruturas e tipos do usuário. Até lá!

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.