Programmazione multifile

Durante la realizzazione di un software costituito da un main() e numerose funzioni, può risultare utile dividere “fisicamente” il codice del programma, in più file sorgenti. In questo modo diventa più facile apportare modifiche e leggere il codice, soprattutto nei programmi di grandi dimensioni. Inoltre, siccome in certi casi il compilatore può compilate le singole parti anche individualmente, in caso di modifiche ad uno dei file sorgenti non è necessario ricompilare tutto il programma, ma solo il file modificato. Comunque tutti i file sorgente appartenenti allo stesso programma dovranno essere contenuti in una sola cartella.

Nel seguente esempio riprenderemo un semplicissimo programma, che sarebbe stato inutile dividere in più file, ma che comunque divideremo per esercizio, per poter capire come procedere anche nei casi più complicati.

main.cpp
/** @file main.cpp 
*
*/
 
#include <iostream>
 
int calcolaTriplo(int x) 
{ 
  std::cout << "promemoria: sto eseguendo calcolaTriplo()..." << std::endl; 
  return 3*x;
}
 
int main()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << calcolaTriplo(mioNumero) << std::endl; // chiamata della funzione
  return 0;
}

La cosa che sembra più semplice da fare è quella di dividerlo in due file sorgenti. Tuttavia, come si vedrà, da questa separazione si rischia di ottenere anche un errore. Vedere il seguente codice sorgente, dove è evidenziata la riga contenente l'errore. Per comprendere l'errore bisogna aver letto il paragrafo sulle dichiarazioni.

calcolatriplo.cpp
/** @file calcolatriplo.cpp 
*
*/ 
 
#include <iostream>
 
int calcolaTriplo(int x) 
{
  std::cout << "promemoria: sto eseguendo calcolaTriplo()..." << std::endl; 
  return 3*x;
}
main.cpp
/** @file main.cpp 
*
*/ 
 
#include <iostream>
                     // <---- ERRORE qui manca una dichiarazione
 
int main()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << calcolaTriplo(mioNumero) << std::endl; // chiamata della funzione
  return 0;
}

NOTA: bisogna usare #include <iostream> SOLO quando serve! Cioè dove si vuole fare Input/Output.

Quando il compilatore legge il contenuto del file main.cpp trova la chiamata alla funzione calcolaTriplo() ma non può verificarne la correttezza del passaggio dei parametri. È necessario far precedere il codice main() con la dichiarazione della funzione che si vuole usare.

int calcolaTriplo(int x);

Dopo questa correzione, i file sorgente non vanno compilati separatamente, bisogna comunicare al compilatore che da questi due file sorgente si deve ottenere un unico file eseguibile. Come già visto (uso del compilatore g++) si effettua prima la compilazione e poi il linking.

  • Il primo comando serve a generare due file oggetto (compilazione)
    • g++ -c calcolatriplo.cpp main.cpp 
  • Il secondo comando serve a collegarli in un unico file eseguibile (linking)
    • g++ calcolatriplo.o main.o -o triplica.exe

La stessa cosa si può ottenere anche usando un unico comando…

g++ main.cpp calcolatriplo.cpp -o triplica.exe

In questo modo il programma viene compilato senza errori, ma il codice può essere ancora migliorato, usando un'organizzazione diversa del codice. Infatti, se per poter usare std::cout è necessaria la direttiva #include, allo stesso modo, una simile direttiva potrebbe essere utilizzata anche per usare calcolaTriplo(). Vedere anche include

Si può creare un nuovo file di intestazione (header file) chiamato calcolatriplo.h che contenga:

  1. gli header file di cui hanno bisogno le funzioni del file calcolatriplo.cpp
  2. le dichiarazioni di tutte le funzioni del file calcolatriplo.cpp,
calcolatriplo.h
/** @file calcolatriplo.h 
*
*/ 
#include <iostream>
 
int calcolaTriplo(int x);

Questo file può sostituire la precedente dichiarazione di calcolaTriplo() per mezzo di un #include. Ecco come si presenterebbe la soluzione finale:

calcolatriplo.cpp
/** @file calcolatriplo.cpp 
*
*/
#include "calcolatripolo.h"  // sostituisce l'inclusione di <iostream>
 
int calcolaTriplo(int x) 
{
  std::cout << "Promemoria: sto eseguendo calcolaTriplo()...." << std::endl;
  return 3*x;
}
main.cpp
/** @file main.cpp 
*
*/ 
 
