Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Valeria Vittorini » 10.Programmazione Modulare: Meccanismi e Strumenti a supporto in C/C++


Modularizzazione in C/C++

La lezione precedente ha introdotto il tema della modularizzazione nello sviluppo di applicazioni software di medie e grandi dimensioni.

E’ stato inoltre accennato a come un uso disciplinato di alcuni meccanismi del linguaggio C/C++ consente una corretta strutturazione di un programma in moduli.

In particolare si è visto come l’inclusione testuale consenta di separare l’interfaccia di un modulo dalla sua implementazione.

Nella presente lezione esaminiamo con maggior dettaglio il ruolo giocato del pre-processore nello sviluppo di applicazioni modulari.

Preprocessore C

Il preprocessore é un programma che viene attivato dal compilatore nella fase precedente alla compilazione, detta di precompilazione.

Il preprocessore legge un sorgente C e produce in output un altro sorgente C, dopo avere sostituito i commenti con spazi bianchi, unito le linee che terminano con ‘\’, espanso in linea le macro, incluso i file e valutato le compilazioni condizionali o eseguito altre direttive.

Una direttiva inizia sempre con il carattere ‘#’ eventualmente preceduto e/o seguito da spazi.

I token seguenti ‘#’ definiscono la direttiva ed il suo comportamento.

Una direttiva al preprocessore puo’ comparire in qualsiasi punto del sorgente in compilazione ed il suo effetto permane fino alla fine del file.

Macro

Definire una macro significa associare un frammento di codice ad un identificatore.

Ogni volta che il preprocessore C incontra l’identificatore così definito, esegue la sua sostituzione in linea con il

frammento di codice ad esso associato.

La definizione delle macro avviene per mezzo della direttiva #define

#define MAX 100

#define STRING_ERR "Rilevato errore !\n"
Nel primo caso a MAX verrà sostituito 100 ovunque MAX compaia nel codice. Analogamente nel secondo caso STRING_ERR verrà sostituito con “Rilevato errore !\n”.
In questo modo è possibile –ad esempio- modificare se e quando necessario il valore 100 o il messaggio di errore in maniera efficace e senza possibilità di errore: basta modificare queste informazioni nella direttiva #define e ricompilare.

Macro

Si tenga ben presente che il preprocessore non ha nessuna informazione circa il possibile significato del risultato della sostituzione testuale che effettua. Si limita a sostituire.
Il compilatore a sua volta si troverà ad operare sul codice sorgente dopo la sostiruzione, per il compilatore MAX e STRING_ERR non esistono.

Il preprocessore elabora l’input in maniera sequenziale, questo significa che nell’esempio in figura ad X verrà sostituito 4 solo dal punto in cui è introdotta la direttiva #define in poi (figura a lato).


Macro

Le macro possono essere definite anche in forma paramentrica; in tal caso la sostituzione dei parametri formali con quelli attuali avviene in modo testuale durante la fase di espansione della macro.

Nell’esempio seguente a min(X,Y) viene  associata la forma contratta di if-then-else che determina il minimo tra due valori (X e Y): se X<Y allora il minimo è X altrimenti è Y.

#define min(X,Y) ((X) < (Y) ? (X) : (Y))

Quando nel codice successivo viene incontrata l’istruzione x=min(a,b) il preprocessore sostituisce nel frammento di codice ((X) < (Y) ? (X) : (Y))  i parametri X e Y con a e b rispettivamente, come se di trattasse dei parametri effettivi in una chiamata a funzione, ma non vi è alcuna funzione da chiamare! Il pre-processore espande la macro sostituendo a min(a,b) il “testo” ((a) < (b) ? (a) : (b)).

Il compilatore troverà quindi l’istruzione: x = ((a) < (b) ? (a) : (b));

Inclusione di file

Il preprocessore C, tramite la direttiva #include, puo’ ricercare il file indicato in alcune directory standard o definite al momento della compilazione ed espanderlo testualmente in sostituzione della direttiva.

La direttiva #include puo’ essere impiegata in due forme:

