[ Suggerimenti
] [ Soluzioni
degli Esercizi] [ Volume
2 ] [ Newsletter
Gratuita ]
[ Seminari
] [ Seminari
su CD ROM ] [ Consulenza]
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
traduzione italiana e adattamento a cura di Umberto Sorbo
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.
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.
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 da un interprete
più veloce[25].
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.
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.
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.
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.
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.
È 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.
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);
C'è una significativa differenza tra il C ed il
C++ per le funzioni con una lista degli argomenti vuota. In C, la dichiarazione:
int func2();
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".
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 ).
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++).
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:
#include <header>
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à.
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 includere alla vecchia maniera iostream.h :
#include <iostream.h>
si può ora scrivere:
#include <iostream>
Il traduttore può implementare il comando di include in un modo che soddisfa i bisogni di quel particolare compilatore e sistema operativo, se necessario troncando il nome ed aggiungendo una estensione. Naturalmente, si può anche copiare gli header file dati dal fornitore del compilatore ed usarli senza estensione se si vuole usare questo stile.
Le librerie che sono state ereditate dal C sono ancora
disponibili con l'estensione tradizionale .h . Tuttavia, li si può
anche usare con i C++ più moderni preapponendo una "c"
prima del nome:
#include <stdio.h> #include <stdlib.h>
diventa:
#include <cstdio> #include <cstdlib>
E così via per tutti gli header C. Ciò fa distinguere al lettore se si sta usando una libreria C o C++.
L'effetto del nuovo modo di inclusione non è
identico al vecchio: usando .h si ottiene la vecchia versione senza
template, omettendo .h si usa la nuova versione con i template. Di
solito si possono avere problemi se si cerca di mischiare le due forme in
un singolo programma.
Il linker raccoglie moduli oggetto ( che spesso hanno
estensione .o oppure .obj), generati dal compiler, in un programma
eseguibile che il sistema operativo può caricare ed eseguire. Questa
è l'ultima fase del processo di compilazione.
Le caratteristiche del linker possono variare da sistema
a sistema. In generale, si dicono al linker solo i nomi dei moduli oggetto
e delle librerie che si vogliono linkare insieme ed il nome dell' eseguibile.
Qualche sistema richiede che si invochi il linker. Con la maggior parte dei
pacchetti C++ si invoca il linker dal compilatore. In molte situazioni, il
linker viene invocato in un modo che a noi è trasparente.
Qualche vecchio linker non cercherà i file oggetto
e le librerie più di una volta e cercheranno in una lista che gli è
stata data da sinistra verso destra. Ciò significa che l'ordine dei
file oggetto e delle librerie è importante. Se si ha un problema misterioso
che si presenta a tempo di link, una possibilità è che l'ordine
in cui i file sono dati al linker.
Ora che si conosce la terminologia di base, si può
capire come usare una libreria. Per usare una libreria:
Questi passi si applicano anche quando i moduli oggetto
non sono combinati in una libreria. L'inclusione di un header file ed il link
dei moduli degli oggetti sono i passi base per la compilazione separata sia
in C che in C++.
Quando si fa riferimento esterno ad una funzione o variabile
in C o C++, il linker, incontrando questo riferimento, può fare due
cose. Se non ha già incontrato la definizione della funzione o della
variabile, aggiunge l'identificatore alla sua lista dei "riferimenti
non risolti". Se il linker ha già incontrato la definizione, il
riferimento viene risolto.
Se il linker non trova la definizione nella lista dei
moduli oggetto, cerca le librerie. Le librerie hanno una specie di indice
in modo che il linker non ha bisogno di cercare attraverso tutti gli moduli
oggetto nella libreria, esso guarda solo nell'indice. Quando il linker trova
una definizione in una libreria, l'intero modulo oggetto, non solo la definizione
della funzione, viene linkata nel file eseguibile. Si noti che non viene linkata
l'intera libreria l'intera libreria, ma solo il modulo oggetto della libreria
che contiene la definizione che si desidera ( altrimenti i programmi sarebbero
grandi senza bisogno). Se si vuole minimizzare la dimensione del programma
eseguibile, si può considerare di mettere una singola funzione in ogni
file del codice sorgente quando si costruiscono le proprie librerie. Ciò
richiede maggiore lavoro[27],
ma può essere utile all'utente.
Poichè il linker cerca i file nell'ordine in
cui vengono dati, si può dare precedenza all'uso
di una funzione di libreria inserendo un file con la propria funzione, usando
lo stesso nome della funzione, nella lista prima che appaia il nome della
libreria. Poichè il linker risolve qualsiasi riferimento a questa funzione
usando la nostra funzione prima di cercare nella libreria, la nostra funzione
viene usata al posto della funzione della libreria. Si noti che questo può
anche essere un bug e i namespace del C++ prevengono ciò.
Quando viene creato un programma eseguibile C o C++,
alcuni pezzi vengono linkati segretamente. Uno di questi è il modulo
di startup, che contiene le routine di inizializzazione che devono essere
eseguite in qualsiasi momento quando un programma C o C++ inizia. Queste routine
impostano lo stack e inizializza alcune variabili del programma.
Il linker cerca sempre nella libreria standard le versioni
compilate di qualsiasi funzione standard chiamata dal programma. Poichè
la ricerca avviene sempre nella libreria standard, si può usare qualsiasi
cosa della libreria semplicemente includendo l'appropriato file header nel
programma; non si deve dire di cercare nella libreria standard. Le funzioni
iostream, per esempio, sono nella libreria Standard C++. Per usarle si deve
solo includere l'header file <iostream>.
Se si sta usanto un libreria aggiuntiva, si deve esplicitamente
aggiungere il nome della libreria alla lista di file gestiti dal linker.
Proprio perchè si sta scrivendo codice in C++, nessuno ci impedisce di usare funzioni di una libreria. Infatti, l'intera libreria C è inclusa per default nel C++ Standard. È stato fatto un gran lavoro per noi in queste funzioni, quindi ci possono far risparmiare un sacco di tempo.
Questo libro userà le funzioni di libreria del
C++ Standard ( e quindi anche C Standard) quando servono, ma saranno usate
solo funzioni di libreria standard, per assicurare la portabilità dei
programmi. Nei pochi casi in cui le funzioni di libreria che devono essere
usate non sono C++ Standard, saranno fatti tutti i tentativi per usare funzioni
POSIX-compliant. POSIX è uno standard basato su uno sforzo di standardizzazione
Unix che include funzioni che vanno oltre l'ambito della libreria C++. Ci
si può generalmente aspettare di trovare funzioni POSIX su piattaforme
UNIX (in particolare su Linux). Per esempio, se si sta usando il multithreading
è meglio usare la libreria dei thread POSIX perchè il codice
sarà più facile da capire, portare e manutenere ( e la libreria
thread POSIX sarà usata per i suoi vantaggi con il sistema operativo,
se sono fornite).
Ora si conosce abbastanza per creare e compilare un
programma. Il programma userà le classi Standard del C++ iostream.
Esse leggono e scrivono su file e da "standard" input ed output
( che normalmente è la console, ma si può redirezionare ai file
o ai device). In questo semplice programma, un oggetto stream sarà
usato per stampare un messaggio sullo schermo.
Per dichiarare le funzioni ed i dati esterni nella classe
di iostream, si deve include un file header con la dichiarazione
#include <iostream>
Il primo programma usa il concetto di standard output,
che significa " un posto di scopo generico per mandare l'output".
Si vedranno altri esempi che utilizzano lo standard output in modi diversi,
ma qui esso utilizza solo la console. Il package iostream automaticamente
definisce una variabile ( un oggetto ) chiamato cout che accetta tutti
i dati da inviare allo standard output.
Per inviare dati allo standard output, si usa l'operatore
<<. I programmatori C conoscono questo operatore come lo shift a sinistra
di bit, che sarà descritto nel prossimo capitolo. Basta dire che uno
shift a sinistra di bit non ha niente a che fare con l'output. Tuttavia, il
C++ permette l'overloading degli operatori ( sovraccaricamento degli operatori).
Quando si usa l'overloading con un operatore, esso gli fornisce un nuovo significato
quando viene usato con un oggetto di un particolare tipo. Con gli oggetti
iostream, l'operatore << significa " manda a ". Per esempio:
cout << "salve!";
manda la stringa "salve! " all'oggetto chiamato
cout ( che è l'abbreviazione di "console input" ).
Quanto detto è sufficiente per quanto riguarda
l'operatore di overload. Il capitolo 12 tratta l'overloading degli operatori
in dettaglio.
Come menzionato nel Capitolo 1, uno dei problemi incontrati
nel linguaggio C è che si finiscono i nomi delle funzioni e degli identificatori
quando i programmi raggiungono una certa dimensione. Naturalmente, non si
finiscono veramente i nomi, tuttavia diventa più difficile pensarne
di nuovi dopo un po'. La cosa più importante, quando un programma raggiunge
una certa dimensione è tipicamente dividere in pezzi, ognuno dei quali
è costruito e manuntenuto da una persona diversa del gruppo. Poichè
il C ha una singola area dove tutti i nomi degli identificatori e delle
funzioni esistono, ciò significa che tutti gli sviluppatori devono
fare attenzione a non usare gli stessi nomi in situazione dove ci possono
essere conflitti. Ciò diventa noioso, fa perdere tempo e risulta costoso.
Lo Standard C++ possiede un meccanismo per prevenire
questa collisione: la parola riservata namespace. Ogni insieme
di definizioni C++ in una libreria o programma è wrappato - "inglobato"
in un namespace e se qualche altra definizione ha un nome identico, ma si
trova in un namespace differente, allora non c'è collisione.
I namespace sono uno strumento utile e pratico, ma la
loro presenza significa che si deve essere consci di essi prima che si scriva
qualsiasi programma. Se si include semplicemente un header file e si usano
delle funzioni o oggetti da quel header, probabilmente si avranno strani errori
quando si cercherà di compilare il programma, fino al punto che il
compilatore con troverà nessuna dichiarazione per quell'oggetto che
si è appena incluso nel file header! Dopo che si è visto questo
messaggio un po' di volte si diventerà familiari con il suo significato
( che è "si è incluso l'header file ma tutte le dichiarazione
sono all'interno di un namespace e non si è detto al compilatore che
si vuole usare le dichiarazione in quel namespace").
C'è una parola riservata che permette di dire
che si vogliono usare le dichiarazione e/o le definizione di un namespace.
La parola riservata, abbastanza appropriata, è using.
Tutte le librerie standard del C++ sono wrappate in un singolo namespace,
che è std ( sta per standard). Poichè questo
libro usa quasi esclusivamente librerie standard, si vedrà la direttiva
using quasi in ogni programma:
using namespace std;
Ciò significa che si vuole esporre tutti gli
elementi da un namespace chiamato std. Dopo questa dichiarazione,
non ci si deve preoccupare che un particolare componente della libreria sia
dentro un namespace, poichè la direttiva using rende
quel namespace disponibile a tutto il file dove la direttiva using
è stata scritta.
Esporre tutti gli elementi di un namespace dopo che
qualcuno ha avuto qualche problema per nasconderli può sembrare un
po' controproducente ed infatti si dovrebbe fare attenzione nel fare ciò
senza riflettere ( come si imparerà più avanti nel libro). Tuttavia,
la direttiva using espone solo quei nomi per il file corrente, quindi non
è così drastico come può sembrare ( ma si pensi due volte
nel fare ciò in un header file, è imprudente ).
C'è una relazione tra i namespace ed il modo
in cui i file header vengono inclusi. Prima che fossero standardizzate le
moderne inclusioni dei file header ( senza i '.h',
come in <iostream>), il tipico modo di includere un
file header era con '.h', come in <iostream.h>.
A quel tempo i namespace non facevano parte neanche del linguaggio. Quindi
per fornire un compatibilità all'indietro con il codice esistente,
se si scriveva
#include <iostream.h>
significava
#include <iostream> using namespace std;
Tuttavia, in questo libro verrà usato il formalismo
standard dell'include ( senza il '.h') e quindi la direttiva
using deve essere esplicita.
Per ora, questo è tutto ciò che si deve
conoscere sui namespace, ma nel capitolo 10 l'argomento è trattato
in modo esauriente.
Un programma in C o C++ è un insieme di variabili, definizioni di funzione e chiamate a funzioni. Quando inizia il programma, esso esegue codice di inizializzazione e chiama una speciale funzione, main(). Qui si mette il codice principale del programma.
int funzione() { // il codice della funzione va qui(questo è un commento!) }
La funzione di sopra ha una lista di argomenti vuota
ed il corpo contiene solo un commento.
Ci possono essere molti insiemi di parentesi all'interno di una definizione di parentesi, ma ci deve sempre essere almeno una che circonda il corpo della funzione. Poichè main() è una funzione, segue le stesse regole. In C++, main() restituisce sempre un int.
Il C e C++ sono linguaggi liberi da forma. Con poche
eccezioni, il compilatore ignora newline e spazi bianchi, quindi ci deve essere
un modo per determinare la fine di una istruzione. I comandi sono delimitati
da punto e virgola.
I commenti del C iniziano con /* e finiscono con */. Possono includere newline ( a capo ). Il C++ usa i commenti del C ed ha un tipo di commento in più: //, che incomincia un commento che termina con un newline. Conviene di più di /**/ per un commento di una linea ed è molto usato in questo libro.
Ed ora finalmente il primo programma:
//: C02:Hello.cpp // dire Ciao con il C++ #include <iostream> // dichiarazioni Stream using namespace std; int main() { cout << "Ciao, Mondo! Ho " << 8 << " anni oggi!" << endl; } ///:~
All'oggetto cout si passano una serie di argomenti
con l'operatore "<<". Esso stampa questi argomenti nell'ordine
da sinistra verso destra. La speciale funzione iostream endl visualizza
la linea ed un fine linea. Con la iostream si possono unire insieme una serie
di argomenti come questo, che rende la classe semplice da usare.
In C, il testo dentro le doppie virgolette è
tradizionalmente detto una stringa. Tuttavia, la libreria standard C++ ha
una potente classe detta string per manipolare il testo e perciò
si userà il termine più preciso array di caratteri per
il testo dentro le doppie virgolette.
Il compilatore crea lo spazio per gli array di caratteri
e memorizza l'equivalente ASCII per ogni carattere. Il compilatore automaticamente
accoda a quest' array di caratteri un pezzo extra di memoria contenente il
valore 0 che indica la fine dell'array di caratteri.
Dentro un array di caratteri, si possono inserire caratteri
speciali usando le sequenze di escape. Esse consistono in un backslash(\)
seguito da un codice speciale. Per esempio \n significa newline. Il
manuale del compilatore o la guida del C fornisce un insieme completo di sequenze
di escape; altre sono \t (tab),\\(backslash) e \b (backslash).
Si noti che le istruzioni possono continuare su linee multiple e che l'intera istruzione termina con un punto e virgola.
Gli argomenti di array di caratteri e i numeri costanti
sono miscelati insieme nella precedente istruzione cout. Poichè
l'operatore << è sovraccaricato in molti modi quando viene usato
con cout, se possono mandare a cout argomenti diversi ed esso
capirà cosa fare con quel messaggio.
In tutto il libro si noterà
che la prima linea di ogni file è un commento che inizia con i caratteri
con cui inizia un commento ( tipicamente //=, seguiti da due punti e l'ultima
linea del listato finisce con un commento seguito da "/:~".
Questa è una tecnica che uso per estrarre facilmente informazioni dai
file di codice ( il programma per fare ciò può essere trovato
nel volume 2 di questo libro, su www.BruceEckel.com). La prima linea
contiene anche il nome e la locazione del file, quindi ci si può riferire
ad esse nel testo o in altri file e facilmente localizzarlo nel codice sorgente
di questo libro.
Dopo aver scaricato e scompattato il codice sorgente
del libro, si trovi il programma nella sottocartella CO2. Si invochi
il compiler con Hello.cpp come argomento. Con programmi semplici di
un file solo come questo, il compilatore compierà tutto il processo.
Per esempio, per usare il compilatore C++ GNU ( che è disponibile liberamente
su Internet), si scriva:
g++ Hello.cpp
Altri compilatori avranno una sintassi simile, si consulti la documentazione del proprio compilatore per i dettagli.
Finora abbiamo visto solo l'aspetto più rudimentale
della classe iostreams. La formattazione dell'output disponibile con iostreams
include anche funzionalità come la formattazione numerica in decimale,
ottale ed esadecimale. Qui c'è un altro esempio di uso di iostreams:
//: C02:Stream2.cpp // altre caratteristiche di iostream #include <iostream> using namespace std; int main() { // Specificare i formati con i manipulators: cout << "a numero in decimale: " << dec << 15 << endl; cout << "in ottale: " << oct << 15 << endl; cout << "in esadecimale: " << hex << 15 << endl; cout << "un numero in virgola mobile: " << 3.14159 << endl; cout << "carattere non stampabile (escape): " << char(27) << endl; } ///:~
Questo esempio mostra la classe iostreams che stampa i numeri in decimale, ottale ed esadecimale usando i manipulators (manipolatori, che non stampano niente, ma cambiano lo stato dell' output stream). La formattazione dei numeri in virgola mobile è determinata automaticamente dal compilatore. In aggiunta, qualsiasi carattere può essere mandato ad un oggetto stream usando un cast ad un char ( un char è un tipo di dato che memorizza un singolo carattere ). Questo cast assomiglia ad una chiamata a funzione: char(), insieme al valore ASCII del carattere. Nel programma sopra, char(27) manda un "escape" a cout.
Un'importante caratteristica del preprocessore C è
la concatenazione degli array di caratteri. Questa caratteristica è
usata in qualche esempio di questo libro. Se due caratteri quotati sono adiacenti
e non c'è punteggiature tra di loro, il compilatore incollerà
gli array insieme in un singolo array di caratteri. Ciò è particolarmente
utile quando i listati di codice hanno restrizioni sulla larghezza:
//: C02:Concat.cpp // concatenzione di array di caratteri #include <iostream> using namespace std; int main() { cout << " Questa è troppo lunga per essere messa su una " " singola linea ma può essere spezzata " " senza nessun problema\nfinchè non si usa " " punteggiatura per separare array di caratteri " " adiacenti.\n"; } ///:~
A prima vista, il codice di sopra può sembrare errato poichè non c'è il familiare punto e virgola alla fine di ogni linea. Si ricordi che il C ed il C++ sono linguaggi liberi da forma e sebbene si vede solitamente un punto e virgola alla fine di ogni linea, è richiesto un punto e virgola alla fine di ogni istruzione ed è possibile che un' istruzione continui su più linee.
Le classi iostreams forniscono la capacità di
leggere l'input. L'oggetto usato per lo standard input è cin
( che sta per console input). cin normalmente si aspetta un input dalla
console, ma questo input può essere deviato su altre sorgenti. Un esempio
di redirezione verrà mostrato più avanti in questo capitolo.
L'operatore delle iostreams usato con cin è
>>. Questo operatore si aspetta lo stesso genere di input come suo argomento.
Per esempio, se si dà un argomento intero, esso si aspetta un intero
dalla console. Ecco un esempio:
//: C02:Numconv.cpp // Converte un decimale a ottale e esadecimale #include <iostream> using namespace std; int main() { int numero; cout << "Inserisci un numero decimale: "; cin >> numero; cout << "valore in ottale = 0" << oct << numero << endl; cout << "valore in esadecimale = 0x" << hex << numero << endl; } ///:~
Questo programma converte un numero inserito dall'utente
in rappresentazione ottale ed esadecimale.
Mentre il tipico modo di usare un programma che legge
dallo standard input e scrive sullo standard output è interno ad uno
script shell Unix o un file batch Dos, qualsiasi programma può essere
chiamato da un programma C o C++ usando la funzione standard C system(),
che viene dichiarata nel file header <cstdlib>:
//: C02:CallHello.cpp // Chiamare un altro programma #include <cstdlib> // dichiara "system()" using namespace std; int main() { system("Ciao"); } ///:~
Per usare la funzione system(), si da un array
di caratteri che normalmente si digiterebbe al prompt dei comandi del sistema
operativo. Ciò può anche includere gli argomenti della linea
di comando e l'array di caratteri può essere creato a run time (invece
di usare solo un array statico come mostrato sopra). Il comando viene eseguito
ed il controllo ritorna al programma.
Questo programma ci mostra come sia semplice usare una
libreria di funzioni C in C++, si deve solo includere l'header file e chiamare
la funzione. Questa compatibilità verso l'alto dal C al C++ è
un grosso vantaggio se si sta imparando il linguaggio partendo dal C.
Sebbene un array di caratteri può essere molto
utile, è anche limitato. È semplicemente un gruppo di caratteri
in memoria, ma se si vuole fare qualcosa con essi si devono gestire tutti
i piccoli dettagli. Per esempio, la dimensione di un array di caratteri quotati
è fissa a tempo di compilazione. Se si ha un array di caratteri e si
vuole aggiungere qualche carattere ad esso, si deve diventare un po' esperti
( inclusa la gestione della memoria dinamica, copia di array di caratteri
e concatenazione) prima di ottenere ciò che si vuole. Ciò è
esattamente il genere di cose che vorremmo un oggetto facesse per noi.
La classe string
del C++ Standard è stata progettata per occuparsi ( e nascondere )
tutte le manipolazioni a basso livello degli array di caratteri che erano
richieste precedentemente al programmatore C. Queste manipolazioni sono state
una costante motivo di spreco di tempo e sorgente di errori dall'inizio del
linguaggio C. Quindi, sebbene un intero capitolo è dedicato alla classe
string nel Volume 2 di questo libro, la string è così
importante ( rende la vita molto più facile) che verrà introdotta
qui ed usata molto nella prima parte del libro.
Per usare string si deve includere l'header file <string>
del C++. La classe string è nel namespace std quindi
è necessaria una direttiva using. Grazie all' overloading dell'operatore,
la sintassi per l'uso delle string è abbastanza intuitiva:
//: C02:HelloStrings.cpp // Le basi della classe string del C++ Standard #include <string> #include <iostream> using namespace std; int main() { string s1, s2; // stringhe vuote string s3 = "Ciao, Mondo."; // inizializzazione string s4("Io sono "); // altra inizializzazione s2 = "Oggi"; // assegnamento s1 = s3 + " " + s4; // somma s1 += " 8 "; // concatenazione cout << s1 + s2 + "!" << endl; } ///:~
Le prime due string, s1 e s2, sono
vuote, mentre s3 e s4 mostrano due modi equivalenti di inizializzare
le stringhe con array di caratteri ( si possono inizializzare string
da altre stringhe).
Si può assegnare alla stringa usando "=".
Ciò rimpiazza il contenuto precedente della stringa con qualunque cosa
è sulla parte destra e non ci si deve preoccupare di ciò che
accade al contenuto precedente, che è gestito automaticamente. Per
unire stringhe si usa l'operatore "+", che permette anche di unire
array di caratteri con stringhe. Se si vuole concatenare sia una stringa che
un array di caratteri ad un'altra stringa, si può usare "+".
Infine, si noti che iostreams già sa cosa fare con le stringhe, quindi
si può mandare una stringa ( o un'espressione che produce una stringa,
che si ottiene da s1+s2+"!") direttamente a cout per stamparla.
In C, aprire e manipolare file richiede molta esperienza
con il linguaggio data la complessità delle operazione. Tuttavia, la
libreria del C++ iostream fornisce un modo semplice di manipolare file e quindi
questa funzionalità può essere introdotta più semplicemente
di come sarebbe in C.
Per aprire file in lettura e scrittua, si deve includere
<fstream>. Sebbene ciò includerà automaticamente
<iostream>, generalmente è più prudente includere
esplicitamente <iostream> se si prevede di usare cin,cout,
ecc...
Per aprire un file in
lettura, si crea un oggetto ifstream, che si comporta come cin.
Per aprire un file in scrittura, si crea un oggetto ofstream, che si
comporta come cout. Una volta che si è aperto il file, si legge
da esso o si scrive come si farebbe con qualsiasi altro oggetto iostream.
È semplice.
Una delle funzioni più semplici della libreria
iostream è getline(), che permette di leggere una linea ( terminata
da newline) in una stringa[28].
Il primo argomento è l'oggetto ifstream da cui si sta
leggendo ed il secondo argomento è l'oggetto string. Quando
la chiamata a funzione è terminata, l'oggetto string conterrà
la linea.
Ecco un semplice esempio, che copia il contenuto di
un file in un altro:
//: C02:Scopy.cpp // Copia un file su un altro, una linea alla volta #include <string> #include <fstream> using namespace std; int main() { ifstream in("Scopy.cpp"); // Apre in lettura ofstream out("Scopy2.cpp"); // Apre in scrittura string s; while(getline(in, s)) // Elimina il carattere newline out << s << "\n"; // ... lo riaggiunge } ///:~
Per aprire i file, si passano agli oggetti ifstream
e ofstream i nomi dei file che si vogliono creare, come sopra.
Qui viene introdotto un nuovo concetto, che è
il ciclo while. Sebbene questo verrà spiegato in dettaglio nel
prossimo capitolo, l'idea base è che l'espressione tra parentesi che
seguono il while controlla l'esecuzione di un istruzione susseguente
( che possono anche essere istruzioni multiple, delimitate da parentesi graffe).
Finchè l'espressione tra parentesi ( in questo caso, getline(in,s))
vale true, le istruzioni controllate dal while continueranno ad essere
eseguite. Ne segue che getline() restituirà un valore che può
essere interpretato come true se un'altra linea è stata letta con successo
e false se non ci sono più linee da leggere. Quindi, il ciclo while
di sopra legge tutte le linee dal file di input e manda ogni linea al file
di output.
getline() legge caratteri finchè
non scopre un newline ( un carattere di terminazione che può essere
cambiato, ma questo argomento verrà trattato nel capitolo di iostream
nel Volume 2). Tuttavia, elimina il newline e non lo memorizza nella stringa
risultante. Quindi, se vogliamo che il file copiato sia identico al file sorgente,
dobbiamo aggiungere un newline, come mostrato.
Un esempio interessante è copiare l'intero file
in una singola stringa.
//: C02:FillString.cpp // Legge un intero file e lo copia in una singola stringa #include <string> #include <iostream> #include <fstream> using namespace std; int main() { ifstream in("FillString.cpp"); string s, linea; while(getline(in, linea)) s += linea + "\n"; cout << s; } ///:~
Grazie alla natura dinamica delle stringhe, non ci si
deve preoccupare di quanta memoria serve per allocare una stringa, la stringa
si espanderà automaticamente qualsiasi cosa ci si metta dentro.
Una delle cose utili mettendo un intero file in una
stringa è che la classe string ha molte funzioni per cercare
e manipolare che permettono di modificare il file come una singola stringa.
Tuttavia ciò ha delle limitazioni. Per prima cosa, è spesso
conveniente trattare un file come un insieme di linee invece di un enorme
blocco di testo. Per esempio, se si vuole aggiungere la numerazione di linea
è molto più facile se si ha ogni linea come un oggetto stringa
separato. Per ottenere ciò, abbiamo bisogno di un altro approccio.
Con le stringhe, si può espandere una stringa
senza sapere di quanta memoria si avrà bisogno. Il problema della lettura
delle linee da un file in oggetti stringa individuali è che non si
conosce il numero di stringhe di cui si ha bisogno, lo si conosce solo dopo
che si è letto l'intero file. Per risolvere questo problema, abbiamo
bisogno di una specie di contenitore che si espanda automaticamente per contenere
tante stringhe quante se ne hanno bisogno.
Infatti, perchè limitarci a gestire stringhe?
Ne consegue che questo tipo di problema " non sapere quanto si ha di
qualcosa mentre si scrive un programma" capita spesso. E questo oggetto
"contenitore" sarebbe molto più utile se potesse gestire
ogni tipo di oggetto! Fortunatamente, la libreria Standard C++ ha una soluzione
bella e pronta : le classi container standard. Esse sono une delle vere potenzialità
del C++ Standard.
C'è spesso un po' di confusione tra i container
e algorithm nella libreria Standard C++ e l'entità nota come STL. Standard
Template Library è il nome usato da Alex Stepanox ( che a quel tempo
lavorava alla Hewlett-Packard) quando presentò la sua libreria alla
commissione per gli Standard del C++ in un meeting a San Diego, California
nella primavera del 1994. Il nome colpì, specialmente dopo che HP decise
di renderla pubblica. Nel frattempo, la commissione la integrò nella
libreria Standard C++, facendo un gran numero di cambiamenti. Lo sviluppo
della STL continua alla Silicon Graphics (SGI; see http://www.sgi.com/Technology/STL).
La STL SGI diverge dalla libreria Standard C++ su molti punti sottili. Quindi
sebbene sia un equivoco popolare, il C++ Standard non include la STL. Può
confondere un po' poichè i container e algorithm nella libreria Standard
C++ hanno la stessa root( e di solito gli stessi nomi) come la SGI STL. In
questo libro, si scriverà "la libreria Standard C++" ed i
"container della libreria standard" oppure qualcosa di simile e
si eviterà il termine STL.
Anche se l'implementazione dei container e algorithm
della libreria standard C++ utilizza concetti avanzati e la spiegazione completa
si estende su due grossi capitoli del Volume 2 di questo libro, questa libreria
può essere molto potente anche senza conoscerne molto di essa. È
così utile che il più semplice dei container standard, il vector,
viene presentato in questo capitolo ed utilizzato lungo tutto il libro. Si
può fare tantissimo solo usando vector e non preoccupandosi
della sua implementazione( di nuovo, una importante meta della OOP). Poichè
si imparerà molto di più di esso e degli altri container quando
si leggerano i relativi capitoli del Volume 2, mi perdonerete se i programmi
che usano vector nella prima parte del libro non sono esattamente ciò
che un programmatore C++ con esperienza farebbe. In molti casi l'uso mostrato
qui sarà adeguato.
La classe vector è un template, che significa
che può essere applicato efficientemente a tipi diversi. Cioè
possiamo creare un vector di Figure, un vector di Gatti,
un vector di string,ecc... Fondamentalmente , con un template
si crea una "classe di niente". Per dire al compilatore con che
classe lavorerà ( in questo caso, cosa vector conterrà
), si mette il nome del tipo desiderato tra < > . Quindi un vector
di string sarà denotato con vector<string>. Quando si
fa ciò, si otterrà un vector che contiene solo oggetti
string e si avrà un messaggio di errore da compilatore se si
cerca di metterci qualcos'altro.
Poichè vector esprime il concetto di un
container, ci deve essere un modo di mettere cose nel container e un modo
per riottenerle indietro. Per aggiungere un elemento alla fine di un vector,
si usa la funzione membro push_back() ( si ricordi che, poichè
è una funzione membro si usa un "." per chiamarlo con un
oggetto particolare). La ragione del nome di questa funzione membro può
sembrare un po' verbosa, push_back() invece semplicemente come "metti",
poichè ci sono altri container e altre funzioni membro per mettere
nuovi elementi nei container. Per esempio, c'è un insert() per
mettere qualcosa nel mezzo di un container, vector lo supporta ma il
suo uso è più complicato e lo esploreremo nel Volume 2 del libro.
C'è anche un push_front() ( non fa parte di vector) per
mettere cose all'inizio. Ci sono molte funzioni membro in vector e
molti container nella libreria Standard C++, ma si resterà sorpresi
di quanto si può fare con essi conoscendo solo poche caratteristiche.
Quindi si possono mettere nuovi elementi in un vector
con push_back(), ma come si riottengono indietro? La soluzione è
ancora più intelligente ed elegante, è utilizzato l'overloading
dell'operatore per far sembrare vector come un array. L'array ( che
sarà descritto nel prossimo capitolo) è un tipo di dato che
è disponibile virtualmente in ogni linguaggio di programmazione quindi
si è familiari con esso. Gli array sono aggregati, che significa
che essi consistono in un numero di elementi ammucchiati insieme. La caratteristica
distintiva di un array è che questi elementi hanno la stessa dimensione
e sono posizionati uno dopo l'altro. La cosa più importante è
che questi elementi possono essere selezionati con un indice, che significa
che si può dire "voglio l'elemento n-simo e quel elemento sarà
ottenuto". Sebbene ci sono eccezioni nei linguaggi di programmazione,
l'indicizzazione è realizzata usando le parentesi quadre, quindi se
si ha un array a e si vuole il quinto elemento, si scrive a[4]
( si noti che l'indice parte da zero).
Questa notazione molto compatta e potente è incorporata
in vector usando l'overloading dell'operatore, proprio come "<<"
e ">>" sono state incorporate nelle iostream. Di nuovo, non
c'è bisogno di sapere come è stato implementato l'overloading,
ma è utile sapere che c'è un po' di magia per far funzionare
[ ] con vector.
Con quanto detto in mente, si può ora vedere un programma che usa vector. Per usare un vector, si include l'header file <vector>:
//: C02:Fillvector.cpp // Copia un intero file in un vector di stringhe #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; int main() { vector<string> v; ifstream in("Fillvector.cpp"); string linea; while(getline(in, linea)) v.push_back(linea); // Aggiunge la linea alla fine // Aggiunge i numeri di linea: for(int i = 0; i < v.size(); i++) cout << i << ": " << v[i] << endl; } ///:~
Molto di questo programma è simile al precedente; un file viene aperto e le linee vengono lette e messe in un oggetto string una alla volta. Tuttavia, questi oggetti stringa vengono inseriti in un vector v. Una volta che il ciclo while è completo, l'intero file risiede in memoria dentro v.
La prossima istruzione
nel programma è detta un ciclo for. È simile ad un ciclo
while tranne che aggiunge qualche simbolo in più. Dopo il for,
c'è una "espressione di controllo" dentro le parentesi, proprio
come il ciclo while. Tuttavia, questa espressione di controllo è
composta di tre parti: una parte che inizializza, una che testa per vedere
se si può uscire dal ciclo ed una che cambia qualcosa, tipicamente
per spostarsi su una sequenza di elementi. Questo programma mostra il ciclo
for nel modo in cui è più comunamente usato: la parte di inizializzazione
int i = 0 crea un intero i per usarlo come contatore del ciclo
e gli assegna il valore iniziale zero. La parte di test dice che per rimanere
nel ciclo, i deve essere minore del numero di elementi del vector
v ( si usa size(), che è stato buttato nel codice, ma si deve ammettere
che ha un significato abbastanza chiaro). La porzione finale usa l'operatore
auto-incremento per aggiungere uno al valore di i. i++ significa:
"prendi il valore di i, aggiungi uno ad esso e rimetti il risultato
di nuovo in i". Quindi il risultato globale del ciclo for
è di prendere una variabile i e passare dal valore zero ad un
valore minore della dimensione di vector. Per ogni valore di i, l'istruzione
cout viene eseguita e scrive una linea che consiste nel valore di i
( magicamente convertito in un array di caratteri da cout), due punti
ed uno spazio, la linea letta dal file ed un newline con endl. Quando
si compila e si lancia, si vede che l'effetto è l'aggiunta dei numeri
di linea al file.
Grazie al modo in cui funziona l'operatore ">>"
con iostream, si può facilmente modificare il programma di sopra in
modo che spezzi l'input in parole separate da spazi bianchi invece di linee:
//: C02:GetWords.cpp // Spezza un file in parole separate da spazi bianchi #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; int main() { vector<string> parole; ifstream in("GetWords.cpp"); string parola; while(in >> parola) parole.push_back(parola); for(int i = 0; i < parole.size(); i++) cout << parole[i] << endl; } ///:~
L'espressione
while(in >> parola)
è ciò che ottiene in input una parola
alla volta e quando questa espressione vale false significa che è stata
raggiunta la fine del file. Naturalmente, delimitare le parole con spazi bianchi
è abbastanza grezzo, ma è un esempio semplice. Più in
avanti nel libro si vedranno esempi più sofisticati che permetteranno
di spezzare l'input nel modo che si vuole.
Per dimostrare come sia facile usare un vector
con qualsiasi tipo, ecco un esempio che crea un vector<int> :
//: C02:Intvector.cpp // Creazione di un vector che contiene interi #include <iostream> #include <vector> using namespace std; int main() { vector<int> v; for(int i = 0; i < 10; i++) v.push_back(i); for(int i = 0; i < v.size(); i++) cout << v[i] << ", "; cout << endl; for(int i = 0; i < v.size(); i++) v[i] = v[i] * 10; // assegnazione for(int i = 0; i < v.size(); i++) cout << v[i] << ", "; cout << endl; } ///:~
Per creare un vector che gestisce un tipo diverso,
si usa quel tipo come un argomento di template ( l'argomento nelle < >
). I template e le librerie di template ben progettate sono intese per essere
usate con facilità.
Quest'esempio continua per dimostrare un'altra caratteristica
essenziale di vector. Nell'espressione
v[i] = v[i] * 10;
si può vedere che il vector non è
usato solo per mettere cose in esso e riprenderle. Si può anche assegnare
a qualsiasi elemento di un vector, usando anche l'operatore di indicizzazione
delle parentesi quadre. Ciò significa che vector è progettato
per scopi generali ed è molto flessibile per lavorare con insiemi di
oggetti.
L'intento di questo capitolo è di mostrare come
possa essere semplice la programmazione orientata agli oggetti, se
qualcun altro ha fatto il lavoro di definire gli oggetti per noi. In quel
caso, si include un header file, si creano gli oggetti e si mandano messaggi
ad essi. Se i tipi che si stanno usando sono potenti e ben progettati, allora
non si avrà molto da lavorare ed il programma risultante sarà
anch'esso potente.
Nel mostrare la facilità di uso delle classi
di librerie con la OOP, questo capitolo presenta anche la famiglia delle iostream
(in particolare, quelle che leggono e scrivono da e verso console e file),
la classe string ed il template vector. Si è visto come
sia semplice usarle e si possono immaginare tante cose che ora si possono
realizzare, ma c'è ancora molto di cui esse sono capaci[29].
Anche quando si utilizza un insieme limitato di funzionalità di questi
strumenti nella prima parte del libro, esse forniscono comunque un grosso
passo avanti rispetto all'apprendimento di un linguaggio come il C e sebbene
apprendere gli aspetti a basso livello del C sia educativo, c'è anche
un gran dispendio di tempo. Quindi si sarà molto più produttivi
se si hanno oggetti che gestiscono gli aspetti dei livelli bassi. Dopo tutto,
il punto cruciale della OOP è nascondere i dettagli in modo che si
possa dipingere con un pennello più grande.
Tuttavia, per come l'OOP cerca di essere ad alto livello,
ci sono aspetti fondamentali del C che non si possono non conoscere e questi
saranno illustrati nel prossimo capitolo.
Le soluzioni agli esercizi selezionati si possono trovare nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile in cambio di un piccolo onorario su www.BruceEckel.com.
[25] I confini
tra compilatori ed interpreti tendono a diventare sfumati, specialmente con
Python, che ha molte caratteristiche e potenzialità dei linguaggi compilati
ma l'immediatezza di un linguaggio interpretato.
[26] Python
è di nuovo un eccezione, poichè fornisce compilazione separata.
[27] Raccomanderei
di usare Perl o Python per automatizzare questo compito come parte del processo
di preparazione delle librerie (si veda www.Perl.org oppure www.Python.org).
[28] Ci sono
diverse varianti di getline( ), che saranno discusse nel capitolo
iostream nel Volume 2.
[29] Se si è
particolarmente ansiosi di vedere tutte le cose che si possono fare con questi
ed altri componenti della libreria Standard, si veda il Volume 2 di questo
libro su www.BruceEckel.com ed anche www.dinkumware.com.
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultima Modifica: 27/02/2003