Mudanças entre as edições de "AULA 25 - Programação 1 - Engenharia"

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar
Linha 32: Linha 32:
 
}
 
}
  
main()
+
int main()
 
{
 
{
 
   int a=5,b=2;
 
   int a=5,b=2;

Edição das 08h26min de 28 de novembro de 2019

Conceito de PROTÓTIPO de função

Note que a ordem de construção de funções impacta na compilação de um programa:

Tente compilar:

int main()
{
  int a=5,b=2;
  swap_var(&a,&b);
}

void swap_var(int *p1, int *p2)
{
  int x;
  x=*p1;
  *p1=*p2;
  *p2=x;
}

E agora tente compilar:

void swap_var(int *p1, int *p2)
{
  int x;
  x=*p1;
  *p1=*p2;
  *p2=x;
}

int main()
{
  int a=5,b=2;
  swap_var(&a,&b);
}
Então sempre devemos declarar tudo antes do main() ???

Com certeza não, até porque este probblema pode aparecer em outras funções que não sejam o main...

Devemos usar PROTÓTIPO de funções:

/* protótipo de ''swap_var'' */
void swap_var(int *p1, int *p2);

main()
{
  int a=5,b=2;
  swap_var(&a,&b);
}

void swap_var(int *p1, int *p2)
{
  int x;
  x=*p1;
  *p1=*p2;
  *p2=x;
}

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. É OPCIONAL para funções.

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. Ela também faz uso da variável global y definida 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.
  2. 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;
  3. 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 extern é opcional.

Ver discussão em: [1]

O problema de múltiplas inclusões 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. Vamos 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 parte2.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.

NOTE que se alterar um único arquivo, mesmo um header, o make se encarregará de verificar as dependenências e recompilar o que for necessário:

touch parte2.h
make

Mais detalhes do make ver em https://www.gnu.org/software/make/manual/html_node/Simple-Makefile.html#Simple-Makefile