#include <iostream>
#include "calcolatriplo.h"  // sostituisce la dichiarazione di calcolaTriplo()
 
int main()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << calcolaTriplo(mioNumero) << std::endl;    // chiamata della funzione
  return 0;
}

Quando si compila il programma: prima vengono inclusi gli header file, poi vengono creati i file oggetto ed infine questi sono collegati dal linker.

Si può pensare a iostream come all'interfaccia che permette di usare std::cout e a calcolatriplo.h come all'interfaccia che permette di usare calcolaTriplo().

NOTA: la differenza tra le virgolette di “calcolatriplo.h” e il simbolo maggiore/minore di <iostream> sta nel differente percorso dei file. Le virgolette si usano quando si vuole specificare un percorso relativo agli altri file sorgente. Il maggiore/minore si usa per indicare le cartelle predefinite del sistema operativo dove sono stati installati tutti gli header.

In questa figura si nota che il file calcolatriplo.h è incluso sia da calcolatriplo.cpp che da main.cpp. Si nota anche che <iostream> è incluso da main.cpp sia in modo diretto, che in modo indiretto (tramite calcolatriplo.h). Includere due volte lo stesso header file diminuisce l'efficienza della compilazione, ma la discussione di questo argomento viene rimandata… In quest'ultima versione si può ripetere la solita compilazione con successivo linking.

  • Il primo comando serve a generare due file oggetto (compilazione)
    • g++ -c calcolatriplo.cpp main.cpp
  • Il secondo comando serve a collegarli in un unico file eseguibile (linking)
    • g++ calcolatriplo.o main.o -o triplica.exe

Aver suddiviso il programma in tre file distinti non è stato facile, ma ora se ne può trarre un vantaggio: ora è possibile modificare e ricompilare SOLO il file calcolaTriplo.cpp e poi ripetere il linking, mantenendo inalterato il file oggetto main.o.

g++ calcolatriplo.cpp main.o -o triplica-bis.exe

In questo modo si evita di ricompilare tutto il programma per piccole modifiche ed inoltre è possibile distribuire le diverse parti del programma in modo diverso, ad esempio, pubblicando il codice sorgente del main e mantenendo “segreto” il codice della funzione.

Le direttive

Le direttive al pre-compilatore sono eseguite prima della compilazione

sostituisce una PAROLA (per convenzione maiuscola) con qualsiasi altra cosa

 #define ANNO 2012 
  • #include sostituisce una riga con un intero file di testo
  • #include con le “virgolette” cerca il file nella cartella locale
  • #include con <maggiore/minore> cerca il file nella cartella predefinita di sistema
  • #include viene usato all'interno dei programmi per includere le dichiarazioni presenti nei file di intestazione (che hanno estensione .h)
  • Di solito l'estensione è .h. Fanno eccezione gli header della libreria standard del linguaggio C++, che sono privi del .h (come <iostream>, <string>, ecc.)
  • Anche le dichiarazioni delle funzioni del linguaggio C sono state incluse nella libreria standard del C++ ma oltre ai vecchi <stdio.h> , <math.h>, ecc., in C++ si può usare anche: <cstdio>, <cmath>, ecc. La differenza è che nei nuovi header (che iniziano con lettera “c” e che sono privi del “.h” finale) tutte le vecchie funzioni del C si trovano nel namespace std).
  • Viceversa, per usare le funzioni del linguaggio C++ nei programmi in C, si possono usare
    #include <iostream.h>
  • Attenzione: il linguaggio C++ “contiene” il linguaggio C, cioè il compilatore C++ compila senza errori anche programmi in linguaggio C. I due linguaggi hanno però due stili diversi (vedi confronto tra linguaggio c e c++…)

Evitano di includere più volte la stessa cosa…

#ifndef MYHEADER_H_ 
#define MYHEADER_H_ 
    // declarations of the header file is inserted here, 
     
#endif

La documentazione del software

Documentare bene gli oggetti e le funzioni create permette al programmatore di riutilizzarle più facilmente in futuro, quindi di fare un investimento per poter risparmiare tempo. Se la documentazione è in lingua inglese, chiunque potrà riutilizzare il software.

La documentazione del software può essere inserita nel software stesso, aggiungendovi degli opportuni commenti all'inizio di ogni file. Se il programma è costituito da più file sorgente, dovrebbe essere documentato anche ogni file, come in questo esempio.