#include <nomefile>

#include “nomefile”

Nel 1° caso il nomefile viene ricercato in un insieme di directory standard definite dall’implementazione ed in altre che sono specificate al momento della compilazione.

Nel 2° caso il nomefile viene ricercato nella directory corrente e poi, se non è stato trovato, la ricerca continua nelle directory standard e in quelle specificate al momento della compilazione come nel 1° caso.

N.B. Nel caso che un header venga modificato, è necessario ricompilare tutti i sorgenti che lo includono.

Compilazione Condizionale

Il preprocessore C può testare espressioni aritmetiche o controllare se un nome è stato definito come macro e decidere di includere o escludere parti del codice sorgente dalla compilazione.

Le direttive:

#if #ifdef #ifndef #elif #else #endif

consentono di associare la compilazione di alcune parti di codice alla valutazione di alcune condizioni.

Compilazione Condizionale: Esempio 1

#if <espressione_costante>

<statement_1>

#else

<statement_1>

#endif

Se l’espressione costante specificata, valutata in compilazione, ritorna TRUE, allora verranno compilati gli statement_1, altrimenti verranno compilati gli statement_2.

Esempi di utilizzo

  • Il programma può richiedere codice diverso su sistemi operativi diversi.
  • Vogliamo compilare una versione di “debug” del programma (vedi la macro ASSERT).

Esempio 1

  • Nell’esempio seguente la compilazione condizionale è utilizzata per decidere quale implementazione di una funzione deve essere compilata e utilizzata in funzione del sistema operativo su cui il programma verrà eseguito.
  • Nel caso specifico si vuole fornire due diverse implementazione della funzione “pausa” (per arrestare l’esecuzione del programma ed attendere un input da tastiera) a seconda che il sistema operativo sia un S.O. windows o meno.
  • Questo si rende necessario perchè la chiamata alla famosa funzione “system” con parametro “PAUSE” è lecita solo su S.O. windows e rende il codice non portabile in altri ambienti.
Mostra codice

Compilazione Condizionale: Esempio 2

Queste direttive possono essere utilizzate per impedire che uno stesso header file venga incluso più di una volta nello stesso file sorgente.

Ciò può causare errori in compilazione (definizione multipla) e aumentare il tempo di compilazione.


Compilazione Condizionale: Esempio 2

In questo modo il primo file che presenta la direttiva #include “header.h” valuta l’espressione #ifndef, trova che _HEADER_H non è stato definito, lo definisce e include il codice compreso tra #ifndef e #endif.

