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.
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.
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.
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).
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));
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.
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.
#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
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.
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
path_compiler\g++ -c nomefile.cpp
Dove:
C:\Programmi\Dev-Cpp\bin\g++ -o myprog.exe main.cpp
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.
Supponiamo che:
# 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
Usiamo il seguente makefile:
all : main.cpp defs.h
g++ -o myprog.exe main.cpp
g++ -o myprog.exe main.cpp
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
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.
prova.exe: prova.o funzprova.o
prova.o: prova.cpp prova.h
funzprova.o: funzprova.cpp prova.h
clean:
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:
Dopodichè, il file prova.exe viene (ri)collegato se:
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
Vantaggi:
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
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.
1. Strutture e typedef. Record in C/C++: Concetti Base
4. Puntatori a tipi di dato strutturati. Allocazione Dinamica
5. Puntatori: aspetti avanzati
7. Asserzioni
8. Gestione delle eccezioni. Concetti base
9. Programmazione modulare: concetti base
10. Programmazione Modulare: Meccanismi e Strumenti a supporto in C/C++
12. Esercitazione: Strutture Dati Pila e Coda
13. Esercitazione. Strutture Dati: Lista Concatenata
14. Meccanismi di Incapsulamento in C++ Namespaces
15. Programmazione orientata agli oggetti. Introduzione