Sei sulla pagina 1di 43

C+oversione 0.

1 (31 agosto 2011)

Ivan Lanese, Cosimo Laneve

Dipartimento di Scienze dellInformazione via di Mura Anteo Zamboni, 7 40127 Bologna

lanese@cs.unibo.it

INDICE

Indice
1 Premessa 2 Sintassi dei token elementari 3 Struttura di un programma C+o3.1 Commenti . . . . . . . . . . . . . 3.2 I tipi di dato fondamentali . . . . 3.2.1 Interi . . . . . . . . . . . 3.2.2 Reali . . . . . . . . . . . . 3.2.3 Caratteri . . . . . . . . . 3.2.4 Booleani . . . . . . . . . . 3.2.5 Operatori relazionali . . . 3.2.6 Cast . . . . . . . . . . . . 3.2.7 Il tipo void . . . . . . . . 3.3 Dichiarazione dei dati . . . . . . 3.3.1 Dichiarazione di variabili 3.3.2 Dichiarazione di costanti . 3.4 Espressioni . . . . . . . . . . . . 3.5 Comandi . . . . . . . . . . . . . . 3.5.1 Assegnamento . . . . . . 3.5.2 Comandi di input/output 3.5.3 Comando composto . . . 3.5.4 Comandi condizionali . . 3.5.5 Comandi iterativi . . . . . 3.6 Regole di scope . . . . . . . . . . 3.6.1 Approccio bottom-up . . 3.6.2 Approccio top-down . . . 3 3 4 4 5 5 5 6 6 6 6 7 7 7 7 8 8 9 9 10 10 11 13 14 14 15 16 17 18 19 19 21 21 21 21 22 24 24

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

4 Funzioni 4.1 Denizione e chiamata di funzioni 4.2 Funzioni con parametri . . . . . . . 4.3 Funzioni che ritornano un valore . 4.4 Prototipi . . . . . . . . . . . . . . . 4.5 Funzioni ricorsive . . . . . . . . . . 4.6 Overloading . . . . . . . . . . . . . 5 Strutture dati 5.1 Array . . . . . . . . . . . . 5.1.1 Denizione e utilizzo 5.1.2 Array e funzioni . . 5.1.3 Stringhe . . . . . . . 5.2 Strutture . . . . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

INDICE 5.2.1 Denizione e uso . . . . . . . . . . . . . . . . . 5.2.2 Strutture e funzioni . . . . . . . . . . . . . . . Puntatori . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.1 Dichiarazione di una variabile di tipo puntatore 5.3.2 Operazioni sui puntatori . . . . . . . . . . . . . 5.3.3 Allocazione dinamica della memoria . . . . . . 5.3.4 Puntatori e passaggio dei parametri . . . . . . Riferimenti . . . . . . . . . . . . . . . . . . . . . . . . oggetti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3 24 26 28 28 28 30 31 32 33 33 33 34 35 35 36 36 37 37 39 39 39 39 40 41 41 42

5.3

5.4

6 Programmazione orientata agli 6.1 Classi . . . . . . . . . . . . . 6.1.1 Metodi . . . . . . . . 6.1.2 Incapsulamento . . . . 6.1.3 Costruttori . . . . . . 6.1.4 Attributi di tipo classe 6.1.5 La keyword this . . . 6.1.6 Operatori overloaded . 6.2 Ereditariet` . . . . . . . . . . a 6.2.1 Classi derivate . . . . 6.2.2 Costruttori . . . . . . 6.3 Template . . . . . . . . . . . 6.3.1 Funzioni template . . 6.3.2 Classi template . . . . 6.4 Classi della libreria . . . . . . 6.4.1 La classe string . . . . 6.4.2 Il template vector . . Riferimenti bibliograci

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

SINTASSI DEI TOKEN ELEMENTARI

Premessa

` Il C+o-1 (da leggere C pi` o meno) ` una variante del C++ denita per ni didattici. E u e un sottoinsieme del C++, che esclude le caratteristiche del C++ che portano alla scrittura di programmi di dicile comprensione. Qualunque compilatore C++ ` in grado di compilare un e programma C+o-. Il C+o- sar` lunico linguaggio accettato durante il corso. In particolare, a programmi corretti in C++ ma non in C+o- verranno considerati errati.

Sintassi dei token elementari

La sintassi del C+o- verr` formalizzata attraverso una grammatica in cui ::= ` il simbolo di a e riscrittura, | indica unalternativa, { } zero o pi` occorrenze, [ ] una o nessuna occorrenza, ecc. u letter ::= a | . . . | z | A | . . . | Z | _ digit ::= 0 | . . . | 9 La grammatica consente di derivare tutti gli elementi validi di una determinata categoria sintattica. Ad esempio la prima regola qui sopra denisce come elementi validi della categoria sintattica letter tutte le lettere dellalfabeto e il simbolo di sottolineatura. In questo caso la derivazione termina con una sola applicazione della regola, perch si ottiene immediatamente un e simbolo terminale. In questo testo i simboli terminali sono indicati con caratteri a spaziatura ssa come questi. In generale, una derivazione termina quando la stringa generata ` composta e da soli simboli terminali. Qui sotto vediamo le regole per le costanti. sign ::= + | int-literal-no-sign ::= digit { digit } int-literal = [ sign ] int-literal-no-sign oat-literal ::= [ sign ] [ int-literal-no-sign ] . int-literal-no-sign [ (e | E) [ sign ] int-literal-no-sign ] char-literal ::= c | \n | \t | \\ | \ | . . . string-literal ::= "{ char-literal }" boolean-literal ::= true | false id ::= letter { (letter | digit) } Vedere [Stro] per lelenco completo delle sequenze di escape dei caratteri. Da notare che per le costanti oat-literal non possono mancare contemporaneamente la parte decimale e lesponente, altrimenti la costante pu` essere confusa con una costante intera. o
Si ringraziano Nadia Busi e Luca Padovani, autori, insieme al secondo autore, del C-- e del relativo manuale, a cui questo manuale deve molto.
1

5 Esempio 1 Derivazione della costante intera 123: int-literal ::= ::= ::= ::= ::= ::= sign int-literal-no-sign sign digit digit digit sign digit digit 3 sign digit 2 3 sign 1 2 3 -123

Quando una stringa di simboli terminali ` derivabile a partire da una categoria sintattica e allora diciamo che tale stringa appartiene alla categoria sintattica. Non si possono usare le parole chiave del C come identicatori validi (si veda lelenco completo in [Stro], a pag. 794): bool, int, char, float, double, long, signed, unsigned, short, for, while, do, if, else, static, const, switch, case, continue, break, return, extern, register, volatile, sizeof, typedef, try, catch class, namespace, new, public, private, friend ` E anche sconsigliato usare gli stessi nomi della libreria standard (es. vector).

Struttura di un programma C+o-

Diamo una prima approssimazione (senza introdurre, per ora, i concetti di funzione, namespace, . . . ): program declaration-part variable-declaration statement-part type ::= ::= ::= ::= ::= int main() { declaration-part statement-part } { variable-declaration } [const] type id { , id } ; { statement } simple-type

3.1

Commenti

` E possibile (e opportuno) inserire commenti in un programma C+o- per chiarirne il funzionamento. I commenti sono delimitati da sequenze di caratteri particolari: /* prima del commento, e */ dopo il commento. I commenti possono occupare pi` righe. u Esempio 2 /* Questo ` un commento e su pi` righe */ u In C+o- i commenti possono essere inseriti in righe dedicate o in fondo a righe che contengono altri elementi del programma.

STRUTTURA DI UN PROGRAMMA C+O-

I commenti sono solo ad uso del programmatore e non hanno nessun eetto sul risultato della compilazione. Tuttavia ` fondamentale inserire commenti per consentire ad altri (o allautore e stesso, magari dopo molto tempo) di comprendere il programma. Commenti signicativi sono la descrizione del signicato delle variabili, la descrizione degli algoritmi usati e la descrizione delleetto di una funzione. Commenti non signicativi, quali /*somma a e b*/ dopo lespressione a+b, sono da evitare.

3.2

I tipi di dato fondamentali

Un tipo di dato ` denito in termini di un insieme di valori che appartengono a quel tipo ed un e insieme di operazioni sui valori. La sintassi dei tipi fondamentali in C+o- ` la seguente: e simple-type ::= | | | | 3.2.1 Interi int double char bool void

Sono un sottoisieme nito della categoria sintattica int-literal. Finito perch un calcolatore ha e un numero massimo di cifre che pu` memorizzare in modo conveniente e/o eciente. o In C+o- il tipo intero fondamentale ` int, ma sono possibili anche le varianti unsigned e (senza segno), short, long, e cos` via. Sui calcolatori del laboratorio gli int sono a 32 bit, questo signica che si possono rappresentare numeri nel range 231 . . . + 231 1 per gli interi con segno (nel range 0 . . . + 232 1 per gli interi senza segno). Le operazioni aritmetiche ammesse sono quelle classiche: +, -, *, / e % per il resto della divisione intera (modulo). Esistono inoltre le operazioni di manipolazione dei bit &, |, ^, ~ (and, or, xor, not) e lo shift <<, >>. Il + ed il - possono anche essere usati in modo unario.
Loperatore di shift a destra >> si comporta in modo dierente a seconda che lintero alla sua sinistra sia con o senza segno. Nel primo caso viene eettuato uno shift aritmetico (con propagazione del bit di segno), nel secondo caso viene eettuato uno shift puramente logico.

La divisione per 0 ha un comportamento indenito. Normalmente comporta la terminazione immediata del programma. Lunderow e loverow invece interrompono il programma e non vengono mai segnalati in alcun modo. Lesecuzione del programma procede normalmente (ma di solito il risultato non ` quello atteso). e Esistono anche delle funzioni della libreria standard (cstdlib.h), come abs per il valore assoluto. Per usare una libreria ` necessario includerla. Per usare cstdlib.h inserire prima del main: e #include <cstdio>

3.2.2

Reali

Sono un sottoinsieme nito della categoria sintattica real-literal. La precisione ` nita ed ` e e stabilita in termini di cifre signicative dopo la virgola.

3.2

I tipi di dato fondamentali

In C+o- i tipi reali sono float per la precisione singola e double per la doppia precisione. Sui calcolatori del laboratorio i float sono a 32 bit e i double a 64 bit. Le operazioni aritmetiche ammesse sono le stesse che per gli interi, escluso il modulo. Da notare che il linguaggio utilizza gli stessi simboli per le operazioni analoghe tra interi e reali (operator overloading). Esistono numerose funzioni di libreria (cmath.h): fabs, sqrt, sin, log,. . . 3.2.3 Caratteri

Sono un insieme nito di simboli, lencoding pi` diuso ` lASCII. u e In C+o- il tipo carattere ` char, che di solito occupa un solo byte di memoria (8 bit). Le e costanti carattere sono racchiuse tra singoli apici. Ci sono delle sequenze di escape, quali \n per la capo. Nelle usuali operazioni aritmetiche i caratteri vengono automaticamente convertiti dal compilatore a numeri interi (vedi 3.2.6), per cui ` possibile scrivere ad esempio A + 1. e La trattazione delle stringhe verr` rimandata alla sezione 5.1.3. a 3.2.4 Booleani