I files successivi all’atto dell’inclusione valutano l’espressione, trovano che _HEADER_H è stato già definito, e saltano alla riga successiva (#endif) senza includere il file header.h

#ifndef _NOMEHEADER_H // sta per #if !define _NOMEHEADER_H

#define _NOMEHEADER_H

Corpo dell'header

#endif

Invocare il compilatore a linea di comando

  • Il compilatore può essere invocato fornendo il relativo comando senza utilizzare alcuna interfaccia grafica, ma semplicemente digitando il comando relativo nella finestra di shell.
  • Per invocare il compilatore g++ (per il linguaggio cpp) e compilare un programma sorgente il comando da digitare è:

path_compiler\g++ -c nomefile.cpp

Invocare il compilatore a linea di comando

Dove:

  • path_compiler è il path della cartella (directory) sotto cui si trova il programma g++
  • -c è l’opzione fornita al compilatore per indicare che si vuole compilare.
  • nomefile.cpp è il nome del file sorgente da compilare.
  • Il compilatore genererà un file oggetto di nome nomefile.o se non diversamente specificato.
  • Ad esempio il comando: C:\Programmi\Dev-Cpp\bin\g++ -c prova.cpp
  • (dato trovandosi nella cartella in cui è memorizzato il file prova.cpp) produce l’oggetto prova.o. In questo esempio il programma g++ si trova evidentemente nella cartella bin in C:\Programmi\Dev-Cpp\
  • Per effettuare il linkaggio (collegamento) al fine di produrre un eseguibile bisogna specificare l’opzione –o. Ad esempio il seguente comando nel caso in cui il programma sia costituito da un unico file sorgente main.coo effettua la compilazione ed il collegamento producendo l’eseguibile dal nome myprog.exe come specificato nel comand stesso:

C:\Programmi\Dev-Cpp\bin\g++ -o myprog.exe main.cpp

Invocare il compilatore a linea di comando

  • Utilizzando un ambiente di sviluppo la chiamata al compilatore viene effettuata mediante l’interfaccia, le opzioni fornite al compilatore vengono determinate dall’azione che l’utente programmatore specifica (ad esempio: “compila”, o “compila il file corrente” o “costruisci tutto”, etc.).
  • Operando in compilazione separata su programmi costituiti da diversi moduli, le informazioni fornite dal programmatore circa i file e le dipendenze tra di esse vengono utilizzate dall’ambiente per lanciare nella corretta sequenza i comandi relativi alla compilazione ed al collegamento dei moduli.
  • Queste operazioni possono essere effettuate direttamente dal programmatore (senza utilizzare alcuna interfaccia ma direttamente da shell) e possono essere automatizzate utilizzando l’utility MAKE.

L’utility make

  • Il codice di un programma di medie dimensioni è tipicamente costituito da più moduli (file sorgenti e file header) ed ogni sorgente modificato deve essere ricompilato, inoltre se un file header viene modificato, ogni sorgente che lo include deve essere ricompilato. Si deve in questi casi ripetere la fase di collegamento (linkaggio) per collegare i diversi file oggetto e produrre il nuovo file eseguibile.
  • E’ molto oneroso eseguire queste attività “a mano” inoltre bisogna essere sicuri di aver ricompilato e rigenerato tutti i file necessari.
  • L’utility make automaticamente determina quali parti del programma devono essere ricompilate ed esegue i comandi per ricompilarle.
  • Nel seguito di questa lezione viene fornita qualche nozione introduttiva all’utilizzo di make, si rimanda al manuale citato al termine della lezione per uno studio più approfondito.

L’utility make

  • make può essere usato con ogni linguaggio di programmazione il cui compilatore può essere invocato con un comando di shell (a linea di comando).
  • make può anche essere usato ogni volta che alcuni file devono essere aggiornati automaticamente a partire da altri quando questi ultimi cambiano.

Il Makefile

  • make ha bisogno di un file (chiamato makefile o Makefile, senza alcuna estensione) per sapere cosa fare.
    Un makefile è costituito da una serie di regole (in figura).
  • Un target è il nome di un file che deve essere generato (es. file oggetto o eseguibile) oppure un nome che identifica un’azione da compiere (in questo caso prende il nome di phony target).
  • Un prerequisito è un file usato come input per creare il target.
  • Un comando è un’azione che make esegue.

Il Makefile

  • Tipicamente, un comando serve a creare il file target se uno dei prerequisiti cambia.
  • Una regola può avere più comandi, uno per riga oppure sulla stessa riga e separati da un ‘;’
  • NOTA: occorre inserire un carattere ‘Tab’ all’inizio di ogni riga che contiene comandi
  • I comandi possono anche essere inseriti sulla riga dei prerequisiti, purchè ci sia un ‘;’ tra i prerequisiti e i comandi
  • Una linea lunga può essere spezzata in più linee utilizzando un backslash ‘\’ alla fine di ogni riga.
  • Una regola può anche non avere prerequisiti.
  • Il carattere ‘#’ in una linea inizia un commento.

Un semplice esempio

  • Quando make incontra un target, controlla se esiste un file avente lo stesso nome.
  • In caso negativo si tratta di un phony target, quindi i comandi associati vanno in ogni caso eseguiti.
  • Nota che i phony target possono avere prerequisiti.
  • Esempio: abbiamo un file sorgente main.cpp, un header file defs.h e vogliamo creare l’eseguibile myprog.

Un semplice esempio (ok)

Usiamo il seguente makefile:

#include<iostream.h>

myprog.exe : main,cpp defs.h
g++ -o myprog.exe main.cpp

La prima volta che eseguiamo make, non esiste un file di nome myprog.exe e quindi myprog.exe è un phony target
Il comando ‘g++ -o myprog.exe main.cpp’ viene eseguito e produce il file myprog.exe
Se eseguiamo di nuovo make, viene trovato il file myprog.exe. I prerequisiti main.cpp e defs.h non sono cambiati quindi non occorre aggiornare il file myprog.exe. Risposta:

make: `myprog.exe' is up to date.

Questo è il comportamento corretto.

Invocare “make”

  • Basta digitare make a linea di comando dalla directory nella quale si trovano il Makefile e i file da compilare e collegare.
  • Il sistema deve essere in grado di trovare il programma make: in questi semplici esempi semplicemente viene specificato l’intero path ogni qualvolta make viene invocato (come abbiamo fatto per invocare g++). In alternativa è sicuramente più efficiente settare opportunamente i path.

Eseguire make

Supponiamo che:

  • il file Makefile e i file main.cpp e defs.h si trovano della cartella:
  •  C:\Documents and Settings\dis\Desktop\P1_9CFU_2011_12\week3\provaMAKE\primo
  • Il programma MAKE si trova nella cartella: C:\Programmi\Dev-Cpp\bin
  • Il programma g++ si trova nella cartella: C:\Programmi\Dev-Cpp\bin
  • Vogliamo eseguire make sul seguente primo makefile di esempio (le righe precedute da # sono linee di commento. Lanciamo make da shell (in questo caso un prompt dei comandi) come mostrato nella figura accanto, ottenendo il messaggio “up to date” come spiegato precedentemente.

# Project: prova
# Makefile semplice di esempio
# make path C:\Programmi\Dev-Cpp\bin

myprog.exe: main.cpp defs.h
C:\Programmi\Dev-Cpp\bin\g++ -o myprog.exe main.cpp

Eseguire make


Un semplice esempio (ko)

Usiamo il seguente makefile:

all : main.cpp defs.h
g++ -o myprog.exe main.cpp

  • La prima volta che eseguiamo make, non esiste un file di nome all e quindi all è un phony target
  • Il comando ‘g++ -o myprog.exe main.cpp’ viene eseguito e produce il file myprog.exe
  • Se eseguiamo di nuovo make, il file all comunque non viene trovato. Dunque all è ancora un phony target e il comando viene di nuovo eseguito. Risposta:
  • g++ -o myprog.exe main.cpp
  • Questo comportamento non è l’obiettivo di un Makefile, perchè compilazione e collegamento sono stati rieseguiti senza necessità di farlo.

Un semplice esempio

Quando un sorgente include un header file è importante specificare l’header file tra i prerequisiti. Perchè?
Consideriamo il seguente sorgente main.cpp:

#include
#include "defs.h"
using namespace std;

int main() {
cout << VALUE;
}

e l’header file defs.h contenente solo la seguente linea di codice:

#define VALUE 10

Un semplice esempio

Se usiamo il seguente makefile omettendo defs.h nei prerequisiti:

myprog.exe : main.cpp
g++ -o myprog.exe main.cpp

Eseguiamo make e poi myprog. Il valore mostrato è 10.

Modifichiamo defs.h: #define VALUE 20

Eseguiamo make: make: `myprog.exe’ is up to date.
E poi eseguiamo nuovamente myprog. Il valore mostrato è ancora 10.
Se invece defs.h è un prerequisito di myprog.exe, il programma viene ricompilato e il valore mostrato è quello corretto.

Un esempio di makefile

prova.exe: prova.o funzprova.o

  • C:\Programmi\Dev-Cpp\bin\g++ -o prova.exe prova.o funzprova.o

prova.o: prova.cpp prova.h

  • C:\Programmi\Dev-Cpp\bin\g++ -c prova.cpp

funzprova.o: funzprova.cpp prova.h

  • C:\Programmi\Dev-Cpp\bin\g++ -c funzprova.cpp

clean:

  • rm prova.exe prova.o funzprova.o

Un esempio di makefile

  • Per creare l’eseguibile chiamato prova basta digitare make.
  • Per eliminare il file eseguibile e tutti i file oggetto, digitare make clean.
  • In entrambi i casi, make fa riferimento al file ‘makefile’ (file utilizzato per default).
  • Il target ‘clean’ non è un file ma specifica un’azione. Siccome non compare tra i prerequisiti di nessuna altra regola, make lo ignora, a meno che non glielo diciamo specificamente (con ‘make clean’).

Come make elabora un makefile

make inizia dal primo target (che non comincia con ‘.’), che prende il nome di default goal (nel nostro esempio è l’eseguibile prova.exe).
I prerequisiti di prova.exe sono 2 file oggetto, per cui make deve elaborare le regole ad essi corrispondenti.
Ogni file oggetto viene (ri)compilato se:

  • Il file oggetto non esiste
  • Il file sorgente o uno dei file header da cui dipende sono più recenti del file oggetto

Dopodichè, il file prova.exe viene (ri)collegato se:

  • Il file prova.exe non esiste
  • Esiste un file oggetto più recente di prova.exe

Come eseguire make

Gli argomenti passati al comando make sono i target che make si occupa di aggiornare
es. ‘make main.o’ aggiorna solo il file main.o
se non specificato, make aggiorna il primo target
Opzioni:
-f file utilizza il file file come makefile
-C dir entra nella directory dir prima di leggere il makefile

Utilizzare “variabili”

  • Un makefile può essere reso più generale e quindi anche più facilmente “riutilizzabile” introducendo un certo livello di parametrizzazione.
  • Nell’esempio che segue sono stati utilizzati i seguenti parametri (“variabili”): CPP, OBJ, EXE, BIN, RM.
  • A tali variabili sono stati “assegnati” dei “valori” ed essi possono essere utilizzati in luogo di quei valori ovunque sia necessario nel makefile.
  • In questo modo si rende più generale il makefile: ad esempio per riutilizzarlo nel caso si voglia cambiare nome al default goal (cioè al file eseguibile), o nel caso cambi il compilatore C++ o il path sotto il quale si trova il compilatore.
Mostra codice

Variabili

Vantaggi:

  • Minore probabilità di errori
  • Scrittura semplificata del makefile

Introduzione alle regole implicite

main.o : main.cpp defs.h
g++ -c main.cpp

Non è necessario specificare il comando in questi casi. Se non lo si specifica, make utilizza il comando:

g++ -c main.cpp -o main.o

Se si sfrutta questa regola implicita è anche superfluo specificare main.c tra i prerequisiti
Per cui è sufficiente:

main.o : defs.h

Regole implicite

  • Alcuni modi standard per creare certi tipi di file sono usati molto spesso (es. creare un .o a partire da un .cpp).
    make utilizza regole implicite per effettuare queste operazioni, evitando all’utente di esplicitarle nel makefile.
  • Il nome del file determina quale regola implicita usare.
  • Le regole implicite utilizzano diverse variabili predefinite, in modo che è possibile cambiare il modo in cui operano.
  • Per consentire a make di cercare una regola implicita per aggiornare un file target, occorre non specificare alcun comando:
    • Scrivere una regola senza comandi.
    • Non scrivere affatto la regola.

Regole implicite

Una regola implicita può anche fornire sia i comandi che i prerequisiti per aggiornare il file target
(es. la regola implicita per aggiornare main.o includerà tra i prerequisiti il sorgente main.cpp).
Tipicamente, si scrive una regola senza comandi quando occorre specificare prerequisiti che la regola implicita non può fornire.
In generale, make cerca una regola implicita per ogni target che non ha comandi e per ogni prerequisito per cui non è specificata una regola.

I materiali di supporto della lezione

Gnu

Da C++ a UMLCapitolo 12, par.12.13

GNU Make Manual

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion