• Non ci sono risultati.

Costruire & Usare gli Oggetti

Nel documento Pensare in C++, seconda ed. Volume 1 (pagine 49-72)

Fase 5: Evoluzione

2: Costruire & Usare gli Oggetti

Questo capitolo introdurrà la sintassi e i concetti necessari per la

costruzione di programmi in C++ permettendo di scrivere ed eseguire qualche piccolo programma orientato agli oggetti. Nel capitolo

seguente verranno illustrate in dettaglio le sintassi base del C e del C++.

Leggendo questo capitolo, si avrà un'infarinatura di cosa significa programmare ad oggetti in C++ e si scopriranno anche alcune delle ragioni dell'entusiasmo che circondano questo linguaggio. Ciò dovrebbe essere sufficiente per passare al Capitolo 3, che può essere più esaustivo poichè contiene la maggior parte dei dettagli del linguaggio C.

Il tipo di dato definito dall'utente o classe, è ciò che distingue il C++ dai linguaggi procedurali. Una classe è un nuovo tipo di dato che viene creato per risolvere un particolare tipo di problema. Una volta creato, chiunque può usarla senza sapere nello specifico come funziona o persino come sono fatte le classi. Questo capitolo tratta le classi come se esse fossero un altro tipo di dato predefinito disponibile all'utilizzo nei

programmi.

Le classi che qualcun altro ha creato sono tipicamente impacchettate in una libreria. Questo capitolo usa diverse librerie di classi che sono disponibili in tutte le

implementazioni del C++. Una libreria standard particolarmente importante è la

iostreams, che ( tra le altre cose) ci permette di leggere dai file e dalla tastiera e di scrivere su file e su schermo. Si vedrà anche la pratica classe string e il contenitore vector dalla libreria Standard C++. Alla fine del capitolo, si vedrà come sia facile usare le classi delle librerie predefinite.

Per creare il nostro primo programma si devono capire gli strumenti utilizzati per costruire le applicazioni.

Il processo di traduzione del linguaggio

Tutti i linguaggi del computer sono tradotti da qualcosa che tende ad essere facile da capirsi per un umano (codice sorgente) verso qualcosa che è eseguito su un

computer(istruzioni macchina). Tradizionalmente i traduttori si divisono in due classi:

interpreti e compilatori. Interpreti

Un interprete traduce il codice sorgente in unità (che possono comprendere gruppi di istruzioni macchina) ed esegue immediatamente queste unità. Il BASIC, per esempio, è stato un linguaggio interpretato popolare. Gli interpreti BASIC tradizionali traducono ed eseguono una linea alla volta e poi dimenticano che quella linea è stata tradotta. Ciò li rende lenti, poichè essi devono ritradurre ogni codice ripetuto. Il BASIC viene anche compilato per essere più veloce. Gli interpreti più moderni, come quelli per il linguaggio Python, traducono l'intero programma in un linguaggio intermedio che viene poi eseguito

Gli interpreti hanno molti vantaggi. La transizione dal codice scritto a quello eseguito è quasi immediato ed il codice sorgente è sempre disponibile perciò l'interprete può essere più dettagliato quando si verifica un errore. Il beneficio spesso citato dagli interpreti è la faciltà di interazione e lo sviluppo rapido ( ma non necessariamente l'esecuzione) dei programmi.

I linguaggi interpretati hanno spesso serie limitazioni quando si costruiscono grossi progetti ( Python sembra essere un eccezione). L'interprete ( o la versione ridotta) deve sempre essere in memoria per eseguire il codice e persino l'interprete più veloce può introdurre inaccettabili restrizioni alla velocità. La maggior parte degli interpreti richiede che il sorgente completo sia portato nell'interprete tutto insieme. Ciò introduce non solo una limitazione di spazio, ma può anche causare bug più difficili da trovare se il linguaggio non fornisce un strumento per localizzare l'effetto dei diversi pezzi di codice.

Compilatori

Un compilatore traduce codice sorgente direttamente in linguaggio assembler o in instruzioni macchina. Il prodotto finale è uno o più file contenenti il codice macchina. Questo è un processo complesso e di solito richiede diversi passi. La transizione dal codice scritto al codice eseguito è significamente più lunga con un compilatore.

