Motivação: Por que modularizar ?
- O que acontece se o programa é composto por muitas classes ?
- Todas ficam dentro do mesmo código-fonte
- Código pode se tornar muito extenso e difícil de gerenciar
- Isso também cria dois outros problemas:
- Dificulta a reutilização das classes em outros programas
- Dificulta o trabalho em equipe
- Então, qual é a solução ?
Solução: Modularização
Agrupam-se as classes relacionadas em módulos. Para cada módulo são utilizados dois arquivos:
- Declarações
- Sufixo .h
- Contém apenas as declarações das classes e das constantes
- Implementação
- Sufixo .cpp
- Contém a implementação das classes declaradas no arquivo .h correspondente
- Inclui o arquivo .h correspondente
-
Para usar uma classe em outro módulo, basta então incluir o arquivo .h correspondente
Declaração da classe Carro
Para exemplificar, utilizaremos a classe Carro trabalhada anteriormente.
Primeiro, escrevemos a declaração da classe Carro no arquivo Carro.h:
class Carro {
public:
Carro();
bool abastecer(float litros);
bool mover(float km);
float getTanque();
float getDistancia();
private:
float tanque;
float distancia;
};
Declaração da classe Carro (2)
Porém, se por engano esse arquivo for incluído mais de uma vez, haverá uma duplicidade
de todas as definições. Como isso pode acontecer facilmente, acrescentamos 3 linhas no
arquivo .h:
#ifndef CARRO_H
#define CARRO_H
class Carro {
public:
Carro();
...
};
#endif
- Essas linhas contêm comandos do pré-processador, que definem um símbolo CARRO_H caso este ainda não
exista.
- Se existir, é porque o arquivo já foi incluído e portanto, o pré-processador pula o código até o #endif.
Implementação da classe Carro
A seguir, escrevemos o arquivo Carro.cpp, que contém a implementação
dos métodos da classe Carro.
#include "Carro.h"
Carro::Carro() {
tanque = 0;
distancia = 0;
}
bool Carro::abastecer(float litros) {
tanque += litros; // tanque = tanque + litros;
...
}
...
- Note que este arquivo deve incluir o arquivo Carro.h
Programa Principal
Finalmente, o programa principal deve ser implementado em um arquivo separado, como por exemplo
main.cpp:
#include <iostream>
#include "Carro.h"
using namespace std;
int main()
{
Carro carro1, carro2;
carro1.abastecer(20);
carro2.abastecer(30);
bool moveu1 = carro1.mover(200);
if(moveu1==false)
cout << "Carro 1: Não há combustível suficiente!" << endl;
...
}
- Observe que o arquivo Carro.h deve ser incluído, pois contém a definição de classe.
Compilação dos Módulos
Para compilar o programa completo, é preciso indicar todos os fontes na linha de comando:
- g++ -o main main.cpp Carro.cpp
Porém, essa estratégia tem algumas implicacões:
- Todos os módulos são SEMPRE compilados novamente
- Mas e se apenas a implementação do carro ?
- E se apenas o programa principal mudar ?
Para um programa tão pequeno, isso não faz tanta diferença...
Mas se o programa for composto por muitos módulos, compilá-los separadamente pode ter um
custo considerável, além de não ser muito prático.
Compilação Modular: Makefiles
Um Makefile é um roteiro de compilação:
- Informa as dependências entre os arquivos
- Indica os comandos necessários para a compilação
O nome do arquivo deve ser preferencialmente Makefile.
Para utilizá-lo, basta digitar o comando make no terminal.
all: main
main: main.o carro.o
g++ -o main main.o carro.o
main.o: main.cpp
g++ -c main.cpp
carro.o: carro.cpp
g++ -c carro.cpp
- O comando g++ -c ... apenas compila o módulo especificado, produzindo
um arquivo .o, que é o chamado código-objeto (sem ligação com bibliotecas).
Compilação Modular: Makefiles (2)
Os comandos de compilação não parecem meio repetitivos ? Podemos melhorar
o Makefile, criando regras genéricas:
CXXFLAGS = -Wall -g # Opções do compilador: todos warnings e debug info
PROG = main
FONTES = main.cpp carro.cpp
OBJETOS = $(FONTES:.cpp=.o)
$(PROG): $(OBJETOS)
g++ $(CXXFLAGS) $(OBJETOS) -o $@
clean:
-@ rm -f $(OBJETOS) $(PROG)
- PROG = ... define o nome do programa executável
- FONTES = ... define todos os módulos
- OBJETOS = ... define como transformar um .cpp em um .o
- clean permite apagar os arquivos .o e executável gerados (make clean)
Compilação Modular: Makefiles (3)
Mas e como especificar dependências automáticas para os .h ?
Uma forma é incluir uma regra que use o comando makedepend:
...
clean:
-@ rm -f $(OBJETOS)
depend:
makedepend -- ${CFLAGS} -- ${FONTES}
Ao digitarmos make depend, o comando makedepend será ativado, e verificará
as dependências de cada módulo, incluindo estas no final do próprio Makefile:
...
depend:
makedepend -- ${CFLAGS} -- ${FONTES}
# DO NOT DELETE
main.o: carro.h
carro.o: carro.h
Compilação Modular: Alternativas ao make
Já existem alguns sistemas alternativos ao make:
- SCons - escrito em Python,
oferece uma sintaxe mais flexível, é multiplataforma e tem diversos recursos interessantes.
- CMake (Cross-Platform Make) - este
sistema tem o objetivo de gerar Makefiles adaptados para a plataforma de compilação.
- Boost.Build - específico
para C++, também multiplataforma e com uma sintaxe simples.
- GNU Build System (autotools) - sistema
mais utilizado atualmente para instalação de software no Linux/Unix (Com software adicional instalado, pode ser
utilizado também no Windows).
- Obs: projetos criados em IDEs como Visual Studio ou Code::Blocks simplesmente automatizam essa tarefa.
Exercícios
- Considere a existência de uma classe Bomba, que representa uma bomba de combustível em um posto.
- Imagine os atributos e operações necessárias para representar objetos dessa classe:
- Receber combustível (por exemplo, do caminhão-tanque)
- Retirar combustível (para abastecer um carro)
- Obter a quantidade de combustível armazenada
- Com base nessa estrutura, crie um programa principal que:
- Instancia dois carros e uma bomba.
- Enche a bomba com 80 litros.
- Tenta abastecer o primeiro carro com 50 litros.
- Tenta abastecer o segundo carro com mais 50 litros.
- Modularize o código-fonte, criando um arquivo para cada classe, e um arquivo separado para o programa principal.
Exercícios (2)
- Melhore a implementação do exercício anterior:
- Do jeito que está, a bomba permite que o combustível seja retirado, mas
não há uma relação direta com o carro que irá recebê-lo.
- Implemente um método abastecerCarro, que
deverá receber uma referência para um objeto Carro,
bem como a quantidade de combustível desejada.