AULA 23 - Programação 1 - Graduação

De MediaWiki do Campus São José
Revisão de 16h00min de 22 de maio de 2015 por Douglas (discussão | contribs) (Separação do programa em múltiplos arquivos)
Ir para: navegação, pesquisa

Separação do Programa em Múltiplos Arquivos

Muitas vezes o programa se torna grande demais e o uso de múltiplos arquivos fonte torna-se necessário. A divisão também permite o desenvolvimento organizado do projeto, onde cada arquivo contém um grupo de instruções e variáveis globais relacionados com uma determinada parte do sistema (subsistema ou módulo).

É necessário, no entanto, criar arquivos cabeçalho (headers ou .h) para declarar protótipos de funções e variáveis globais cuja visibilidade deve ser exportada para outros arquivos. Exemplo:

Seja um projeto com dois arquivos. No arquivo t1.c existe o seguinte conteúdo:

#include <stdio.h>

#include "t2.h"

int y;

void alfa(int x)
{
  printf("x=%d\n",x);
}

main()
{
  y = 20;
  alfa(2);
  beta();
}

Observe que a função main() usa as funções a alfa() e beta(). Mas beta() não está implementada em t1.c. Ela está implementada em um outro arquivo t2.c. Neste caso, para que o compilador possa validar os parâmetros e o retorno da função beta() é necessário incluir um arquivo header t2.h que possui tais informações.

Arquivo t2.h:

extern void beta();
Note o uso da palavra extern para informação do PROTÓTIPO da função.

O arquivo t2.c possui a implementação de beta():

#include <stdio.h>
#include "t1.h"
void beta()
{
  alfa(23);
  printf ("y=%d\n",y);
}

Note que beta() usa a função alfa() que está implementada em t1.c. Neste caso ela inclui o arquivo t1.h que contém o protótipo de alfa():

extern int y;
extern void alfa(int x); /* extern aqui é opcional */

REGRAS GERAIS PARA CONSTRUÇÂO DO HEADER

  1. Se você definir uma variável global em arquivo fonte, digamos no t1.c, e deseja que outros arquivos "vejam" está variável então coloque uma declaração desta variável em um arquivo header (t1.h) com a palavra chave extern.
 NOTA: note que existe uma diferença entre definir e declarar. Se você cria uma variável global, por exemplo, int x; no
 arquivo fonte t1.c, você está definindo a variável. Será alocada uma área de memória para esta veriável. Se você declara
 a variável no t1.h usando o extern, você simplesmente está informando que esta variável existe e qual tipo possui.
  1. Nunca defina a variável no header pois estará abrindo a possibilidade para que cada arquivo que inclua este header crie uma instância desta variável;
  2. Se você quer publicar (informar) outros arquivos fonte sobre funçṍes de um arquivo, por exemplo, a função beta() do exemplo passado, então insira uma declaração da função no header. Neste caso a palavra chave é opcional.

Ver discussão em: [1]

O problema de múltiplas inclusão de headers [2]

Seja um arquivo avo.h:

struct familia {
};

E um arquivo pai,h:

#include "avo.h"

Finalmente um arquivo filho.h:

#include "avo.h"
#include "pai.h"

Considere o fonte filho.c

#include "filho.h"

Se este arquivo for compilado, teremos um erro porque devido a dupla inclusão do header avo.h a estrutura familia estará sendo duplicada.

Compilação Condicional e Guard Headers

As diretivas de pré-compilação #ifndef e #ifdef são usadas quando queremos compilar "condicionalmente" determinado bloco de código.

Por exemplo, podemos criar guard headers usando a diretiva #ifndef de forma a inserir unicamente um código de um header. No exemplo anterior, o arquivo avo.c poderia ser "guardado" da forma:

#ifndef AVO_H

#define AVO_H

struct familia {
};

#endif

Neste caso, guando o arquivo filho.c for compilado, o arquivo avo.h é duplamente incluído. O compilador, ao encontrar a diretiva #ifndef AVO_H da primeira inclusão, observa que o símbolo AVO_H não foi definido ainda (com um #define). Neste caso, ele compila o código que se segue. No código que se segue o símbolo AVO_H é definido. Ao encontrar a segunda inclusão de de avo.h, o compilador novamente se depara com a diretiva #ifndef AVO_H. Neste momento, o símbolo AVO_H já foi definido e o compilador ignora o código que se segue até encontrar o #endif. É a compilação condicional...

É uma boa prática proteger os arquivos headers com uma guarda de forma a evitar problemas posteriores.

Uso do make

O utilitário make é muito usado para gerenciar a compilação de múltiplos arquivos. #endifamos ver o seu uso através de um exemplo.

Seja um programa executável programa_bin formado a partir de dois arquivos fontes: main.c e parte2.c

Seja main.h um header de main.c e parte2.h um header de parte2.c. Os arquivos são:

Arquivo main.h:

#ifndef MAIN_H
#define MAIN_H

extern void alfa(char *p);

#endif

Arquivo main.c:

#include <stdio.h>
#include "parte2.h"
#include "main.h"

void alfa(char *p)
{
  printf("ALFA: %s\n", p);
}

main()
{
  alfa ("Alo Mundo - Invocado de main");
  beta ("Alo Mundo - Invocado de main");
}

Arquivo parte1.h:

#ifndef PARTE2_H
#define PARTE2_H

extern void beta(char *p);

#endif

Arquivo parte2.c:

#include <stdio.h>
#include "parte2.h"
#include "main.h"

void beta(char *p)
{
  printf("BETA: %s\n", p);
  alfa ("Alo Mundo - Invocado de BETA");
}

Seja o arquivo Makefile:

#arquivo Makefile
programa_bin: main.o parte2.o
	gcc main.o parte2.o -o programa_bin

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

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

clean:  
	rm *.o
	rm -r ./install

install:
	mkdir install
	mv programa_bin ./install

Para chamar o make basta fazer:

 make

O make interpreta o Makefile do diretório corrente tenta construir o arquivo programa_bin usando a primeira regra do arquivo. Se este estiver atualizado nada será feito. Caso ele observe que main.o ou parte2.o estão desatualizados, ele tenta remontá-los usando as respectivas regras. Note que ao lado do nome da regra estão as dependências.