AULA 25 - Programação 1 - Engenharia

De MediaWiki do Campus São José
Ir para: navegação, pesquisa

Objetivos

Após esta aula o aluno deverá ser capaz de:

  • Saber construir protótipos de funções, declarar variáveis com extern e construir headers (arquivos .h);
  • Saber colocar guardas nos headers;
  • Aplicar os princípios de programação modular na construção de programas em C;
  • Usar o IDE Clion para construir projetos com múltiplos arquivos.

Programação Modular

Programação Modular (Wikipedia):

"Programação Modular é uma técnica para projeto de software que enfatiza a separação das funcionalidades de um programa em módulos. Cada módulo possui todas funções necessárias para para cumprir uma determinada parte funcional de um programa." Um módulo deve ter uma interface bem definida com outros módulos. A implementação do módulo deve ficar separada da interface. As funções dentro do módulo possuem uma forte conexão.

Referências

Vantagens da Programação Modular [1]

  • Reutilização de módulos em outros projetos;
  • Redução do tamanho de arquivos fonte;
  • Mudanças na implementação de um módulo na impacta diretamente nos demais módulos, desde que as interfaces não sejam modificadas;
  • Auto-Documentação pois as interfaces especificam o que os módulos fazem;
  • Facilita a depuração;
  • Compilação mais rápida.

Mecanismos do C para suporte a Programação Modular

Os seguintes mecanismos proporcionam a programação modular no C:

  • Funções, retorno de valores em funções e passagem de parâmetros;
  • Construção de protótipos de funções;
  • Compilação separada de arquivos e posterior "linkagem";
  • Diretiva 'extern';
  • Construção de arquivos headers (.h), que se constituem na especificação de interface de módulos;
  • Construção implementação de módulos (.c) incluindo as interfaces (#include <arquivo.h>)

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);
  return 0;
}

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

A compilação acima teve problemas pois o compilador não sabia quem era "swap_var()" quando começou a compilar "a função main()". Normalmente o compilador lê o arquivo fonte do início em direção ao final. 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);
}
O problema foi contornado construindo primeiro a função "swap_var()". Então sempre devemos declarar tudo antes do main() ???

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

Devemos usar PROTÓTIPO de funções. Um protótipo de uma função contém somente o tipo de retorno, o nome da função e seus parâmetros. A implementação da função não é fornecida no protótipo. Vamos a um exemplo:

/* 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;
}

Múltiplos arquivos no C: usando protótipos, declarações de variáveis com extern e HEADERS

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 (ver Programação Modular acima) 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 de INTERFACE (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 main.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 main.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 é OPCIONAL para funções.

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

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

Note que beta() usa a função alfa() que está implementada em main.c. Ela também faz uso da variável global y definida em main.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 */

Exemplo : Sistema de Gerenciamento de Salas, Usuários (alunos) e Turmas

Considere um sistema de um sistema escolar que visa construir turmas de alunos e alocar estas turmas em salas da escola. Este sistema possivelmente terá armazenará de forma separada as alunos, as salas da escola e as turmas formadas com respectiva alocação de turmas as salas. Sem pretender construir um exemplo completo pode-se imaginar as seguintes funcionalidades em separado:

  • gerenciar alunos: inserir, remover, listar e editar alunos;
  • gerenciar salas: inserir, remover, listar e editar salas;
  • gerenciar turmas: criar, remover, listar e editar turmas;
  • alocar salas: associar automaticamente ou de forma manual salas as turmas;
Um Esqueleto SEM DIVISÃO EM MÓDULOS para este problema
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

/* Autores: Professores PRG1 - IFSC -SJ   */
/******************************************/
/*
Para fins didáticos estão concentradas aqui
todas as funções para exemplificar a elaboração
de menus em modo texto.
Em versões finais o código deve ser organizado em arquivos .h
e .c, agrupando as funções conforme a funcionalidade.
Por exemplo: em um arquivo salas.c colocar as funções
de gerenciamento de sala, com os protótipos no salas.h
*/
/******************************************/

/******************************************/
/* Protótipos de funções  - colocar em .h */
/******************************************/

void gerenciar_usuarios();
void gerenciar_salas();
void adicionar_usuario();
void remover_usuario();
void adicionar_sala();
void remover_sala();

/******************************************/
/* Variáveis Globais                      */
/******************************************/

int total_usuarios;

/* a fazer */