In C+o- i valori booleani sono contenuti in variabili di tipo bool, con valori true e false. I booleani vengono automaticamente convertiti dal compilatore in numeri interi, con 0 per false e 1 per true. Le operazioni sui booleani sono &&, ||, ! (rispettivamente and, or e not). 3.2.5 Operatori relazionali

Sono operatori che permettono di vericare la relazione dordine che sussiste tra due elementi dello stesso tipo: ==, !=, <, >, <=, >= (uguale, diverso, minore, maggiore, minore o uguale, maggiore o uguale). Operano indierentemente tra numeri interi e reali e, di conseguenza, anche tra caratteri e booleani. Il risultato di un confronto ` un valore booleano. e 3.2.6 Cast

In C+o- ` possibile specicare operazioni aritmetiche (o relazionali) tra tipi di dato eterogenei, e come ad esempio tra interi e reali, tra interi e caratteri, ecc. In questi casi il compilatore converte il tipo pi` piccolo al tipo pi` grande. Ad esempio, in u u 1 + 67.3 l1 viene dapprima convertito a reale, e poi la somma viene eseguita tra due reali (di conseguenza anche il risultato ` un numero reale). Questa operazione ` chiamata cast. e e

8 3.2.7 Il tipo void

STRUTTURA DI UN PROGRAMMA C+O-

Il tipo void ` un tipo speciale a cui non corrisponde alcun valore ed alcuna operazione. Viee ne usato principalmente per specicare che una funzione non ritorna un valore o non accetta argomenti.

3.3
3.3.1

Dichiarazione dei dati


Dichiarazione di variabili

Diamo due denizioni del concetto di variabile: denizione astratta: una variabile ` unentit` il cui valore pu` cambiare durante lesecuzione e a o del programma; denizione concreta: una variabile ` unarea di memoria che ` stata riservata dal proe e grammatore, tramite unopportuna dichiarazione, per conservare un dato di un certo tipo. Allentit` (nella prima denizione) o allarea di memoria (nella seconda denizione) si accede a per mezzo di un nome, detto appunto nome di variabile. Esempio 3 int a,b; double variabile_reale; Osservazione: il valore di una variabile ` indenito al momento della dichiarazione, diventa e denito solo dopo che la variabile ` stata inizializzata (ad esempio tramite un assegnamento). e Una variabile pu` assumere solo valori appartenenti al suo tipo di denizione, ma vale o comunque il discorso dei cast come per le operazioni tra tipi eterogenei (vedi sezione 3.2.6). ` E possibile dichiarare pi` variabili dello stesso tipo in una stessa dichiarazione. u 3.3.2 Dichiarazione di costanti

In un programma C+o- ` possibile specicare che il valore di una variabile non cambia mai e tramite il modicatore const. La variabile diventa cos` una costante. Il compilatore segnala un errore nel caso si tenti di cambiare il valore di una costante. Esempio 4 const double pi 3.14; ... area = raggio * raggio * pi; ... ` E buona norma usare costanti per tutti i numeri magici che compaiono in un programma.

3.4

Espressioni

3.4

Espressioni

Denizione 1 Unespressione rappresenta una sequenza di operazioni che restituisce un valore. Esempio 5 (3 + 2) * 4 3 + 2 * 4 3 + x * 4.0 Si distinguono operandi (valori, identicatori, chiamate di funzioni) e operatori (+, -, . . . ). Lordine di valutazione ` determinato dalle regole di precedenza degli operatori speciche del e linguaggio (vedi [Stro], pag. 120) e dalle parentesi. A parit` di precedenza ` possibile stabilire a e lordine di valutazione guardando lassociativit` degli operatori (di solito a sinistra). Che vuol a dire? Esempio 6 1 - 2 + 3 In C+o-, gli operatori + e - hanno la stessa precedenza ed entrambi sono associativi a sinistra. Ci` signica che un operando circondato da + e - verr` sempre catturato dalloperatore alla o a sua sinistra. Lesempio di cui sopra ` quindi equivalente a (1 - 2) + 3. e Segue la sintassi (semplicata) delle espressioni in C+o-: expression ::= | | operand ::= | lhs-expression ::= literal ::= unary-operator ::= binary-operator ::= operand unary-operator operand operand binary-operator expression literal | lhs-expression | ( expression ) function-call id | . . . int-literal | oat-literal | char-literal | string-literal - | ... + | - | * | / | ...

Questa grammatica non ` proprio corretta, perch d` associativit` a destra a tutti gli operatori. e e a a Per i nostri scopi ` suciente capire la forma delle espressioni, le priorit` degli operatori vengono e a stabilite separatamente.
In base alla grammatica data ` in teoria possibile scrivere espressioni prive di senso come ad esempio e !10.5, ciao + 5.4. Il type-checker del compilatore provvede a fare questi controlli e a segnalare eventuali errori di tipo.

3.5

Comandi

Denizione 2 I comandi sono entit` sintattiche che richiedono al computer di eseguire detera minate azioni. Eetto tipico dei comandi ` quello di modicare la memoria. A dierenza delle e espressioni, in C+o- i comandi non restituiscono un valore.

10

STRUTTURA DI UN PROGRAMMA C+O-

statement ::= | | | | | 3.5.1 Assegnamento

assignment io-statement compound-statement selection-statement iteration-statement void-function-call

In un linguaggio imperativo (come C+o-) ` il comando pi` importante, perch modica la mee u e moria. In linguaggi funzionali puri, quali Haskell, non c` memoria e, di conseguenza, non c` e e un comando di assegnamento. assignment ::= lhs-expression = expression ; Esempio 7 x = 0; x = x + 1; Per memorizzare un valore in una certa variabile, occorre che il tipo del valore ed il tipo della variabile coincidano (oppure che siano compatibili, vedi sezione 3.2.6). Lo stesso avviene quando un assegnamento potrebbe implicare una perdita di informazione (ad esempio assegnare un valore reale ad una variabile intera). Alcuni compilatori emettono un avvertimento, ma non tutti. 3.5.2 Comandi di input/output

I comandi di input/output consentono di ricevere input dallutente tramite la tastiera e di comunicare i risultati allutente tramite lo schermo. In C+o- linput e loutput avvengono tramite luso degli stream cin, per linput, e cout, per loutput. Per usare gli stream ` necessario e includere la libreria iostream.h. io-statement ::= cin >> expression ; | cout << expression ; Esempio 8 cout<<"Inserisci un numero\n"; cin>>x; cout<<"Il quadrato di "<<x<<" e "<<x*x; Come si vede dallesempio ` possibile scrivere pi` output in una sola volta. Similmente ` possibile e u e leggere pi` input. u

3.5

Comandi Comando composto

11

3.5.3

Intuitivamente, il comando composto permette di racchiudere una sequenza di comandi da eseguire in successione, nellordine con cui sono specicati. compound-statement ::= { statement-part } Come vedremo pi` avanti, nella sua forma pi` generale il comando composto pu` racchiudere u u o al suo interno anche delle dichiarazioni di variabili. 3.5.4 Comandi condizionali

Servono per esprimere una decisione. Detto in atri termini, consentono di eseguire in modo condizionato alcuni comandi, in base al vericarsi o meno di condizioni. Esempio 9 Supponiamo di dover calcolare il massimo tra due variabili intere a1 e a2 e mettere il risultato in c if (a1 > a2) c = a1; else c = a2; A destra dellif (ramo then) c` il comando da eseguire se la condizione ` soddisfatta, a e e destra dellelse (ramo else) c` il comando da eseguire se la condizione non ` soddisfatta. La e e condizione va sempre racchiusa tra parentesi tonde. selection-statement ::= if ( epression ) statement [ else statment ] Da notare che il ramo else ` opzionale e pu` essere omesso. In questo caso il comando ` e o e equivalente a questa forma estesa: if (e) c else { } C` tuttavia un problema legato proprio a questo fatto, in particolare quando ci sono pi` e u comandi if annidati: Esempio 10 if (n > 0) if (a > b) x = a; else z = b; A quale if fa riferimento lelse? A quello pi` interno. Se si vuole legare lelse allif pi` u u esterno occorre scrivere if (n > 0) { if (a > b) x = a; } else z = b; ` E comunque buona prassi mettere sempre le parentesi grae quando ci sono pi` comandi u condizionali annidati. I comandi condizionali possono essere usati in successione per operare scelte multiple: Esempio 11 Assegnare alla variabile b il valore 1 quando il valore della variabile a ` < 10, 2 e quando ` < 20, 3 in tutti gli altri casi. e if (a < 10) b=1; else if (a < 20) b=2; else b=3;

12 3.5.5 Comandi iterativi

STRUTTURA DI UN PROGRAMMA C+O-

Sono comandi che consentono di ripetere un comando (eventualmente composto). La ripetizione pu` essere denita, quando ` noto a priori il numero di iterazioni, indenita in tutti gli altri casi. o e Questa importante distinzione si riette anche a livello di sintassi: il C+o- prevede dei costrutti diversi per luna o laltra forma di iterazione. iteration-statement ::= while ( expression ) statement | do statement while ( expression ) ; | for ( lhs-expression =expression ; expression ; lhs-expression = expression ) statement While. La semantica informale del comando while (e) c ` la seguente: e

1. valuta lespressione e, se ` falsa termina lesecuzione del ciclo; e 2. esegui il comando (eventualmente composto) c; 3. torna all1. Tipicamente, la forma ben strutturata di un while ` la seguente: e /* inizializza la/le variabili interessate nel ciclo */ /* in particolare nel test */ while (...) { /* comandi del ciclo */ /* modifica il valore di una o piu variabili * interessate nel test */ } Lespressione del while ` detta guardia e viene sempre eseguita almeno una volta. Viceversa, e i comandi del ciclo possono non essere mai eseguiti, se la guardia fallisce subito alla prima iterazione. Esempio 12 Scrivere un programma che prende in input un intero n e stampa i primi n numeri interi pari. main() { int n, r; cout<<"Inserire un numero intero:\n"; cin>>n;

3.5

Comandi r = 0; while (r < n) { cout<<r<<\n; r = r + 2; }

13

} Attenzione ai loop inniti! Una guardia errata pu` fare in modo che il ciclo non termini mai. o Se nellesempio precedente al posto di r < n metto r != n il programma termina se n ` pari e e non termina se n ` dispari. e Esempio 13 Algoritmo standard per il calcolo del massimo comun divisore. Informalmente lalgoritmo ` il seguente: e 1. prendi due numeri, m ed n, poni x = 2 e mcd = 1 (tutti i numeri sono divisibili per 1) 2. se x > m oppure x > n ho nito 3. verica se sia m che n sono divisibili per x 4. s` dividi sia m che n per x e poni mcd = mcd x : 5. no: poni x uguale al numero primo successivo a x stesso 6. vai a 2 In C+o-: main() { int m, n, x, mcd; cin>>m>>n; x = 2; mcd = 1; while (x <= m && x <= n) { if ((m % x == 0) && (n % x == 0)) { m = m / x; n = n / x; mcd = mcd * x; } else { ...x = <prossimo numero primo>(x + 1) } } cout<<mcd; }

14 Do while.

STRUTTURA DI UN PROGRAMMA C+O-

La semantica informale del comando do c while (e); ` la seguente: e

1. esegui il comando (eventualmente composto) c; 2. valuta lespressione e, se ` vera torna all1, altrimenti termina lesecuzione del ciclo. e Il comando do while ` utile quando il corpo del ciclo va sempre eseguito almeno una volta, ad e esempio se serve ad inizializzare la guardia stessa. A tutti gli eetti ` equivalente a c; while e (e) c. For. Viene utilizzato principalmente per le iterazioni denite, quando cio` ` noto a priori il ee numero di iterazioni necessarie. La semantica informale del comando for ( i ; e ; m ) c ` la seguente: e 1. esegui il comando i, usato in genere per inizializzare le variabili coinvolte nel ciclo; 2. valuta lespressione e, se ` falsa allora termina lesecuzione del ciclo; e 3. esegui il corpo del ciclo c e, successivamente, il comando di modica m, che di norma agisce sulle variabili del test; 4. torna al 2. Il comando for pu` essere riscritto in termini del comando while: i; while(e) { c; m; }. o

3.6

Regole di scope

La forma generale di un comando composto (detto anche blocco) ` la seguente: e compound-statement ::= { declaration-part statement-part } In particolare, si possono dichiarare delle variabili locali allinterno di un comando composto. Le variabili locali sono visibili solo allinterno del blocco in cui vengono denite. Questo ` utile e per restringere la visibilit` delle variabili solo allinterno dei frammenti di codice in cui quelle a variabili vengono eettivamente utilizzate. ` E anche possibile ridenire in un blocco una variabile gi` denita in un blocco pi` esterno, a u anche dandole un tipo diverso dal precedente. La seconda denizione crea una nuova variabile con lo stesso nome. A questo punto sorge il problema di individuare a quale variabile un determinato nome fa riferimento. Questo compito spetta al compilatore, ma ` comunque necessario capire il meccanismo e al ne di scrivere programmi corretti. Ci sono due approcci per risolvere il problema: lapproccio bottom-up e lapproccio top-down.

3.6

Regole di scope Approccio bottom-up

15

3.6.1

Nellapproccio bottom-up, si prende il nome di una variabile, diciamo x, nel punto in cui essa viene utilizzata (non dichiarata) e ci si domanda: a quale dichiarazione fa riferimento questo utilizzo? Si individua il blocco pi` interno che contiene lutilizzo in questione, si controlla se u questo blocco ha una parte dichiarativa e, se s` se tra le dichiarazioni c` anche quella di x. In , e questo caso abbiamo gi` trovato la dichiarazione che ci interessa. Se il blocco non ha una parte a dichiarativa, oppure se nella parte dichiarativa non compare la dichiarazione di x, si individua il blocco immediatamente pi` esterno che contiene il blocco in cui ci troviamo ora e si ripete il u procedimento. Esempio 14 main() { int x; ... -1-> x = 0; if (...) { -2-> while (x > 0) { double x; -3-> x = a + 1; } } else { char x; ... } } Le frecce indicano diverse occorrenze del nome x (non della sua dichiarazione). La prima occorrenza ha una dichiarazione (di tipo int) nel medesimo blocco. La seconda occorrenza non ha una dichiarazione nel blocco immediatamente circostante (il ramo then) per cui si risale e quindi anche questa occorrenza ` legata alla x dichiarata nel corpo del main. La terza occorrenza invece e ` legata alla x locale (di tipo double). Da notare, quindi, che il corpo del ciclo agisce su una e variabile che ` del tutto diversa dalla variabile omonima nel test del ciclo, per cui ` possibile che e e il ciclo non termini. 3.6.2 Approccio top-down

Nellapproccio top-down, si parte dalla dichiarazione di una variabile, diciamo x, e ci si domanda: in quali parti del programma il nome x fa riferimento (in gergo: ` legato) a questa dichiarazione e di x? O, in modo equivalente: qual ` lo scope di questa dichiarazione di x? e Denizione 3 Lo scope di una dichiarazione di variabile ` il blocco in cui occorre la dichiarae zione stessa, inclusi tutti i suoi sotto-blocchi diretti ove la variabile non viene dichiarata nuovamente. La denizione si ripete induttivamente per tali sotto-blocchi.

