.H
), fonte
(.C
), objecto (.o
), biblioteca ou arquivo (.a
),
e executável.inline
.namespaces
).
Modularização em pacotes.Desenhar no quadro o código abaixo. Dizer para não passarem!
No primeiro semestre fomos sempre desenvolvendo programas que consistiam num único ficheiro C++. Hoje vamos ver que é possível um programa consistir em vários ficheiros.
A divisão de um programa em vários ficheiros permite aquilo a que se chama "compilação separada". Tem várias vantagens face à solução de um ficheiro único:
A modularização física corresponde à divisão de um programa em ficheiros. Infelizmente, os módulos deste nível de modularização são conhecidos por módulos simplesmente, o que se pode tornar um pouco confuso. Dependendo do contexto, a palavra "módulo" pode ter o seu sentido lato (um módulo qualquer, que pode ser uma função, uma classe, um módulo físico, ou um pacote), ou o seu sentido estrito de um módulo físico.
Os módulos físicos correspondem tipicamente a um par de ficheiros fonte: o ficheiro de implementação (com extensão .C) e o ficheiro de interface ou cabeçalho (de extensão .H). No primeiro indica-se como se usam as ferramentas disponibilizadas pelo módulo e no segundo implementam-se as ditas ferramentas. Quem usa o módulo normalmente limita-se a usar o ficheiro de interface.
Finalmente, os pacotes são tipicamente constituídos por vários módulos físicos, com ferramentas relacionadas. Em C++ os pacotes não são suportados directamente. Mas existe uma construção sintáctica no C++ que permite organizar as ferramentas quase como num pacote: os espaços nominativos.
Quer a modularização física quer os espaços nominativos são assunto desta aula. Antes de avançarmos, porém, vamos estudar as várias fases da construção de um programa (um ficheiro executável) a partir de vários módulos.
programa.C
:
#include "matematica.H"
#define TURMAS_GRANDES
#ifdef TURMAS_GRANDES
int const número_máximo_de_alunos = 100;
#else
int const número_máximo_de_alunos = 30;
#endif
int main()
{
int notas[número_máximo_de_alunos];
....
cout << média(notas, 10) << endl;
}
matematica.H
:
double média(int const m[], int n);
matematica.C
:
Dizer que o ficheiro
#include "matematica.H"
double média(int const m[], const int n)
{double soma = 0.0;
for(int i = 0; i != n; ++i)
soma += m[i];
return soma / n;
}
programa.C
está gatado, pois
não tem #include <iostream>. Pedir-lhes para imaginarem
que se podia usar o cout de qualquer forma...
A construção de uma ficheiro executável a partir de um conjunto de ficheiros fonte passa para 3 fases:
O pré-processamento pega num ficheiro fonte, tipicamente num ficheiro de implementação .C, e gera ficheiro pré-processado, com extensão .ii. O ficheiro pré-processado continua a conter código C++, mas o pré-processador faz-lhe algumas alterações. O comando para pré-processar é:
O compilador pega num ficheiro pré-processado e tradu-lo para linguagem máquina. Mas não gera um ficheiro executável! Gera um ficheiro objecto, que além de conter o código máquina contém informação adicional acerca desse código máquina. Os ficheiros objecto têm extensão .o. O comando para compilar é:c++ -E programa.C -o programa.ii
c++ -E matematica.C -o matematica.ii
c++ -Wall -g -c programa.ii
c++ -Wall -g -c matematica.ii
Explicar que na segunda versão a compilação é precedida pelo pré-processamento).c++ -Wall -g -c programa.C
c++ -Wall -g -c matematica.C
Note-se que quer o pré-processamento quer a compilação agem SOBRE UM SÓ FICHEIRO! Daí que se fale em compilação separada: os ficheiros .C são todos compilados separadamente...
O fusor é que funde todos os ficheiros objecto do programa e gera o ficheiro executável.
g++ -o programa programa.o matematica.o
Vamos ver cada uma destas fases mais em pormenor:
PRÉ-PROCESSAMENTO
Explicar que o pré-processor copia do ficheiro .C para o ficheiro .i, mas sempre que encontra uma linha começada por # interpreta-a. São as chamadas directivas de pré-processamento.
Dizer que o pré-processador é uma ferramenta pré-histórica que se mantém no C++ por causa das suas origens no C.
Dizer há muitas directivas do pré-processador, mas só algumas nos interessam.
Explicar #include. Explicar diferença entre <> (ficheiro de interface "oficial") e "" (ficheiro de interface "nosso"). Dizer que includes podem ter includes.
Explicar vagamente macros como "variáveis" do pré-processador.
Explicar compilação condicional. Dizer que nas aulas práticas se verá uma melhor aplicação para ela.
COMPILAÇÃO
A compilação de um ficheiro C++ (já pré-processado) consiste na sua tradução para linguagem máquina e é feita em três passos pelo menos:
O resultado da compilação não é apenas código máquina. É algo mais. Simplificando grosseiramente as coisas, tem associadas duas tabelas: uma das disponibilidades, e outra das necessidades.
Explicar que o ficheiro media.o tem na tabela das disponibilidades a função main e na das necessidades a função média. Por outro lado o ficheiro matematica.o tem na tabela das disponibilidades a função media e na tabela das necessidades não tem nada.
Cada ficheiro objecto, por si só, não é executável. Por exemplo, ao ficheiro matematica.o falta a função main() e ao ficheiro media.o falta a função media(). A informação do que cada ficheiro objecto tem e do que necessita é o que possibilita ao fusor fazer o seu trabalho.
FUSÃO
Na fase da fusão os ficheiro objecto são todos fundidos num único executável.
Fazer boneco mostrando as três fase que evidencie bem as compilações separadas e a fusão final.
O fusor basicamente concatena o código máquina de cada ficheiro objecto depois de verificar que:
Dizer que mesmo no semestre passado não trabalhavam só com um ficheiro!
O programa mais simples tem pelo menos um #include e precisa de ferramentas da chamada biblioteca padrão do C++. O que acontece é que o fusor funde os nosso ficheiros objecto automaticamente com a biblioteca. Mais, nós podemos criar as nossas próprias bibliotecas! Podemos pegar num conjunto de ficheiros objecto e arquivá-los num ficheiro de arquivo ou biblioteca: prefixo lib e extensão .a.
Para arquivar ficheiros objecto não se usa o fusor: usa-se o arquivador. O programa arquivador invoca-se normalmente como se segue:
ar ru ficheiros.o ...Já vimos como era feita a compilação separada. Vamos voltar à modularização física. Um módulo físico corresponde tipicamente a dois ficheiros fonte: o ficheiro de implementação e o de interface. É normal que o módulo onde está o main() não tenha ficheiro de interface. Assim, no nosso exemplo temos dois módulos:
matematica: ficheiros fonte matematica.C e matematica.HQue colocar em cada módulo? O que se coloca em cada um dos ficheiro fonte?
programa: ficheiro fonte programa.C
A primeira pergunta é mais difícil de responder duma forma taxativa. Mas pode-se dizer que cada módulo deve corresponder a um conjunto muito coeso de funções, procedimentos, classes, etc. É típico que um módulo corresponda a uma classe apenas e algumas funções e procedimentos membro associadas a essa classe. Por vezes há mais do que uma classe, quando essas classes estão muito interligadas. Por vezes não há classe nenhuma, como no nosso módulo matemática, que provavelmente só conteria funções e procedimentos para operações matemáticas.
A outra pergunta é mais fácil de responder. Em cada módulo, o que se coloca no ficheiro de interface e o que se coloca no ficheiro de implementação? Ao ficheiro de interface só deve ir parar aquilo que é estritamente necessário para se poder usar o módulo. Em particular:
E falta falar do nível de modularização seguinte: os pacotes.
Explicar problema da colisão de nomes. Dar exemplo com empresa que compra duas bibliotecas de funções, uma para lidar com a logística e outra com a contabilidade e que quer usá-las simultaneamente num programa.
Manda construir o programa e... o fusor queixa-se que cada uma das bibliotecas define um procedimento consolida(). Que fazer? Pedir aos fornecedores para alterarem o nome do procedimento? E eles mudam? E quanto tempo levam?
O C++ tem um mecanismo que evita este problema da colisão de erros e que, simultaneamente, pode ser usado para agrupar as ferramentas num nível mais elevado de modularização: os espaços nominativos (namespaces).
Podem-se colocar vários módulos num mesmo espaço nominativo (e vice-versa, mas não é tão útil...). Por exemplo, se as empresas fornecedores (Verão Software e Inverno Software) tivem sido mais competentes, todos os módulos das suas bibliotecas estariam num espaço nominativo correspondente ao nome da empresa. Por exemplo:
logistica.H
namespace VerãoSoft {
...
void consolida();
...
}
logistica.C
...
void VerãoSoft::consolida() {
...
}
...
contabilidade.H
namespace InvernoSoft {
...
void consolida();
...
}
contabilidade.C
Agora a nossa empresa não tem problemas. Pode escrever:...
void InvernoSoft::consolida() {
...
}
...
Explicar directiva de utilização para passar todos os nomes para o âmbito de visibilidade corrente. Explicar versão menos dramática da declaração de utilização. Relembrar std::. Explicar que o problema em Linux (pr enquanto) é que a biblioteca não está em std:: mas deveria estar...#include <contabilidade.H>
#include <logistica.H>
int main()
{
VerãoSoft::consolida();
InvernoSoft::consolida();
}
Cada uma dessas empresas podia ter na biblioteca vários pacotes (cada um com vários módulos físicos), com objectivos diferentes. Nesse caso cada um teria espaços nominativos diferentes:
logistica_básica.H
namespace VerãoSoft {
namespace Logística {
...
void consolida();
...
}
}
logistica_básica.C
...
void VerãoSoft::Logística::consolida() {
...
}
...
contabilidade_avançada.H
namespace InvernoSoft {
namespace Contabilidade {
...
void consolida();
...
}
}
contabilidade_avançada.C
Agora a nossa empresa deve escrever:...
void InvernoSoft::Contabilidade::consolida() {
...
}
...
#include <contabilidade_avançada.H>
#include <logistica_básica.H>
using namespace VerãoSoft;
using namespace InvernoSoft;
int main()
{
Logística::consolida();
Contabilidade::consolida();
}