In base all'acume di chi ha scritto il compilatore, i programmi generati dai compilatori tendono a richiedere molto meno spazio per essere eseguiti e sono eseguiti molto più rapidamente. Sebbene le dimensione e la velocità sono probabilmente le ragioni più spesso citate per l'uso di un compilatore, in molte situazioni non sono le ragioni più importanti. Alcuni linguaggi ( come il C) sono progettati per permettere la compilazione indipendente di pezzi di programma. Questi pezzi sono alla fine combinati in un programma eseguibile finale da uno strumento detto linker. Questo processo è chiamato compilazione separata. La compilazione separata ha molti benefici. Un programma che, preso tutto insieme eccederebbe il limite del compilatore o dell'ambiente di compilazione, può essere

compilato in pezzi. I Programmi possono essere costruiti e testati un pezzo alla volta. Una volta che un pezzo funziona, può essere salvato e trattato come un blocco da costruzione. Le raccolte di pezzi testati e funzionanti possono essere combinati in librerie per essere usati da altri programmatori. Man mano che ogni pezzo viene creato, la complessità degli altri pezzi viene nascosta. Tutte queste caratteristiche aiutano la creazione di programmi di

grosse dimensioni[26].

Le caratteristiche di debug dei compilatori sono andate migliorando significativamente. I primi compilatori generavano solo codice macchina ed i programmatori inserivano dei comandi di stampa per vedere cosa stava succedento. Ciò non era sempre efficace. I compilatori moderni possono inserire informazioni sul codice nel programma eseguibile. Questa informazione è usata dai potenti debugger a livello di sorgente per mostrare esattamente cosa sta succedento in un programma tracciando il suo avanzamento nel sorgente.

Qualche compilatore affronta il problema della velocità di compilazione usando la

compilazione in memoria. La maggior parte dei compilatori funziona con file, leggendo e

scrivendo in ogni passo del processo di compilazione. I compilatori in memoria mantegono il programma del compilatore nella RAM. La compilazione di programmi piccoli può sembrare veloce come un interprete.

Il processo di compilazione

Per programmare in C ed in C++, si ha bisogno di capire i passi e gli strumenti nel processo di compilazione. Alcuni linguaggi (C e C++ in particolare) cominciano la compilazione eseguendo un preprocessore sul sorgente. il preprocessore è un semplice programma che rimpiazza pattern nel codice sorgente con altri patter che il programmatore ha definito ( usando le direttive del preprocessore ). Le direttive del preprocessore sono utilizzate per risparmiare la scrittura ed aumentare la leggibilità del codice. ( Più avanti nel libro, si imparerà come il design del C++ è inteso per scoraggiare un frequente uso del

preprocessore, perchè può causare bug). Il codice pre-processato viene spesso scritto in un file intermedio.

I compilatori di solito eseguono il loro lavoro in due passi. Il primo passo parsifica il codice preprocessato. Il compilatore spezza il codice sorgente in piccole unità e le organizza in una struttura chiamata albero. Nell'espressione A+B gli elementi A,+ e B sono foglie sull'albero di parsificazione.

Un ottimizzatore globale è usato a volte tra il primo ed il secondo passo per produrre un codice più piccolo e veloce.

Nel secondo passaggio, il generatore di codice utilizza l'albero di parsificazione e genera il linguaggio assembler o il codice macchina per i nodi dell'albero. Se il generatore di codice crea codice assembler, l'assembler deve essere eseguito. Il risultato finale in entrambi i casi è un modulo oggetto (un file che tipicamente ha estensione .o oppure .obj). Un

ottimizzatore peephole è a volte utilizzato nel secondo passaggio per trovare pezzi di codice

che contengono comandi ridondanti del linguaggio assembler.

L'uso della parola "oggetto" per descrivere pezzi di codice macchina è un artificio non fortunato. La parola è stata usata prima che la programmazione orientata agli oggetti fosse di uso generale. "Oggetto" viene usato nello stesso senso di "scopo" quando si discute di compilazione, mentre nella programmazione object-oriented significa "una cosa

delimitata".

Il linker combina una lista di oggetti modulo in un programma eseguibile che può essere caricato ed eseguito dal sistema operativo. Quando una funzione in un modulo oggetto fa riferimento ad una funzione o variabile in un altro modulo oggetto, il linker risolve questi riferimenti; si assicura che tutte le funzioni esterne ed i dati di cui si reclama l'esistenza esistano durante la compilazione. Il linker aggiunge anche uno speciale modulo oggetto per eseguire attività di avviamento.