16 Esempio 15 A { int x; A x = 1; B { int x; B x = 2; B } A cout<<x; /* stampa 1 */ A }

FUNZIONI

Segue un esempio pi` complesso per evidenziare la sottigliezza della denizione appena data: u Esempio 16 A { int x; A x = 0; B { int x; B x = 1; C { C x = 2; C } B } A } Il blocco C ` eettivamente un sotto-blocco di A, ma non ` un sotto-blocco diretto. Dunque, e e la x che compare in C ` soggetta alle dichiarazioni del sopra-blocco diretto, cio` B. e e In generale non ` una buona prassi di programmazione ridichiarare con lo stesso nome pi` e u variabili, a meno che queste dichiarazioni non siano distanti sintatticamente. Tipicamente, ` e ammesso (e a volte consigliato) riutilizzare lo stesso nome di variabile solo in funzioni dierenti.

Funzioni

Approssimativamente, una funzione ` un blocco (dichiarazioni + comandi) a cui ` associato un e e nome. Ad esempio, main ` una funzione con nome main. e Useremo come sinonimi per il termine funzione i termini sottoprogramma e procedura. Le funzioni possono avere dei parametri e restituire un risultato. Ad esempio, sqrt, sin ecc. sono funzioni, denite nella libreria standard. Le funzioni sono utili per: evitare ripetizione di codice identico: se lalgoritmo del massimo comun divisore serve in vari punti del programma posso incapsularlo in una funzione e poi richiamarla quando necessario. Le librerie standard in particolare deniscono funzioni utili nella scrittura di numerosi programmi. decomporre il codice in moduli disgiunti: aspetti concettualmente indipendenti del programma possono essere realizzati in funzioni distinte, minimizzando il rischio di interferenze.

4.1

Denizione e chiamata di funzioni

17

Luso di funzioni ha le conseguenze seguenti: la lunghezza complessiva del codice diminuisce; il codice ` pi` leggibile e strutturato; e u si evitano errori: riscrivendo il codice si rischia di sbagliare; la scrittura dei programmi risulta pi` semplice poich le funzioni consentono di astrarre u e dai dettagli; funzioni dierenti possono essere implementate da persone dierenti; il programma ` pi` modicabile grazie alla localizzazione del codice da aggiornare. e u