/**
 * @file main.cpp
 * @author  Fabio <blabla@example.com>
 * @version 1.3
 *
 * @section LICENSE
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 * 
 */

A loro volta ogni def. di classe e ogni def. di funzione devono essere documentate come in questi esempi:

/**
 * @file time.cpp
 *
 * @section DESCRIPTION
 *
 * The time class represents a moment of time.
 */
 
class Time {
 
    public:
 
       /**
        * Constructor that sets the time to a given value.
        * 
        * @param timemillis Number of milliseconds
        *        passed since Jan 1, 1970.
        */
       Time (int timemillis);
       
       /**
        * show the time
        *
        */
       static Time now ();
};
/**
 * <una descrizione breve, di una riga>
 *
 * <una descrizione piu' completa>
 * <che puo' avere anche piu' righe>
 *
 * @param  descrizione dei parametri della funzione membro
 * @param  ...
 * @return descrizione dell'eventuale valore restituito
 */
void NomeClasse::nomeFunzione()
{
      // il codice della funzione...
}

namespace

In C++, come in C, si può suddividere il codice sorgente in più file, ma in più si possono usare i namespace per organizzare ancora meglio la visibilità degli elementi al loro interno. Di solito, quando più header contengono dichiarazioni di elementi affini o su uno stesso argomento, si possono raggruppare all'interno di uno stesso namespace. Ad esempio, tutti gli elementi di una libreria possono appartenere ad un certo namespace. Il namespace a cui appartiene un header file viene specificato dentro lo stesso header file.

  • i namespace sono dei contenitori che diminuiscono la visibilità del loro contenuto
  • anche le classi sono un altro modo di definire un namespace
  • i namespace, rispetto alle classi, non contengono funzioni né dati membri
  • ????i namespace, rispetto alle classi, possono essere estesi anche dopo la loro definizione

Approfondimento: namespace

In inglese, il campo di visibilità di un elemento (o ambito di visibilità) è chiamato scope. All'interno di un programma esistono alcuni elementi che possono essere usati solo in alcune parti del codice. Ad esempio, le variabili locali di una funzione sono visibili solo dentro tale funzione. Come conseguenza di questo, tali variabili non sono nemmeno accessibili, cioè non si possono usare. Il concetto di accessibilità è più sottile di quello di visibilità e verrà approfondito successivamente nel paragrafo data_hiding della programmazione orientata agli oggetti.

Ogni elemento (variabile, oggetto o funzione) dichiarato in un certo campo di visibilità è visibile solo agli elementi che si trovano dentro tale campo di visibilità, mentre non è visibile dal suo esterno. In altri termini, gli elementi più interni sono i meno visibili. Questo sistema offre una certa protezione dei dati da modifiche indesiderate.

Ad esempio, una variabile di una funzione (variabile locale) è visibile (esiste) solo durante l'esecuzione della funzione. Oppure, un oggetto di un certo tipo classe può vedere solo le funzioni che si trovano all'interno del suo tipo di classe.

Gli elementi globali, essendo i più esterni, sono visibili in ogni momento, in qualsiasi punto del codice. Da un lato ciò è utile perché è comodo poter accedere ai dati globali in ogni momento, ma dall'altro lato ciò è rischioso perché ciò può ridurre la “sicurezza” dei dati.

Come si vede in questa figura, per creare un nuovo campo di visibilità si possono usare funzioni, classi oppure namespace. Il campo di visibilità interno alle funzioni è detto anche locale, in contrapposizione a quello globale (esterno a tutte le funzioni e a tutti i namespace).

In un programma si possono usare due namespace diversi per poter avere due funzioni con lo stesso nome e poterle usare in momenti e contesti diversi.

NOTA: Ogni classe crea automaticamente anche un nuovo namespace

30.h
namespace MioNameSpace 
{
  // tutto ciò che voglio...
}

La situazione più comune è quella in cui viene definito un namespace per tutti gli header file che riguardano lo stesso argomento (esempio, di una stessa libreria di funzioni). A titolo di esempio si può citare la libreria standard del C++, che adotta il namespace “std”, o la libreria wxwidget, che adotta il namespace “wx”.