/******************************************/
void menu_principal()
{
    system("clear");
    printf("**********************************\n");
    printf("Menu Nível 1\n\n");
    printf("1. Gererenciar Usuários\n");
    printf("2. Gerenciar Salas\n");
    printf("3. Gerenciar  Turmas\n");
    printf("4. Gerenciar  Associação Turma Sala\n");
    printf("5. Sair\n");
    printf("**********************************\n");
}
/******************************************/
int main()
{
    int opcao;

    do {
        menu_principal();
        scanf("%d",&opcao);
        switch (opcao) {
            case 1: gerenciar_usuarios();
                break;
            case 2: gerenciar_salas();
                break;
            case 3: printf("A fazer...n");
                break;
            case 4: printf("A fazer...\n");
                break;
            case 5: printf("Finalizando Programa...\n");
                break;
            default: printf("Opção inexistente. Tente novamente...\n");
                sleep(1); /* tempo somente para fins de visualização */
                break;
        }

    } while (opcao != 5);
    return 0;
}

/***************************************************/
/* Esta funções deveriam estar em arquivo separado */
/***************************************************/
void gerenciar_usuarios()
{
    int opcao;

    do {
        system("clear");
        printf("**********************************\n");
        printf("Menu Nível 2 - Gerenciar Usuários\n\n");
        printf("1. Adicionar Usuário\n");
        printf("2. Remover Usuário\n");
        printf("3. Sair do Gerenciar Usuários\n");
        printf("**********************************\n");
        scanf("%d",&opcao);
        switch (opcao) {
            case 1:
                adicionar_usuario();
                break;
            case 2:
                remover_usuario();
                break;
            case 3: printf("Finalizando Gerenciar Usuários...\n");
                sleep(1); /* tempo somente para fins de visualização */
                break;
            case 4:
                /* gerenciar turmas: a fazer */
                break;
            case 5:
                /* associar turmas a salas: a fazer */
            default:
                printf("Opção inexistente. Tente novamente...\n");
                sleep(1); /* tempo somente para fins de visualização */
                break;
        }

    } while (opcao != 3);
}

void adicionar_usuario()
{
    /* aqui poderia ter um menu nível 3 */
    printf("adicionando usuário\n ");
    sleep(1);
    printf("usuário adicionado\n ");
    sleep(1);
}

void remover_usuario()
{
    /* aqui poderia ter um menu nível 3 */
    printf("removendo usuário\n ");
    sleep(1);
    printf("usuário removido\n ");
    sleep(1);
}


/***************************************************/
/* Esta funções deveriam estar em arquivo separado */
/***************************************************/

void gerenciar_salas()
{
    int opcao;

    do {
        system("clear");
        printf("**********************************\n");
        printf("Menu Nível 2 - Gerenciar Salas\n\n");
        printf("1. Adicionar Sala\n");
        printf("2. Remover Sala\n");
        printf("3. Sair do Gerenciar Salas\n");
        printf("**********************************\n");
        scanf("%d",&opcao);
        switch (opcao) {
            case 1: adicionar_sala();
                break;
            case 2: remover_sala();
                break;
            case 3: printf("Finalizando Gerenciar Sala...\n");
                sleep(1); /* tempo somente para fins de visualização */
                break;
            default: printf("Opção inexistente. Tente novamente...\n");
                sleep(1); /* tempo somente para fins de visualização */
                break;
        }

    } while (opcao != 3);
}

void adicionar_sala()
{
    /* aqui poderia ter um menu nível 3 */
    printf("adicionando sala\n ");
    sleep(1);
    printf("sala adicionada\n ");
    sleep(1);
}

void remover_sala()
{
    /* aqui poderia ter um menu nível 3 */
    printf("removendo sala\n ");
    sleep(1);
    printf("sala removida\n ");
    sleep(1);
}

Olhando o exemplo acima fica claro que:

  • existem funções fortemente relacionadas ao gerenciamento de usuários: criar_usuário(), remover usuário(), listar usuário(). Para fins de organização é fortemente recomendado que estas funções sejam agrupadas em arquivo separado (um arquivo .c).
  • da mesma forma, existem funções fortemente relacionadas com gerenciamento de salas. Tais funções podem claramente ser colocadas em arquivo separado;
  • Os protótipos das funções que serão vistas por outros módulos (arquivos) deverão ser colocadas em arquivos cabeçalho (.,) que funcionam como INTERFACES do módulo.

Supondo que existam funções relacionadas ao gerenciamento de salas, turmas e de alocação de turmas, a mesma abordagem deve ser usada. A figura abaixo mostra uma divisão para salas e turmas. O programa principal permanece em um arquivo separado main.c.

FigurasMultiplosArquivos.png

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 variá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 (protótipo) da função no header. Neste caso a palavra chave extern é opcional.

Ver discussão em: [2]

O problema de múltiplas inclusões de headers [3]

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