4.1

Denizione e chiamata di funzioni

A livello di grammatica questi due aspetti delle funzioni si riettono in due diverste categorie sintattiche, function-denition e function-call. function-denition ::= type id ( formal-parameters ) compound-statement La denizione consta di unintestazione (in cui si specica il tipo restituito dalla funzione, il nome della funzione stessa e lelenco dei parametri formali ) ed un corpo. Il tipo restituito va sempre specicato. Si pu` usare void come tipo di ritorno di funzioni che non restituiscono o nessun valore. Dove possono essere denite le funzioni? Non allinterno della declaration-part dei comandi composti, ma solo nella declaration-part del programma intero (in gergo, solo al top-level). Sebbene il C++ non faccia distinzioni sintattiche tra funzioni che restituiscono void e funzioni che restituiscono non-void, in C+o- distingueremo questi due casi a livello di chiamata: function-call ::= id ( [ actual-parameters ] ) void-function-call ::= function-call ; Questa distinzione serve sostanzialmente a classicare una function-call come unespressione (e per questo motivo ` ammessa come operando nella denizione di espressione), ed una voide function-call come un comando (non restituisce alcun risultato). Esempio 17 Scrivere una funzione che scambia il contenuto delle due variabili globali x e y. int x; int y; void swap() { int tmp; tmp = x;

18 x = y; y = tmp; } main() { x = 1; y = 2; swap(); cout<<x<<" "<<y<<"\n"; /* stampa 2 e 1 */ }

FUNZIONI

4.2

Funzioni con parametri

Le funzioni senza parametri sono in genere poco essibili, perch comunicano con il mondo e esterno esclusivamente per mezzo di variabili globali. Viceversa, attraverso i parametri possiamo comunicare a una funzione i dati su cui deve operare. Esempio 18 Scrivere una funzione che prende un parametro di tipo intero e pone la variabile globale b a 1 se il parametro ` un numero primo, 0 altrimenti. e int b; void primo(int x) { int y; y = 2; while ((y < x) && ((x % y) != 0)) y = y + 1; if (y == x) b = 1; else b = 0; } Vediamo pi` in dettaglio la sintassi dei parametri formali: u formal-parameters ::= void | type id { , type id } I parametri formali sono identicatori a cui ` associato un tipo. e Al momento dellinvocazione di una funzione con parametri occorre elencare i valori con cui saranno istanziati i parametri formali. Questi valori sono detti parametri attuali. actual-parameters ::= [ expression { , expression } ] Ovviamente ci sono dei vincoli da rispettare anch la chiamata ad una funzione sia corretta: e

4.3

Funzioni che ritornano un valore il numero dei parametri attuali deve coincidere con quello dei parametri formali2 ;

19

il tipo del parametro attuale deve essere uguale al tipo del corrispondente parametro formale (ma vale comunque il discorso del cast, sezione 3.2.6). Per quanto riguarda lo scope dei parametri formali, essi sono considerati come facenti parte del blocco che denisce il corpo della funzione. Sono quindi soggetti alle stesse regole di visibilit` a delle variabili locali. Esempio 19 int x; int y; void fun(int x) { ... x = y + x; /* x parametro formale, y variabile globale */ ... }

4.3

Funzioni che ritornano un valore