Ad esempio, entrambe queste librerie potrebbero avere un proprio tipo di dato string, senza che vi fosse possibilità di confusione. Ogni tipo si distinguerebbe dall'altro grazie al namespace: std::string e wx::string. Il namespace diventa una specie di estensione del nome di un elemento. Un po' come si usa il cognome per distinguere due persone che hanno lo stesso nome: Rossi Mario e Dotti Mario.

Se in un programma non si usa nessun namespace, significa che si sta lavorando nel namespace globale…

Come visto, un namespace viene dichiarato dal programmatore per racchiudere le funzioni appartenenti ad un file.h. Viceversa, il programmatore che userà tali funzioni, dovrà specificare quel namespace solo all'interno dei file.cpp, in uno dei seguenti modi:

  • con la direttiva using in un file.cpp, per sottintendere l'uso di un certo namespace per tutte gli elementi utilizzati. Può sembrare una direttiva comoda, ma va usata con moderazione e solo nei file.cpp
    using namespace std;
  • con la dichiarazione using, per sottintende l'uso di un certo namespace per un solo elemento
     using std::cout; using std::sqrt()
std::string parola;

Il simbolo del doppio “due punti” (::) appena visto è un operatore che si deve usare per introdurre l'uso di un qualificatore: il namespace. Nei file.cpp, questo operatore permette di usare un elemento che si trova in un diverso namespace da quello attualmente in uso. Nei file.cpp, in generale, lo scope resolutor :: si può usare liberamente, mentre la direttiva using è sconsigliata. Questa direttiva si potrebbe usare quando fosse noioso ripetere sempre lo scope resolutor, decine di volte.

Ad esempio, se si usasse la direttiva

using namespace std; 

questo sarebbe equivalente a sovrapporre il namespace std al campo di visibilità globale, cioè a sovrapporre, nella figura, il disco arancione a quello bianco.

Lo scope resolutor si può usare, sempre nei file.cpp, anche per definire gli elementi (come le funzioni) che erano stati precedentemente solo dichiarati dentro un certo namespace, in un file.h. Questo sistema permette, durante la stesura del codice, di separare la parte che riguarda l'interfaccia di una funzione dalla sua implementazione, cioè le dichiarazioni, che vanno nel file.h, dalle definizioni, che vanno nei file.cpp.

La stessa separazione tra dichiarazioni e definizioni si può applicare anche per le funzioni membro di una classe, poiché anche le classi definiscono automaticamente un loro namespace.

38.h
//l'interfaccia 
namespace MioNameSpace
{
   void fun(std::string x);
}
38.cpp
//l'implementazione
void MioNameSpace::fun(std::string x)
{
  // codice...
}

In quest'ultimo esempio si può notare che la funzione (fun) e il tipo del suo parametro (x) possono non appartenere allo stesso namespace…

omissis…

In un file.h è possibile dichiarare un namespace senza specificare nessun nome

 namespace 
{
// codice...
}

In questo caso viene creato un namespace “anonimo” esclusivo per questo file.h. Diversamente dagli altri namespace, un namespace anonimo non può essere “condiviso” da più header, quindi non si possono aggiungere funzioni e classi a questo namespace oltre a quelle all'interno del file.h.

In questo caso è possibile separare l'interfaccia dall'implementazione?

39.h
//l'interfaccia ANONIMA
namespace
{
   void fun(int x);
}
38.cpp
//ERRORE: l'implementazione ANONIMA non è possibile dall'esterno
void ::fun(int x)
{
  // codice...
}

Poiché non è possibile, il namespace anonimo può essere usato da un programmatore che scrive l'interfaccia di una libreria e che vuole impedirne l'implementazione dall'esterno.

Oltre al namespace anonimo di un singolo file, esiste anche un namespace genitore anonimo. Dichiarare oggetti all'interno di questo namespace corrisponde ad usare il livello di visibilità “globale”. Gli elementi globali sembrano comodi da usare perché sono sempre visibili e durano per tutta la durata del programma, ma sono di solito uno spreco e una cattiva abitudine…

 
...to do

Un altro modo per definire elementi a “lunga durata” dentro una classe, è quello che utilizza static

(cenni a make cmake http://www.kitware.com/products/protraining3.html)

Esempi

 1.
    esempio di due namespace che contengono funzioni omonime
 2.
    esempio namespace condiviso tra più header / header contenenti più namespace
 3.
    esempio namespace anonimo e anonimo genitore
  • appunti3s/programmazione_multifile.txt
  • Last modified: 2019/07/27 10:47
  • by profpro