Il linker può cercare in file speciali chiamati librerie in modo da risolvere tutti i suoi riferimenti. Una libreria contiene un insieme di moduli oggetto in un singolo file. Una libreria viene creata e manutenuta da un programma chiamato librarian.

Controllo del tipo statico

Il compilatore esegue un controllo del tipo durante il primo passaggio. Il controllo del tipo testa l'uso opportuno degli argomenti nelle funzioni e previene molti tipi di errori di programmazione. Poichè il controllo del tipo avviene durante la compilazione invece che quando il programma è in esecuzione, esso è detto controllo del tipo statico .

Qualche linguaggio orientato agli oggetti ( particolarmente Java) esegue il controllo del tipo statico a runtime ( controllo del tipo dinamico ). Se combinato con il controllo del tipo statico, il controllo del tipo dinamico è più potente del controllo statico da solo. Tuttavia, aggiunge un overhead all'esecuzione del programma.

Il C++ usa il controllo del tipo statico perchè il linguaggio non può eseguire nessuna

particolare azione per operazioni cattive. Il controllo statico del tipo notifica all'utente circa gli usi errati dei tipi durante la compilazione e così massimizza la velocità di esecuzione. Man mano che si imparerà il C++, si vedrà che le scelte di design del linguaggio

favoriscono la programmazione orientata alla produzione e l'altà velocità, per cui è famoso il C.

Si può disabilitare il controllo del tipo statico in C++. Si può anche fare il proprio controllo del tipo dinamico, si deve solo scrivere il codice.

Strumenti per la compilazione separata

La compilazione separata è particolarmente importante quando si costruiscono grossi progetti. In C e C++, un programma può essere creato in pezzi piccoli, maneggevoli e testati indipendentemente. Lo strumento fondamentale per dividere un programma in pezzi è la capacità di creare routine o sottoprogrammi. In C e C++, un sottoprogramma è chiamato funzione e le funzioni sono pezzi di codice che possono essere piazzati in file diversi, permettono la compilazione separata. Messo in altro modo, la funzione è un'unità atomica di codice, poichè non si può avere parte di una funzione in un unico file e un'altra parte in un file diverso; l'intera funzione deve essere messa in un singolo file ( sebbene i file possono e devono contenere più di una funzione).

Quando si chiama una funzione, si passano tipicamente degli argomenti, che sono i valori con cui la funzione lavora durante la sua esecuzione. Quando la funzione è terminata, si ottiene un valore di ritorno, un valore che la funzione ci riporta come risultato. È anche possibile scrivere funzioni che non prendono nessun argomento e non restituiscono nessun valore.

Per creare un programma con file multipli, le funzioni in un unico file devono accedere a funzioni e dati in altri file. Quando si compila un file, il compilatore C o C++ deve

conoscere le funzioni e i dati negli altri file, in particolare i loro nomi ed il corretto uso. Il compilatore assicura che le funzioni e i dati siano usati correttamente. Questo processo di dire al compilatore i nomi delle funzioni e dati esterni e come dovrebbero essere è detto

dichiarazione. Una volta che si dichiara una funzione o una variabile, il compilatore sa

come eseguire il controllo per essere sicuro che è stato usato correttamente.

Dichiarazioni e definizioni

È importante capire la differenza tra dichiarazioni e definizioni perchè questi termini saranno usati in maniera precisa nel libro. Essenzialmente tutti i programmi C e C++ richiedono dichiarazioni. Prima che si possa scrivere il primo programma, c'è bisogno di capire il modo corretto di scrivere una dichiarazione.

Una dichiarazione introduce un nome, un indentificativo, nel compilatore. Dice al compilatore "Questa funzione o variabile esiste da qualche parte e qui è come dovrebbe apparire. Una definizione dall'altra lato dice: "Crea questa variabile qui o Crea questa

funzione qui. ". Essa alloca memoria per il nome. Il significato funziona sia se si sta