Il tipo del valore ritornato da una funzione va specicato nellintestazione della funzione stessa. Si usa a questo scopo il comando return, che ha il duplice scopo di: terminare lesecuzione della funzione nel punto esatto in cui compare; specicare il valore di ritorno della funzione. In C+o- il comando return ` ammesso esclusivamente nelle funzioni non-void. Inoltre non ` e e ammesso il suo uso allinterno di comandi iterativi. statement ::= . . . | return expression ; Il tipo dellespressione usato nella return deve coincidere col tipo di ritorno dichiarato nellintestazione della funzione. Esempio 20 Scrivere una funzione che calcola il valore assoluto di un numero intero: int assoluto(int x) { int ris; if (x >= 0) ris = x;
Ci` non ` sempre vero in C++, perch alcune funzioni, ad esempio printf, prendono un numero variabile di o e e argomenti.
2

20 else return ris; } ris = -x;

FUNZIONI

4.4

Prototipi

` E possibile dichiarare una funzione senza denirla: in tal caso la dichiarazione prende il nome di prototipo di funzione e serve unicamente al compilatore per informarlo del fatto che la funzione esiste, viene denita da qualche parte, e soprattutto per indicare il tipo dei parametri ed il tipo restituito. I prototipi servono perch in C+o- non ` possibile invocare una funzione che non sia stata e e dichiarata in precedenza, quindi luso dei prototipi ` necessario per denire funzioni mutuamente e ricorsive (due funzioni sono mutuamente ricorsive se si invocano a vicenda): Esempio 21 /* prototipo di min, il nome dei parametri puo * essere omesso */ int min(int, int); void f(int n) { ... a = min(3, 4); ... } int min(int a, int b) { if (a < b) { f(a-2); return a; } else return b; } La presenza del prototipo della funzione min ` necessaria per poter eettuare linvocazione e dentro f.

4.5

Funzioni ricorsive

Denizione 4 Una funzione ` detta ricorsiva quando ` denita in termini di se stessa. e e

4.5

Funzioni ricorsive

21

Da un punto di vista pi` pragmatico, una funzione ` ricorsiva se nel suo corpo c` una u e e chiamata alla funzione stessa. In matematica le pi` note funzioni ricorsive sono quelle del fattoriale e di Fibonacci: u fattoriale(x) = bonacci(x) = 1, se x = 0 x fattoriale(x 1), altrimenti 1, se x = 0 oppure x = 1 bonacci(x 1) + bonacci(x 2), se x > 1

In C+o- ` possibile scrivere funzioni ricorsive perch lo scope del nome di una funzione comprende e e il corpo della funzione stessa. int fattoriale(int x) { if (x == 0) return 1; else return x * fattoriale(x - 1); } int fibonacci(int x) { if (x == 0 || x == 1) return 1; else return fibonacci(x - 1) + fibonacci(x - 2); } La ricorsione ` importante perch permette di scrivere in modo compatto delle funzioni che e e altrimenti sarebbero molto pi` complicate. Inoltre, ci sono delle funzioni che a questo punto del u corso non si possono scrivere se non in modo ricorsivo. Esempio 22 Scrivere una funzione che legge da tastiera una sequenza di numeri (terminata dal numero 0) e li stampa in ordine inverso. void revert() { int d; cin>>d; if (d != 0) revert(); cout<<d<<"\n"; } Comunque, per tutti gli esempi visti esistono dei programmi equivalenti iterativi, ma in genere sono pi complicati. u

22

STRUTTURE DATI

4.6

Overloading

Solitamente conviene dare nomi diversi a funzioni diverse, ma quando una funzione esegue lo stesso compito di unaltra su un dato di tipo diverso, ` conveniente usare lo stesso nome. e void print(int n); void print(double d); //stampa un int //stampa un double

Il compilatore riconosce la funzione da invocare in base al tipo dei parametri. Nel caso non venga individuata una denizione adatta (ad esempio, se nessuna denizione accetta il tipo del parametro attuale) il compilatore segnala un errore. Va notato che il compilatore al ne di determinare la funzione da invocare non guarda n i e nomi dei parametri, n il tipo di ritorno. e

5
5.1

Strutture dati
Array
Denizione e utilizzo

5.1.1

Denizione 5 Un array ` una struttura in cui: e sono memorizzati un numero nito di valori dello stesso tipo (omogenei) ` possibile selezionare (lettura/scrittura) in modo uniforme questi valori attraverso un e opportuno indice. Estendiamo quindi la categoria sintattica delle dichiarazioni, ammettendo la dichiarazione di array: variable-declaration ::= [const] type var-specier { , var-specier } ; var-specier ::= id { [ [ int-const-no-sign ] ] } Alcuni esempi: int vettore[30], a, b; double vect[500][20]; char stringa[80]; Da notare che non ` possibile denire array a lunghezza variabile, per esempio scrivendo int e a[i];, a meno che i non sia stato denito come una costante. Lunica operazione ammessa per gli array ` la selezione per accedere agli elementi. In particolare, non ` possibile assegnare un e e array ad un altro con loperatore = n confrontare due array con loperatore ==. In entrambi i casi e occorre lavorare esplicitamente sui singoli elementi. Un elemento di un array ` una lhs-expression. e lhs-expression ::= id | lhs-expression { [ int-const-no-sign ] }+ Assumendo le dichiarazioni di prima:

5.1

Array

23

vettore[0] = 0; vettore[1] = vettore[0]; vect[0][0] = 10.0; Gli indici validi per laccesso ad un array vanno da 0 alla dimensione dellarray meno 1. A run-time non viene mai eettuato alcun controllo sulla validit` degli indici (perch di fatto la a e dimensione dellarray va persa al momento della compilazione e non fa parte del tipo dellarray). Vedremo pi` avanti come i vettori consentano di superare questa ed altre limitazioni degli array. u 5.1.2 Array e funzioni

` E possibile passare un array come parametro di una funzione, ma occorre tenere presente di alcune particolarit`. Innanzi tutto, come ` stato detto prima, la lunghezza di un array non fa a e parte del suo tipo (e quindi non compare nel prototipo della funzione). Quindi nel momento in cui si passa un array come parametro di una funzione occorre passare un ulteriore parametro che ne indichi la lunghezza: int f(char v[], int length) { ... } Larray passato come parametro deve avere lo stesso tipo dellarray dichiarato nellintestazione della funzione. In particolare non sono ammessi cast (cfr. sezione 3.2.6). Esempio 23 Scrivere un programma che genera una stringa casuale e verica se ` palindroma. e int palindroma(char s[], unsigned int length) { unsigned int i; bool b; b = true; i = 0; while (b && (i < (length / 2))) { b = (s[i] == s[length - i - 1]); i = i + 1; } return b; } int main() { unsigned int i; bool res; const unsigned int length=30; char a[length];

24

STRUTTURE DATI

for (i = 0; i < length; i = i + 1) { a[i] = a + rand() % (z - a + 1); cout<<a[i]; } res = palindroma(a,length); if (res) cout<<" e palindroma!\n"; else cout<<" non e palindroma!\n"; return res; } Laltra particolarit` importante degli array ` che vengono passati per riferimento (o per variaa e bile), mentre tutti gli altri parametri in C+o- vengono passati per valore. Allatto pratico, la distinzione tra i due tipi di passaggio ` la seguente: e passaggio per valore: uneventuale modica di un parametro passato per valore non si riette allesterno della funzione. Allinterno della funzione si lavora con una copia del parametro. passaggio per riferimento/variabile: uneventuale modica di un parametro passato per riferimento/variabile si riette allesterno della funzione. Il fatto che gli array vengano passati per riferimento pu` essere utile anche per sopperire al fatto o che non ` possibile scrivere una funzione che ritorni un array. e Esempio 24 void prova_valore(int x) { x = 1; } void prova_riferimento(int x[]) { x[0] = 1; } void stampa(int y) { cout<<"y vale "<<y<<"\n"; } int main() { int y[1]; y[0] = 0;

5.2

Strutture stampa(y[0]); /* --> 0 */ prova_valore(y[0]); stampa(y[0]); /* --> 0 */ prova_riferimento(y); stampa(y[0]); /* --> 1 */

25

} 5.1.3 Stringhe

La trattazione delle stringhe ` stata rimandata no ad ora in quanto in C+o- non esiste un tipo e nativo stringa. Le stringhe vengono implementate come array di caratteri. Tuttavia, per le stringhe diviene particolarmente fastidioso il fatto che non sia possibile calcolare a runtime la lunghezza di un array, per questo motivo in C+o- esiste la convenzione che una stringa deve essere terminata dal carattere \0 (il carattere nullo). Vedremo pi` avanti come un altro tipo u di stringhe, denite come oggetti, non abbiano questi problemi. Esempio 25 Scrivere una funzione che calcola la lunghezza eettiva di una stringa. int string_length(char s[]) { int i; i = 0; while (s[i] != \0) i = i + 1; return i; } La libreria standard cstring.h comprende numerose funzioni per la gestione e la manipolazione delle stringhe (vedi [Stro]).

5.2
5.2.1

Strutture
Denizione e uso

Denizione 6 Una struttura ` (appunto) una struttura in cui: e sono memorizzati un numero nito di valori di tipo non necessariamente uguale (eterogenei) ` possibile selezionare (lettura/scrittura) per mezzo di unetichetta questi valori. e Alcuni esempi di informazioni adatte ad essere memorizzate in una struttura sono: data (giorno della settimana, giorno del mese, mese, anno), studente (nome, cognome, matricola, et`, media a dei voti), ecc. In C+o- si possono denire come segue:

