MindView Inc.

 

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[
Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

traduzione italiana e adattamento a cura di Umberto Sorbo

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 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.

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:

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".

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:

#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à.

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 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.

Linking

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.

Utilizzo delle librerie

Ora che si conosce la terminologia di base, si può capire come usare una libreria. Per usare una libreria:

  1. Includere l'header file della libreria.
  2. Usare le funzioni e le variabili della libreria.
  3. Linkare la libreria nel programma eseguibile.

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++.

Come il linker cerca una libreria

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ò.

Aggiunte segrete

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.

Usare librerie C

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).

Il primo programma C++

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.

Usare le classi di iostream

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.

Namespace

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.

Fondamenti della struttura del programma

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.

"Ciao, mondo!"

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.


Lanciare il compiler

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.

Ancora riguardo le iostreams

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.

Concatenazione di array di caratteri

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.

Leggere l'input

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.

Chiamare altri programmi

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.

Introduzione a strings

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.

Leggere e scrivere i file

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.

Introduzione a vector

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.

Sommario

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.

Esercizi

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.

  1. Modificare Hello.cpp in modo che stampi il proprio nome ed età ( o la misura delle scarpe oppure l'età del proprio cane, se vi fa sentire meglio). Compilare ed eseguire il programma.
  2. Usare Stream2.cpp e Numconv.cpp come linee guida, creare un programma che chiede il raggio di un cerchio e stampa l'area di quel cerchio. Si usi solo l'operatore '*' per il quadrato del raggio. Non stampare il valore in ottale o esadecimale ( funziona solo con tipi interi).
  3. Creare un programma che apre un file e conta le parole separate da spazi bianchi in quel file.
  4. Creare un programma che conta la ricorrenza di una parola in un file (usare l'operatore == della classe string ).
  5. Cambiare Fillvector.cpp in modo che stampi le linee dall'ultima alla prima.
  6. Cambiare Fillvector.cpp in modo che concatena tutti gli elementi del vector in una singola stringa prima di stamparla, ma non si aggiunga la numerazione della linea.
  7. Visualizzare un file una linea alla volta, aspettando che l'utente prema il tasto Enter dopo ogni linea.
  8. Creare un vector<float> e mettere 25 numeri in esso usando un ciclo for. Visualizzare il vector.
  9. Creare tre oggetti vector<float> e riempire i primi due come nell'esercizio precedente. Scrivere un ciclo for che somma ogni elemento corrispondente nei primi due vector e mette il risultato nel terzo vector. Visualizzare tutti e tre i vector.
  10. Creare un vector<float> e metterci 25 numeri come nell'esempio precedente. Fare il quadrato di ogni numero e rimettere il risultato nella stessa posizione nel vector. Visualizzare il vector prima e dopo la moltiplicazione.


[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