parlando di una variabile che di una funzione; in entrambi i casi, al punto della definizione il compilatore alloca memoria. Per una variabile, il compilatore determina quanto grande quella variabile sia e causa la generazione di spazio in memoria per mantenere i dati di quella variabile. Per una funzione, il compilatore genera codice, che termina con l'occupare spazio in memoria.

Si può dichiarare una variabile o una funzione in diversi posti, ma ci deve essere una sola definizione in C e C++ ( questo è a volte detto ODR: one-definition rule una regola di defizione). Quando il linker sta unendo tutti i moduli oggetto, di solito protesta se trova più di una definizione della stessa funzione o variabile.

Una definizione può anche essere una dichiarazione. Se il compilatore non ha visto il nome

x prima che si definisca int x; , il compilatore vede il nome come una dichiarazione e

alloca spazio per esso tutto in una volta.

Sintassi di dichiarazione delle funzioni

Una dichiarazione di funzione in C e in C++ richiede il nome della funzione, i tipi degli argomenti passati alla funzione ed il valore di ritorno. Per esempio, qui c'è una

dichiarazione per una funzione chiamata func1() che prende due argomenti interi ( gli interi sono denotati in C/C++con la parola riservata int) e restituisce un intero:

int func1(int,int);

La prima parola riservata che si vede è il valore di ritorno: int. Gli argomenti sono

racchiusi in parentesi dopo il nome della funzione nell'ordine in cui sono usati. Il punto e virgola indica la fine della dichiarazione; in questo caso, dice al compilatore "questo è tutto" non c'è definizione di funzione qui!

Le dichiarazioni C e C++ tentano di imitare la forma di uso dell'argomento. Per esempio, se

a è un altro intero la funzione di sopra potrebbe essere usata in questo modo: a = func1(2,3);

Poichè func1() restituisce un intero, il compilatore C o C++ controllerà l'uso di func1() per essere sicuro che a può accettare il valore di ritorno e che gli argomenti siano

appropriati.

Gli argomenti nelle dichiarazioni della funzione possono avere dei nomi. Il compilatore li ignora ma essi possono essere utili come congegni mnemonici per l'utente. Per esempio, possiamo dichiarare func1() in un modo diverso che ha lo stesso significato:

int func1(int lunghezza, int larghezza); Beccato!

C'è una significativa differenza tra il C ed il C++ per le funzioni con una lista degli argomenti vuota. In C, la dichiarazione:

significa "una funzione con qualsiasi numero e tipo di argomento". Ciò evita il controllo del tipo, perciò nel C++ significa "una funzione con nessun argomento".

Definizione di funzioni.

Le definizioni delle funzioni sembrano dichiarazioni di funzioni tranne che esse hanno un corpo. Un corpo è un insieme di comandi racchiusi tra parentesi. Le parentesi denotano l'inizio e la fine di un blocco di codice. Per dare una definizione a func1() con un corpo vuoto ( un corpo non contente codice), si può scrivere:

int func1(int lunghezza, int larghezza) { }

Si noti che nella definizione di funzione, le parentesi rimpiazzano il punto e virgola. Poichè le parentesi circondano un comando od un gruppo di comandi, non c'è bisogno del punto e virgola. Si noti anche che gli argomenti nella definizione della funzione devono avere i nome se si vuole usare gli argomenti nel corpo della funzione ( poichè essi non sono mai usati qui, essi sono opzionali ).

Sintassi di dichiarazione delle variabili

Il significato attribuito alla frase "dichiarazione di variabile" è stato storicamente confuso e contradditorio ed è importante che si capisca la corretta definizione per saper leggere correttamente il codice. Una dichiarazione di variabile dice al compilatore come una variabile appare. Essa dice: "So che non hai mai visto questo nome prima, ma ti prometto che esiste da qualche parte ed è di tipo X".

In una dichiarazione di funzione, si dà un tipo ( il valore di ritorno), il nome della funzione, la lista degli argomenti ed il punto e virgola. Ciò è sufficiente per far capire al compilatore che è una dichiarazione e che come dovrebbe apparire la funzione. Per deduzione, una dichiarazione di variabile potrebbe essere un tipo seguito da un nome. Per esempio:

int a;