26 struct char int int int }; data { giorno_della_settimana[3]; giorno; mese; anno;

STRUTTURE DATI

struct studente { char nome[15]; char cognome[20]; int matricola; int eta; double media; }; I componenti della struttura sono detti campi. Una volta denito un tipo strutturato, ` possibile e dichiarare variabili che hanno quel tipo: struct data oggi, data_di_nascita; struct studente corso_programmazione[300]; declaration-part ::= | | type-declaration ::= struct-specier ::= eld ::= type ::= | type-declaration variable-declaration prototype-declaration { struct-specier ; } struct id { eld ; { eld ; } } type specier simple-type struct id

La principale operazione sulle strutture ` la selezione di uno dei campi: e lhs-expression ::= . . . | lhs-expression . id Esempio 26 { struct data oggi; oggi.giorno = ieri.giorno + 1; oggi.mese = 12; oggi.anno = 2010; } A dierenza degli array, ` possibile assegnare una struttura ad unaltra, a patto che siano dello e stesso tipo. Tutti i campi vengono copiati esattamente come sono. Se la struttura contiene un array, allora anche larray viene copiato. Non ` comunque possibile confrontare due strutture per mezzo delloperatore ==. e

5.2

Strutture Strutture e funzioni

27

5.2.2

Le strutture posso essere passate come argomento di funzioni (come gli array) ma anche restituite come valore. Esempio 27 struct point { double x; double y; }; struct point make_point(int a, int b) { struct point t; t.x = a; t.y = b; return t; } int main() { struct point a; a = make_point(10, 10); } Per quanto riguarda il passaggio di strutture come parametri, il passaggio avviene per valore (a dierenza degli array). Per esempio: void incr_point(struct point b) { b.x = b.x + 1; b.y = b.y + 1; } Invocando incr_point(a); dal main non si ottiene leetto desiderato. Invece, una copia della struttura viene passata alla funzione e gli incrementi hanno eetto solo su quella copia. Esempio 28 Implementare il tipo di dato pila (LIFO) per mezzo di strutture e array. Limplementazione deve fornire le seguenti operazioni: creazione di una pila vuota, test per pila vuota, push e pop (che stampa solo) di elementi. Per semplicit` si assuma che la pila contenga valori a interi. const int length = 1000; struct stack {

28 int data[length]; int top; }; struct stack stack_new() { struct stack tmp; tmp.top = length; return tmp; } bool stack_is_empty(struct stack s) { return s.top == length; } struct stack stack_push(struct stack s, int e) { if (s.top == 0) { cout<<"error, stack full\n"; } else { s.top = s.top - 1; s.data[s.top] = e; } return s; } struct stack stack_pop(struct stack s) { if (s.top == length) { cout<<"error, empty stack\n"; } else { cout<<"pop"<<s.data[s.top]<<"\n"; s.top = s.top + 1; } return s; }

STRUTTURE DATI

Esercizio 1 Implementare in modo analogo il tipo di dato coda (FIFO). Limplementazione deve fornire le seguenti operazioni: creazione di una coda vuota, test per coda vuota, enqueue, dequeue. Per semplicit` si assuma che la pila contenga valori interi. a

5.3

Puntatori

29

5.3

Puntatori

Tutti i tipi di dato studiati no ad ora hanno una dimensione che ` nota staticamente e non e pu` cambiare a tempo di esecuzione. Ci sono casi in cui sono necessari dati la cui dimensione o non ` nota a priori e pu` cambiare a tempo di esecuzione. Strutture dati di questo tipo sono e o dette dinamiche. Pu` essere utile realizzare come strutture dati dinamiche sequenze, pile, code, o alberi, ecc. I puntatori forniscono un modo essibile per gestire strutture dinamiche (e non solo). Lidea di base ` che queste strutture sono composte da unit` omogenee correlate tra loro in qualche e a modo. Per esempio, in una pila ogni elemento ` correlato a quello che giace immediatamente e sotto di esso. In un albero gli elementi sono correlati secondo la relazione padre-glio. Queste correlazioni possono essere visualizzate gracamente per mezzo di archi che collegano i diversi elementi (detti nodi). Un puntatore non ` altro che un particolare tipo di dato che permette di memorizzare e lindirizzo di memoria di un elemento, e quindi fornisce un modo per collegare elementi correlati. Inoltre, un puntatore non punta ad un indirizzo di memoria qualsiasi, ma solo allindirizzo di un oggetto di un determinato tipo. 5.3.1 Dichiarazione di una variabile di tipo puntatore

Per indicare che un identicatore ` un puntatore a un elemento di un certo tipo si pregge lo e stesso identicatore con il simbolo *. Esempi di dichiarazione sono: int *p; /* p e un puntatore a intero */ struct item { int val; struct item *next; /* next e un puntatore a una struct item */ }; double *f; /* puntatore a double */

int *a[30]; /* array di 30 puntatori a interi */ Estendiamo in modo corrispondente la sintassi dei var-specier. var-specier ::= id { [ [ int-const-no-sign ] ] } | * var-specier Al momento della dichiarazione di una variabile locale di tipo puntatore il valore della variabile ` indenito (e, in particolare, non esiste un oggetto valido puntato da quel puntatore). e 5.3.2 Operazioni sui puntatori

La costante 0 ` lunica che pu` essere assegnata a un puntatore (di tipo qualsiasi). Tipicamente e o ` viene denita un costante simbolica con lo stesso valore chiamata NULL. E preferibile usare que-

30

STRUTTURE DATI

stultima perch tende a rendere il programma pi` leggibile (occorre includere il le iostream.h e u o cstdio.h). Questa costante speciale signica puntatore non valido, e viene tipicamente utilizzata per terminare liste, alberi e altre strutture simili. Esempio 29 int *p; double *q, r; p = NULL; q = NULL; if (p == NULL) { ... } if (p == q) { ... } /* errore! anche se sia p che q * valgono NULL, hanno tipi diversi */ r = p; /* errore! r non e un puntatore */ Ci sono due operatori fondamentali che lavorano con i puntatori: expression ::= | lhs-expression ::= | ... & lhs-expression ... * expression

Loperatore & ` detto indirizzo-di e serve per ottenere lindirizzo di una lhs-expression, cio` e e di una variabile, una struttura, un campo di una struttura, un elemento di un array, ecc.). Esempio 30 int a; int *pa; a = 15; pa = &a; Dopo le operazioni qui sopra, pa contiene lindirizzo della variabile a. Loperatore *, che in un qualche modo ` linverso di &, permette di de-referenziare e un puntatore, cio` di accedere alloggetto puntato. Nellesempio che segue (continuazione del e frammento precedente) si accede alla variabile a indirettamente per mezzo del puntatore che punta ad essa: Esempio 31 *pa = 20; cout<<a<<"\n"; /* stampa 20!!! */ ` E importante notare che lapplicazione di * produce a sua volta una lhs-expression (infatti possiamo usarlo a sinistra dellassegnamento).

5.3

Puntatori Allocazione dinamica della memoria

31

5.3.3

Loperatore new permette di allocare (sinonimo di riservare) dinamicamente unarea di memoria per memorizzare un valore di tipo specicato. struct item *p; p = new struct item; Una volta allocata la memoria, ` possibile usare loperatore di de-referenziazione * per lege gerne/scriverne il contenuto. Importante: la memoria appena allocata non ` inizializzata, quindi e occorre inizializzarla prima di usarla. Esempio 32 Invertire una sequenza di interi senza usare una funzione ricorsiva. struct item { int val; struct item *next; }; int main() { struct item *p; int d; p = NULL; do { struct item *q; cin>>d; if (d != 0) { q = new struct item; /* di seguito occorrono le parentesi * perche il . ha precedenza sul * */ (*q).val = d; (*q).next = p; p = q; } } while (d != 0); while (p != NULL) { cout<<(*p).val<<"\n"; p = (*p).next; } }

32

STRUTTURE DATI

Tip: loperatore * usato congiuntamente con la selezione di un campo in una struttura ` mole to frequente, per questo c` unabbreviazione, ovvero anzich scrivere (*q).val = ... si pu` e e o scrivere q->val = .... Oltre alloperatore new esiste anche loperatore inverso delete, che rilascia unarea di memoria precedentemente allocata. Se si volesse rilasciare la memoria allocata nellesempio precedente mentre si stampa la lista si potrebbe fare cos` : while (p != NULL) { cout<<(*p).val<<"\n"; delete p; p = (*p).next; } In questa soluzione c` per` un problema: si utilizza larea puntata da p anche dopo averla e o rilasciata! La soluzione corretta ` la seguente: e while (p != NULL) { struct item* next; cout<<(*p).val<<"\n"; next = (*p).next; delete p; p = next; } 5.3.4 Puntatori e passaggio dei parametri

I puntatori possono essere ecacemente utilizzati in congiunzione con il passaggio dei parametri per simulare il passaggio per riferimento (o, pi` precisamente, per indirizzo). Che cosa sucu cede se un argomento di una funzione ` di tipo puntatore? Il C+o- passa quellargomento per e valore, ma ci` che viene passato ` ovviamente un puntatore, che pu` essere usato per modicare o e o lelemento puntato. Esempio 33 Siamo in grado, a questo punto, di scrivere una funzione che scambia il valore di due variabili (intere) senza usare array o variabili globali: void swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; } Esercizio 2 Limplementazione seguente di swap viene compilata correttamente, ma non funziona bene come laltra, perch? e

5.4

Riferimenti

33

void swap(int *a, int *b) { int *tmp; tmp = a; a = b; b = tmp; } Esercizio 3 Dire cosa viene stampato dal seguente programma, supponendo che limplementazione di A sia A1 in un caso e A2 nellaltro. void A1(int *q) { q = new int; *q = *q + 3; } void A2(int *q) { *q = *q + 4; } main() { int *p; p = new int; *p = 0; A(p); cout<<*p; }

5.4

Riferimenti

Un riferimento ` un nome alternativo per una variabile. Vediamo un esempio: e int i = 1; int& r = i; // ora r e i si riferiscono allo stesso int int x = r; // x vale ora 1 r = 2; // i ora vale 2 Qualunque operazione applicata ad un riferimento ` in realt` applicata alloggetto riferito. e a Un riferimento pu` essere restituito da una funzione o passato come argomento. Utilizzi dei o riferimenti saranno visti pi` avanti. u

34

PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

6
6.1

Programmazione orientata agli oggetti


Classi

Una classe ` un tipo denito dallutente. Si denisce una classe per rappresentare un concetto e che non ha una controparte in un tipo predenito. La denizione di un nuovo tipo serve per separare i dettagli implementativi (ad esempio, come sono memorizzati i dati) dalle informazioni necessarie per un corretto uso del tipo stesso (ad esempio, le funzioni denite sul tipo). 6.1.1 Metodi