potrebbe dichiarare la variabile a come un intero, usando la logica di sopra. Qui c'è il conflitto: c'è abbastanza informazione nel codice di sopra perchè il compilatore crei spazio per un intero chiamato a e ciò è quello che accade. Per risolvere questo dilemma, una parola riservata è stata necessaria per il C ed il C++ per dire: "Questa è solo una dichiarazione, la sua definizione è altrove". La parola riservata è extern. Essa può

significare che la definizione è esterna al file o che la definizione appare più avanti nel file. Dichiarare una variabile senza definirla significa usare la parola chiave extern prima di una descrizione della variabile:

extern int a;

extern può anche essere applicato alle dichiarazioni di funzioni. Per func1(), ecco come

appare:

extern int func1(int lunghezza, int larghezza);

Questa dichiarazione è equivalente alla precedenti dichiarazioni di func1(). Poichè non c'è il corpo della funzione, il compilatore deve trattarla come un dichiarazione di funzione

piuttosto che una definizione di funzione. La parola riservata extern è perciò superflua ed opzionale per le dichiarazione di funzione. È un peccato che i progettisti del C non abbiano richiesto l'uso di extern per la dichiarazione delle funzioni, sarebbe stato più consistente e avrebbe confuso meno ( ma avrebbe richiesto più caratteri da scrivere, ciò probabilmente spiega la decisione).

Ecco altri esempi di dichiarazione:

//: C02:Declare.cpp

// esempi di dichiarazioni & definizioni

extern int i; // dichiarazione senza definzione extern float f(float); // dichiarazione di funzione float b; // dichiarazione & definizione

float f(float a) { // definizione

return a + 1.0; }

int i; // definizione

int h(int x) { // dichiarazione & definizione

return x + 1; } int main() { b = 1.0; i = 2; f(b); h(i); } ///:~

Nelle dichiarazioni delle funzione, gli identificatori degli argomenti sono opzionali. Nelle definizioni sono richiesti ( gli identificatori sono richiesti solo in C non in C++).

Includere un header

Molte librerie contengono numeri significativi di funzioni e variabili. Per risparmiare lavoro ed assicurare coerenza quando si fanno dichiarazioni esterne per questi pezzi, il C ed il C++ usano un componente chiamato file header. Un file header è un file contentente le dichiarazioni esterne per una libreria; esso ha convenzionalmente una estenzione "h", per esempio headerfile.h (si può anche vedere qualche vecchio codice che utilizza altre estensioni come .hxx o .hpp ma è raro).

Il programmatore che crea la libreria fornisce il file header. Per dichiarare le funzioni e le variabili esterne nella libreria, l'utente semplicemente include il file header. Per includere il file header, si usa la direttiva del preprocessore #include. Questo dice al preprocessore per aprire il file header ed inserire il suo contenuto dove appare il comando #include. Un

#include può menzionare un file in due modi: con < > oppure con doppie virgolette.

I nomi di file tra le < > come:

dicono al preprocessore di cercare un file in un modo che è una nostra implementazione, ma tipicamente ci sarà una specie di percorso di ricerca per l'include, che si specifica nell'ambiente di sviluppo o dando una linea di comando al compilatore. Il meccanismo per impostare il percorso di ricerca può variare tra macchine, sistemi operativi e

implementazioni del C++ e può richiedere qualche indagine da parte propria. I nomi dei file tra doppie virgolette:

#include "local.h"

dicono al preprocessore di cercare un file in un modo definito dall'implementazione ( secondo la specifica). Questo tipicamente vuol dire ricercare il file nella cartella corrente. Se il file non viente trovato, allora la direttiva di include è riprocessata come se avesse le parentesi ad angolo invece delle virgolette.

Per l'include dell'header file della iostream si deve scrivere:

#include <iostream>

Il preprocessore cercherà l'header file della iostream ( spesso in una sottocartella chiamata "include" ) e lo inserirà.

Formato include C++ Standard

Con l'evolvere del C++, i fornitori di compilatori scelsero estensioni diverse per i nomi dei file. In aggiunta, vari sistemi operativi avevano restrizioni diverse sui nomi dei file, in particolare sulla lunghezza del nome. Questi problemi causarono problemi di portabilità del codice. Per smussare questi angoli, lo standard usa un formato che permette nomi di file più lunghi dei noti otto caratteri ed elimina le estensioni. Per esempio, invece di

Nel documento Pensare in C++, seconda ed. Volume 1 (pagine 49-72)

Documenti correlati