Immaginiamo di voler denire delle funzioni che usano la struttura struct data denita nella sezione 5.2. struct char int int int }; void void void void data { giorno_della_settimana[3]; giorno; mese; anno;

init_data(struct data* d,int gg, int mm, int aa, char *gds); add_giorno(struct data* d, int n); add_mese(struct data* d, int n); add_anno(struct data* d, int n);

In questa implementazione non c` nessuna connessione esplicita tra il tipo di dati e le e funzioni che lo usano. Una connessione esplicita pu` essere creata dichiarando le funzioni dentro o la classe (una struttura ` sostanzialmente una classe): e struct char int int int void void void void }; Queste funzioni vengono chiamate metodi della classe e devono essere invocate su una specica variabile di tipo appropriato, usando la sintassi standard di accesso a elementi della classe: data { giorno_della_settimana[3]; giorno; mese; anno; init_data(int gg, int mm, int aa, char *gds); add_giorno(int n); add_mese(int n); add_anno(int n);

6.1

Classi

35

struct data dd; dd.init_data(5,11,2002,"Me"); dd.add_giorno(3); ... Allinterno di queste funzioni i nomi dei campi della classe (detti anche attributi) possono essere usati senza riferimento esplicito alla classe stessa. In questa situazione il campo riferito ` e quello delloggetto sul quale la funzione ` stata invocata (si chiama oggetto un dato il cui tipo e ` una classe). e In C+o- i metodi delle classi vengono deniti al di fuori delle classi stesse. Poich metodi di e classi diverse possono avere lo stesso nome, quando si denisce un metodo ` necessario specicare e il nome della classe: void data::add_giorno(int n) { giorno=giorno+n; } 6.1.2 Incapsulamento

Come si ` detto, lobiettivo delle classi ` quello di separare i dettagli implementativi dalle e e informazioni utili per lutilizzo delloggetto. La denizione basata sulle struct non raggiunge ` lobiettivo. E necessario usare una denizione di classe: class data { char giorno_della_settimana[3]; int giorno; int mese; int anno; public: void init_data(int gg, int mm, int aa, char *gds); void add_giorno(int n); void add_mese(int n); void add_anno(int n); }; I nomi dichiarati prima della keyword public costituiscono i dettagli implementativi, e possono essere usati solo dalle funzioni membro. I nomi dichiarati dopo la keyword public costituiscono linterfaccia della classe, e possono essere usati liberamente. Il vantaggio di questa separazione ` che per capire il funzionamento di una classe ` suciente guardare le funzioni e e membro. Inoltre, se la rappresentazione interna di una classe viene modicata, ` suciente e modicare le funzioni membro.

36 6.1.3 Costruttori

PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

Per garantire che unoggetto venga sempre inizializzato ` possibile denire un costruttore. Un e costruttore ` una funzione speciale il cui obiettivo ` costruire un oggetto di una classe. Si e e riconosce perch ha lo stesso nome della classe, e non ha tipo di ritorno. e class data { char giorno_della_settimana[3]; int giorno; int mese; int anno; public: data(int gg, int mm, int aa, char *gds); void add_giorno(int n); void add_mese(int n); void add_anno(int n); }; Quando una classe denisce un costruttore, tutti gli oggetti di quella classe devono essere inizializzati invocando il costruttore stesso. data dd=data(5,11,2002,"Me"); dd.add_giorno(3); ... Pi` costruttori per la stessa classe possono essere deniti usando il meccanismo delloverloau ding. Gli oggetti possono essere inizializzati anche mediante copia: sugli oggetti ` possibile utilize zare loperatore di assegnamento. La copia di un oggetto avviene copiandone tutti gli attributi. 6.1.4 Attributi di tipo classe

Un attributo di una classe pu` essere a sua volta un oggetto. Ad esempio, nella classe data, o lattributo giorno pu essere di tipo giorno_del_mese. In questo caso possibile specicare o e nel costruttore di data quale costruttore invocare per giorno (se esiste un costruttore senza parametri e non viene specicato altro, questo viene invocato automaticamente). class data { char giorno_della_settimana[3]; giorno_del_mese giorno; int mese; int anno; public: data(int gg, int mm, int aa, char *gds) void add_giorno(int n);

6.1

Classi void add_mese(int n); void add_anno(int n);

37

}; data::data(int gg, int mm, int aa, char* gds): giorno(gg) { ... } Linizializzazione avviene nella denizione del costruttore, preceduta da :. Inizializzatori multipli sono separati da virgole. 6.1.5 La keyword this

Allinterno di un metodo ` possibile riferirsi alloggetto sul quale il metodo ` stato invocato e e usando la keyword this, che ` un puntatore alloggetto in questione. e data& data::add_giorno(int n) { giorno=giorno+n; return *this; } La funzione data::add_giorno ora restituisce un riferimento alloggetto sul quale ` stata e invocata. In questo modo possiamo concatenare diversi aggiornamenti: data dd=data(5,11,2002,"Me"); dd.add_giorno(3).add_giorno(4); ... Il valore di dd viene incrementato di 7 giorni. 6.1.6 Operatori overloaded

` E possibile denire funzioni che consentano luso di oggetti con le notazioni standard dei tipi predeniti. Ad esempio possiamo denire loperatore di somma tra una data e un intero per avere una notazione pi` conveniente per add_giorno. u data& operator+(data a, int n) { return a.add_giorno(n); } Ora possiamo scrivere ad esempio: data dd2=dd+3;

38 Altri possibili operatori sono: bool bool bool bool

PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

operator==(data a, data b); operator!=(data a, data b); operator<(data a, data b); operator>(data a, data b);

data operator+(data d, int n); data operator-(data d, int n); ostream& operator<<(ostream& os, data d); istream& operator>>(istream& is, data& d); Questi operatori non fanno parte della classe data, ma semplicemente ne usano linterfaccia. Esercizio 4 Denire una classe che rappresenta un numero complesso con operatori ==, !=, +, -.

6.2

Ereditariet` a

Come abbiamo visto, le classi servono per rappresentare concetti. Tuttavia spesso concetti diversi sono correlati. Con gli strumenti visti nora ` possibile mettere in relazione due classi e A e B ad esempio dicendo che un campo di A ha tipo B. Una relazione particolarmente importante ` quella di sottoinsieme. Ad esempio, possiamo e avere una classe Impiegato e una classe Manager. Ogni Manager ` anche un Impiegato, mentre e non ` vero il contrario. Se noi implementiamo la classe Manager come classe con un campo e Impiegato questa informazione viene persa. Ad esempio non ` possibile passare un puntatore a e Manager a una funzione che si aspetta un puntatore a Impiegato. Per risolvere questo problema vengono introdotte le classi derivate. 6.2.1 Classi derivate

Una classe Impiegato pu` essere denita come segue: o class Impiegato { char* nome; int cdc; int eta; ... public: Impiegato(char* n, int c, int e) { nome=n; cdc=c;

6.2

Ereditariet` a eta=e;

39

} void presentati() { cout<<"Sono "<<nome<<", limpiegato numero "<<cdc<<" e ho "<<eta<<" anni\n"; } ... }; Deniamo ora una classe Manager usando il meccanismo dellereditariet`. a class Manager : public Impiegato { int livello; public: Manager(char* n, int c, int e, int l); void presentati() { Impiegato::presentati(); cout<<"Sono di livello "<<livello<<"\n"; } };

Si dice che Manager ` una classe derivata da Impiegato. Viceversa, Impiegato ` una classe e e base per Manager. Una classe derivata pu` accedere a tutti gli attributi public della sua classe base come se o fossero deniti nella classe stessa. Nellesempio lo specicatore Impiegato:: ` necessario solo e per evitare ambiguit`, visto che Manager ridenisce il metodo in questione. Una classe derivata a ` invece non pu` accedere a metodi privati della sua classe base. E possibile dichiarare metodi o e campi della classe base protected per renderli accessibili alle sue classi derivate (ma non a classi generiche): class Impiegato { char* nome; protected: int cdc; int eta; ...

40 public: ... }; 6.2.2 Costruttori

PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

Alcune classi derivate, come la classe Manager, necessitano di costruttori. In questo caso, il costruttore della classe base deve essere richiamato dal costruttore della classe derivata. Da questo punto di vista la classe base si comporta come un attributo della classe derivata: Manager::Manager(char* n, int c, int e, int l): Impiegato(n,c,e) { livello=l; }

6.3
6.3.1

Template
Funzioni template

Spesso ` necessario eseguire la stessa operazione su dati di tipo diverso. La scelta di scrivere una e funzione per ogni tipo causa replicazione inutile di codice. Per ovviare a questo problema sono state create le funzioni template. Una funzione template ` una funzione parametrica rispetto a e un tipo. Vediamo ad esempio come denire una funzione generica per eseguire la somma: template <class T> T add(T a, T b) { return a+b; } La dicitura template <class T> che precede la denizione dichiara che la funzione ` pae rametrica rispetto a un tipo T. Una volta introdotto il tipo T, esso pu` essere usato come un o comune tipo. Una funzione di questo genere pu` essere applicata a qualunque tipo: o cout<<add<int>(5,6)<<"\n"; cout<<add<double>(5.5,6.6)<<"\n"; Naturalmente il tipo scelto deve supportare loperatore +, altrimenti la compilazione di a+b dar` errore. a 6.3.2 Classi template

Anche le classi possono essere denite come template: qui sotto deniamo una classe calc che pu` essere usata per eseguire operazioni aritmetiche su dati di qualunque tipo. o

6.4

Classi della libreria

41

template <class T> class calc { T a,b; public: calc(T aa,T bb) { a=aa; b=bb; } T add() { return a+b; } T sub() { return a-b; } void set(T aa, T bb) { a=aa; b=bb; } }; Possiamo ora costruire due diversi oggetti calc, uno per eseguire operazioni su interi e uno su double. calc<int> c1(5,6.6); cout<<c1.add()<<" "<<c1.sub()<<"\n"; calc<double> c2(5,6.6); cout<<c2.add()<<c2.sub()<<"\n"; Da notare che anche se i due oggetti c1 e c2 vengono usati nello stesso modo i risultati sono diversi: nel primo caso il valore decimale viene troncato mediante un cast, mentre nel secondo caso i calcoli sono eettuati usando dei double.

6.4

Classi della libreria

La libreria del C+o- denisce diverse classi e template di utilit` generale. Vediamo ora come a usare alcune di loro.

42 6.4.1 La classe string

PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

La classe string ` una rappresentazione pi` ranata per le stringhe, basata sui concetti visti e u nora. Le denizioni relative alle stringhe, e numerose funzioni per manipolarle, sono denite nella libreria standard string.h (vedi [Stro]). Vediamo un esempio: string s; cin>>s; if(s.length()>4) cout<<s.substr(2,3); else cout<<s+" troppo corta"; Come si vede dallesempio, gli operatori standard possono essere usati su oggetti di tipo string. Inoltre string denisce metodi quali length per calcolare la lunghezza di una stringa e substr per estrarne una sottostringa (il primo parametro indica il carattere iniziale, il secondo il numero di caratteri da estrarre). ` In questo caso s ` inizializzata dal costruttore di default come stringa vuota. E anche e possibile inizializzarla con un letterale di tipo stringa. 6.4.2 Il template vector

Cos come string ` una rappresentazione pi` ranata per le stringhe, il template vector ` una e u e rappresentazione pi` ranata per gli array. u Vediamo un esempio: vector<int> v(5); int i; for(i=0;i<5;i++) cin>>v[i]; for(i=1;i<5;i++) v[i]=v[i]+v[i-1]; for(i=0;i<5;i++) cout<<v[i]<<"\n"; Un vector ` inizializzato specicando il tipo dei suoi elementi e la dimensione massima. Sui e vector ` denito loperatore di assegnamento e di selezione di un elemento (senza controllo che e lindice abbia un valore legale).

RIFERIMENTI BIBLIOGRAFICI

43

Riferimenti bibliograci
[Stro] Stroustrup, Bjarne, The C++ programming language (3rd edition), Addison Wesley, Reading, Massachussets, USA, 1997.

Potrebbero piacerti anche