Dispensa di Informatica III ITC - Learning Live

annuncio pubblicitario
Graziano Donati
Corso di Informatica
Per il IIIo anno degli Istituti Tecnici Commerciali
Centro Studi Bellini
Graziano Donati
Corso di Informatica
Per il IIIo anno degli Istituti Tecnici Commerciali
Centro Studi Bellini
Programma di Informatica - IIIo - Istituto Tecnico Commerciale
GLI ALGORITMI. Il concetto di algoritmo. Proprietà fondamentali di un algoritmo. Rappresentazione degli algoritmi. Linguaggio di progetto. Diagrammi di flusso. Elaborazione. Decisione. Entrata e uscita dati. Cicli iterativi.
INTRODUZIONE ALLA PROGRAMMAZIONE. Il linguaggio C++. Il concetto di assegnamento. Struttura generale di un programma C++. Direttive al preprocessore. Funzione
void. Le dichiarazioni. Il corpo del programma. Le librerie principali.
ISTRUZIONI DI INGRESSO E USCITA. Le istruzioni cin e cout. Inserimento di dati. Visualizzazione dei risultati. Esempi.
ISTRUZIONE CONDIZIONALE If-else. La decisione e l'istruzione condizionale. Condizioni annidate. Scelta multipla. Esempi.
ISTRUZIONI DI CICLO. Le istruzioni for, while, do. Rappresentazione delle istruzioni cicliche in un algoritmo. Esempi di utlizzo.
LE FUNZIONI. Utilizzo delle funzioni nella programmazione strutturata. Variabili locali e
globali. Passaggio dei parametri. Chiamata di una funzione.
VETTORI ED ARRAY. Dichiarazione di vettori e di array. Utilizzo dei vettori e degli array
nella programmazione. Esempi.
Prefazione
Scopo di questa dispensa è quello di esaminare le nozioni fondamentali riguardanti la scrittura di
programmi senza esaminare in dettaglio alcun linguaggio di programmazione.
Una scelta precisa viene invece effettuata a livello di modello di programmazione, a vantaggio
del paradigma imperativo, senza che ciò impedisca un breve esame anche degli altri paradigmi.
Il linguaggio cui si fa riferimento, pur senza dettagliarne i costrutti e le istruzioni, è il c++;
per questo anche il linguaggio di progetto per la descrizione degli algoritmi, proposto nel prossimo capitolo, si ispira alla sintassi del linguaggio c++.
La dispensa inizia con la teoria degli algoritmi, allargando il discorso all’attività di analisi da
svolgere come primo passo del ciclo di vita dei programmi; si passa poi alle generalità sui paradigmi di programmazione, alla suddivisione in livelli dei linguaggi, alle modalità per la loro
traduzione; successivamente si illustrano le più importanti strutture dati astratte e si conclude con
un breve esame sulla documentazione e produzione del software.
Graziano Donati
Centro Studi Bellini
Cap 1
GLI ALGORITMI
Gli algoritmi
Il termine algoritmo è ispirato dal nome del matematico e astronomo arabo del VII° secolo Abu Ja Mohammed
Ibn Musa Al-Khowarizmi che, oltre ad essere l’inventore dello zero e dell’algebra, ha la paternità di quello
che è considerato uno dei primi algoritmi della storia: il metodo per sommare due numeri incolonnandoli
e procedendo cifra per cifra da destra verso sinistra tenendo conto dei riporti. Per ‘algoritmo’ si può intendere un «insieme di istruzioni che definiscono una sequenza di operazioni mediante le quali si risolvono tutti i problemi di una certa classe»; in particolare è importante l’ultima parte della definizione dove si
specifica che un algoritmo deve avere tra le sue caratteristiche quella della ‘generalità’. Un’altra fra le
numerose definizioni possibili può essere la seguente: «metodo di elaborazione da applicare a certi dati iniziali per ottenere dei dati finali o risultati». In ogni caso è importante che, in partenza, il problema da risolvere sia «ben posto» e cioè che:
• sia chiaro l’obiettivo da raggiungere;
• i dati di partenza sia noti e sufficienti;
• il problema sia risolubile da parte di chi lo affronta.
Se manca una di queste condizioni l’algoritmo non può essere individuato. Si deve anche notare come sia
importante, quando si affronta un problema, non confondere il risultato (ciò che si vuole ottenere) con la soluzione (il metodo che conduce al risultato elaborando i dati di partenza). L'algoritmo quindi rappresenta
proprio la soluzione o procedura risolutiva di un problema, o meglio di una determinata classe di problemi.
Classe
di
Problemi
Algoritmo
(Procedure Risolutiva)
Risultati
L’individuazione dell’algoritmo risolutivo di un problema è solo il primo passo di un procedimento in più
fasi che conduce alla soluzione del problema stesso in modo automatico con l’utilizzo del sistema di elaborazione. Questo procedimento verrà dettagliato in seguito, ma possiamo già accennare che il passo successivo consiste nella trasformazione dell’algoritmo in un programma scritto usando un linguaggio di
programmazione; si può affermare che l’algoritmo e il relativo programma sono due descrizioni, fatte con
mezzi diversi, dello stesso metodo di soluzione.
Formalizzazione di un problema
Generalmente, la descrizione di un qualsiasi problema (o classe di problemi) consiste in un predicato (affermazione espressa in lingua italiana, inglese, ecc) nel quale si espone il contesto, le condizioni e la struttura della problematica che si deve affrontare e nel quale si pongono in evidenza le variabili che costituiscono
i dati disponibili o dati di ingresso e nel quale si esplicitano le variabili che costituiscono i dati che si vogliono ottenere o dati di uscita. Quindi, una classe di problemi è costituita dai seguenti elementi:
• Condizioni, contesto e struttura della problematica da affrontare
• Elenco dei dati che abbiamo a disposizione (dati di ingresso)
• Elenco dei dati che si vogliono ottenere (dati di uscita)
Ad esempio, consideriamo il problema costituito dalla soluzione di un'equazione algebrica di secondo grado.
La formalizzazione di tale problema, secondo lo schema indicato, potrebbe portare alle seguenti conclusioni:
1) Condizioni, contesto e struttura della problematica
L'equazione di secondo grado è costituita da un'uguaglianza algebrica del tipo:
ax 2 + bx + c = 0
Nella quale i tre parametri a,b,c sono tre numeri reali noti, mentre il numero reale x è incognito. Risolvere
tale equazione algebrica in campo reale, significa determinare quei valori (uno o più di uno) dell'incognita x
che sostituiti nell'uguaglianza la soddisfano effettivamente (cioè rendono il primo membro uguale al secondo e riducono l'uguaglianza ad una identità).
2) Elenco dei dati che abbiamo a disposizione
Ogni qual volta viene assegnato il problema che consiste nel risolvere un'equazione di secondo grado, ci devno essere forniti i tre numeri reali a,b,c. Quindi tali numeri sono i dati di partenza su cui dobbiamo lavorare
per risolvere il problema. Quindi:
Dati di ingresso: a,b,c (numeri reali)
3) Elenco dei dati che si vogliono ottenere
La soluzione dell'equazione di secondo grado consiste nel determinare i valori reali di x che rendono vera
l'uguaglianza. Dalla teoria matematica delle equazioni, sappiamo che in generale, per un'equazione di secondo grado esistono due valori di x che rendono vera l'uguaglianza. Quindi, almeno in generale:
Dati di uscita: x1,x2 (numeri reali)
Problema: ax2+bx+c = 0
a
Dati noti
x1
?
b
Dati da ricavare
c
x2
Quindi, il problema dell'equazione di secondo grado, è un problema con tre ingressi (i tre numeri reali a,b,c)
e due uscite (i due numeri reali x1 e x2). Il punto interrogativo all'interno del rettangolo è il nostro
ALGORITMO (per ora non noto), cioè la procedura risolutiva che ci consentirà, a partire dai tre dati di ingresso a,b,c (numeri reali) di ottenere i due dati di uscita x1,x2 (numeri reali). Il nostro scopo è quello di determinare e descrivere tale algoritmo, cioè quella procedura o sequenza di elaborazioni e operazione che applicate ai dati di ingresso (numeri a,b,c) ci consente di ottenere i dati di uscita (numeri x1,x2). Dovremo
quindi sostituire tale punto interrogativo con una descrizione dei passi e delle elaborazioni che formano l'algoritmo (cioè la procedura risolutiva).
In generale quindi, è sempre possibile rappresentare un problema in forma grafica mediante la seguente
simbologia:
I1
I2
U1
Algoritmo
U2
?
In
Um
Descrizione delle caratteristiche
del problema
Sul lato sinistro del rettangolo indichiamo n frecce di ingresso I1,I2, ... , In che rappresentano gli n dati di
ingresso del problema. Sul lato destro del rettangolo indichiamo m frecce di uscita U1, U2, ... , Un che rappresentano gli m dati di uscita del problema. Gli n dati di ingresso del problema I1,I2, ... , In vengono supposti noti, gli m dati di uscita U1, U2, ... , Un sono considerati da ricavare. Nella descrizione del problema,
esporremo il contesto, le condizioni e la struttura della problematica. Daremo una spiegazione degli n dati di
ingresso, degli m dati di uscita e di come essi si correlano al problema. A questo punto, il prossimo passo
consiste nel determinare il nostro punto interrogativo, cioè l'ALGORITMO, ovvero la procedura risolutiva
che ci consente di passare dagli n dati di ingresso agli m dati di uscita. Tale algoritmo dovrà essere ricavato
mediante uno studio della specifica classe di problemi che si deve affrontare in questo caso, dopo di chè dovrà essere descritto per mezzo di qualche linguaggio.
L'Algoritmo (procedura risolutiva)
Il punto interrogativo contenuto nel rettangolo rappresenta ciò che ci consente di passare dai dati di ingresso
noti ai dati di uscita che dobbiamo determinare. Quindi il punto interrogativo rappresenta la procedura che
consente di risolvere il problema, ovvero il nostro ALGORITMO. L'algoritmo è quindi un insieme di meccanismi, operazioni, elaborazioni, scelte che applicate, in un dato ordine, ai dati di ingresso noti, ci consentono di ottenere i dati di uscita richiesti. Quindi, un algoritmo può essere pensato come una successione di
passi di elaborazione (operazioni matematiche e non, operazioni logiche, scelte, ecc) che applicati in un ben
determinato ordine, costruiscono la nostra procedura risolutiva, in grado di farci passare dai dati di ingresso
noti, ai dati di uscita da ottenere.
Proprietà degli algoritmi
Distinguiamo da ora in poi l’aspetto della risoluzione del problema da quello dell’esecuzione del relativo
algoritmo; distinguiamo cioè tra ‘risolutore’ (l’uomo, che individua il metodo di soluzione del problema) ed ‘esecutore’ (la macchina che deve eseguire tale metodo, descritto tramite un algoritmo).
L’algoritmo è solo la ‘descrizione delle operazioni’ da svolgere; è quindi un’entità statica
costituita da istruzioni, anch’esse statiche. Quando l’algoritmo viene eseguito si ottiene un ‘flusso
d’esecuzione’ che invece è ovviamente dinamico ed è costituito da ‘passi’ (ogni passo è l’esecuzione
di un’istruzione). A tale proposito può essere utile pensare alla differnza esistente tra una ricetta di
una torta (descrizione delle operazioni da svolgere) e la sua «esecuzione» per la realizzazione della torta.
[
]
Nel contesto informatico l’esecutore, cioè il sistema di elaborazione, è una macchina non intelligente, capace solo di eseguire istruzioni molto elementari (anche se in modo rapidissimo), ma senza alcuna capacità critica o di ragionamento autonomo.
Un algoritmo deve allora avere le seguenti proprietà:
• ‘generalità’, della quale abbiamo accennato in precedenza: non è un algoritmo il metodo che mi permette di stabilire se 19 è un numero primo, lo è invece il metodo che permette di decidere se un qualsiasi
numero naturale è primo;
• ‘comprensibilità’: l’algoritmo deve essere espresso in una forma comprensibile all’e- secutore;
questo non vuol dire che gli algoritmi debbano essere espressi nel linguaggio proprio della macchina,
cioè quello binario, e neanche che si debbano subito utilizzare i lin- guaggi di programmazione, ma
significa che si deve rispettare fin da subito un formalismo abbastanza rigido nella loro stesura;
• ‘eseguibilità’: l’algoritmo deve anche essere eseguibile dalla macchina e quindi non deve prevedere operazioni ad essa sconosciute (l’insieme delle istruzioni eseguibili da un computer ha ‘cardinalità finita’) o che non possano essere eseguite in un tempo finito (le istruzioni devono
avere ‘complessità finita’);
• ‘finitezza’: l’algoritmo deve avere un numero finito di istruzioni;
• ‘riproducibilità’: esecuzioni successive dell’algoritmo sugli stessi dati di input devono dare lo stesso risultato;
• ‘non ambiguità’: non possono esserci istruzioni vaghe, che prevedano un comportamen- to probabilistico o la scelta casuale del prossimo passo da eseguire; la macchina non è infatti in grado di prendere decisioni complesse con ragionamento autonomo e quindi il metodo di soluzione deve essere ‘completo’
(devono essere previste tutte le possibilità che possano verificarsi durante l’esecuzione) e ‘deterministico’ (in caso contrario verrebbe meno anche la caratteristica della riproducibilità);
• ‘discretezza’: l’esecuzione deve avvenire per passi discreti; i dati coinvolti nei calcoli assumono
valori che cambiano tra un passo e il successivo e non assumono altri valori
«intermedi»;
• ‘non limitatezza dell’input’: non deve esserci un limite (almeno in linea teorica) alla lunghezza dei dati di input (e quindi anche alla capacità di memoria dell’esecutore);
• ‘non limitatezza dei passi d’esecuzione’: devono essere possibili (almeno in linea
teorica) esecuzioni costituite da un numero infinito di passi.
Le ultime due proprietà possono sembrare irrealistiche nelle applicazioni pratiche, ma sono necessarie per i
seguenti motivi:
• se ci fosse un limite alla lunghezza dei dati di input verrebbe meno la proprietà della gene- ralità: ad esempio non potremmo più concepire un algoritmo per decidere se un qualsiasi numero naturale è primo
oppure no;
• esistono funzioni che si dimostra essere computabili a patto di ammettere un numero infinito di
passi nell’algoritmo di calcolo.
Gli algoritmi
4
Si deve comunque notare che queste grandi potenzialità teoriche degli algoritmi (input di lunghezza infinita e
esecuzioni con infiniti passi) non sono sufficienti ad assicurare che tutte le funzioni siano computabili o, detto in altri termini, che tutti i problemi prevedano un algoritmo risolutivo. Esiste ad esempio il famoso problema
della «terminazione degli algoritmi»: si può dimostrare che «non esiste un algoritmo che permetta di stabilire se
un qualsiasi altro algoritmo abbia termine».
Questi temi prettamente teorici inerenti la «teoria della computabilità» e che comprendono tra
l’altro lo studio degli automi, della macchina di Turing, della «Tesi di Church-Turing«, pur
molto interessanti, esulano dagli scopi di queste dispense e non vengono quindi ulteriormente trattati.
Rappresentazione degli algoritmi
Per quanto detto in precedenza appare del tutto impraticabile l’idea di affidare la descrizione di un algoritmo
all’uso del linguaggio naturale (nel nostro caso la lingua italiana) con il rischio di introdurre ambiguità dovute
alla presenza di sinonimi, omonimie, modi diversi di intendere la stessa frase. Si pensi ad esempio alle possibili diverse interpretazioni che si possono dare alle seguenti frasi: «succede al Sabato», «lavoro solo da due anni»,
«mi piace molto la pesca». Le interpretazioni e le sfumature delle frasi espresse in linguaggio naturale, spesso dipendenti dal contesto, possono essere colte solo dalla mente umana che è un «esecutore» molto più sofisticato, versatile ed elastico di qualsiasi macchina. Per descrivere gli algoritmi si deve quindi ricorrere a ‘linguaggi
formali’ creati appositamente (e quindi artificiali); in queste dispense prenderemo in considerazione due
esempi:
• un linguaggio ‘lineare’ basato sul testo, spesso denominato ‘linguaggio di progetto’;
• un linguaggio ‘grafico’ basato su simboli chiamato ‘diagramma di flusso’
(flow chart) o ‘diagramma a blocchi’.
Notiamo che, qualsiasi sia il metodo utilizzato per la rappresentazione dell’algoritmo, vale sem- pre la regola che
le operazioni da esso descritte vengono eseguite una alla volta nell’ordine in cui sono scritte (a meno che non intervengano costrutti di programmazione particolari che alterano il normale flusso di esecuzione). Prima di illustrare i due metodi di rappresentazione degli algoritmi è importante chiarire natura e ruolo delle variabili.
Concetto di variabile
Per ‘variabile’ si intende un oggetto che ha un nome e che permette di immagazzinare e conservare un
determinato valore; l’insieme delle variabili utilizzate in un certo algoritmo prende il nome di ‘area di lavoro’ relativa ad esso.
In generale una variabile è caratterizzata da tre elementi:
[
]
• ‘tipo’: indica se la variabile è un valore intero, reale, un carattere e così via;
• ‘nome’: deve essere univoco e «significativo» (cioè inerente al ruolo che la variabile ricopre
nell’elaborazione);
• ‘contenuto’: il valore che in un certo passo dell’elaborazione è assegnato alla variabile.
In un algoritmo possono essere poi presenti altri oggetti, detti ‘costanti’, che sono invece delle entità non
modificabili. Consideriamo ad esempio l’algoritmo (per il momento descritto con normali frasi della lingua
italiana) per calcolare il perimetro di un quadrato di lato 5:
• esegui l’operazione 5*4;
• poni il risultato nella variabile P;
• visualizza (o stampa) il valore del risultato P.
In questo esempio vengono usate la variabile P e le costanti 4 e 5.
Si noti in particolare la natura della seconda istruzione («poni ...») che è chiamata ‘istruzione di assegnazione’ ed è «distruttiva» in quanto, assegnando un nuovo valore a P cancella il contenuto precedente di
tale variabile. L’algoritmo appena descritto non è degno neppure di essere considerato tale perché manca del
tutto di generalità; una versione migliore è la seguente, ottenuta sostituendo una variabile ad una costante in modo
da ottenere il metodo di calcolo del perimetro di un quadrato di lato L qualsiasi:
• acquisisci in input (leggi) il valore del lato ponendolo in L;
• esegui l’operazione L*4;
• poni il risultato nella variabile P;
• stampa il valore di P.
In questo caso abbiamo la presenza delle variabili P ed L e della costante 4.
Sostituendo anche a quest’ultima con una una variabile si ha una versione ancora più generale dell’algoritmo,
che permette di calcolare il perimetro di un poligono regolare di N lati lunghi L:
• leggi il numero dei lati e la misura del lato ponendoli in N e L;
• esegui l’operazione L*N;
• poni il risultato nella variabile P;
• stampa il valore di P.
Gli algoritmi
6
Linguaggio di progetto
Il linguaggio di progetto che utilizziamo non fa parte di quelli ufficialmente riconosciuti come lo «Pseudo Pascal» o i linguaggi «L2P» e «L3P» creati dal professor Callegarin, ma è definito a scopo dimostrativo solo per
queste dispense. Si tratta di linguaggio basato su alcune parole della lingua italiana, sul simbolo di assegnazione
«=», sui normali operatori aritmetici e di confronto. Per quanto riguarda la sintassi delle istruzioni si fa riferimento a quella del linguaggio di programmazione c prevedendo quindi:
• l’uso del «;» per terminare ogni istruzione;
• i simboli «{» e «}» per iniziare e terminare i blocchi di istruzioni sequenziali;
• l’uso degli operatori «==», «!=» rispettivamente per l’uguaglianza e la disuguaglianza;
• l’uso degli operatori logici «!», «&&», «||» corrispondenti al NOT, AND, OR,
rispettivamente;
• l’uso delle parentesi tonde per racchiudere i controlli logici.
Con questo strumento dobbiamo descrivere tutte le normali fasi di un algoritmo che sono:
• ‘inizio’ dell’algoritmo: con la parentesi graffa aperta;
• ‘dichiarazione delle variabili’: effettuata usando le parole intero,
reale, carattere, stringa seguite dal nome della variabile o delle variabili separati
da virgole;
• ‘acquisizione dei dati di input’: con l’istruzione leggi e le variabili di
input indicate tra parentesi;
• ‘elaborazioni varie’: con gli opertaori aritmetici, logici e il simbolo di assegna-
zione
«=»;
• ‘emissione dei risultati’: istruzione stampa e le variabili di input indica-
te tra parentesi;
• ‘fine’ dell’algoritmo: con parentesi graffa chiusa.
Come esempio riscriviamo l’algoritmo per il perimetro del poligono regolare:
{
intero L,N,P;
leggi
(N,L);
P = N*L;
stampa(P);
}
Torniamo ancora brevemente sull’istruzione di assegnazione P = N*L; la logica di un’istruzione di
questo tipo è la seguente:
• a sinistra del simbolo di assegnazione ci può essere solo una variabile (non una costante o
un’espressione di qualche genere);
Gli algoritmi
7
• a destra del simbolo di assegnazione può trovarsi una variabile, una costante o
un’espressione di qualsiasi genere;
• viene valutato l’oggetto presente a sinistra e il valore risultante viene assegnato alla
variabile a destra, sovrascrivendo il valore precedentemente contenuto in essa.
Diagrammi di flusso
I ‘diagrammi di flusso’ permettono di descrivere gli algoritmi basandosi su simboli grafici
contenenti le operazioni da eseguire.
I simboli si chiamano appunto blocchi e si differenziano per la loro forma in base alla funzione che svolgono nella descrizione dell’algoritmo; sono uniti da archi orientati (frecce) che
permettono di esprimere la direzione del flusso di elaborazione.
Nella figura 2.2 è riportata la lista dei blocchi fondamentali con indicazione del ruolo svolto
all’interno del diagramma.
Figura 2.2
Dal blocco di inizio avremo sempre una freccia uscente, nel blocco di fine solo una freccia entrante; negli altri blocchi una freccia entrante ed una uscente ad eccezione del blocco di decisione
che in uscita prevede due flussi.
Un caso a parte è il blocco connettore che non ha un ruolo effettivo nella descrizione dell’algoritmo ma serve solo ad unire parti diverse di un diagramma suddiviso per motivi di
spazio.
Come esempio di primo banale diagramma di flusso, vediamo ancora una volta l’algoritmo per
il calcolo del perimetro del poligono regolare, mostrato nella figura 2.3.
Gli algoritmi
8
Figura 2.3
Strutture di controllo fondamentali degli algoritmi
Le istruzioni che compongono un algoritmo sono organizzate in ‘strutture’ che permettono di
avere un ‘controllo’ sul flusso di elaborazione.
Le ‘strutture di controlo fondamentali’, cioè quelle grazie alle quali si può descrivere qualsiasi algoritmo, sono:
• ‘sequenza’;
• ‘selezione’;
• ‘iterazione’ o ‘ciclo’.
Sequenza
La sequenza è una struttura molto banale e presente in qualsiasi algoritmo in quanto contiene
semplicemente un successione di istruzioni che devono essere eseguite nell’ordine in cui sono
elencate e questo corrisponde appunto alla natura di un qualsiasi algoritmo.
Negli algoritmi meno banali avremo però la presenza di strutture di sequenza all’interno di altri
tipi di strutture.
L’inizio e la fine di una sequenza si indicano nel linguaggio di progetto con la parentesi graffa
aperta e chiusa mentre nei diagrammi di flusso non sono previsti simboli particolari.
L’algoritmo del perimetro del poligono regolare è un esempio in cui è presente solo la struttura
di sequenza; come ulteriore esempio vediamo, con il linguaggio di progetto, l’algoritmo per lo
scambio del valore di due variabili:
{
intero A,B,C;
leggi (A, B);
C = A;
A = B;
B = C;
stampa(A, B);
}
Gli algoritmi
9
In questo algoritmo la variabile C non è né di input né di output ma è comunque indispensabile
per l’elaborazione che deve essere svolta; si dice allora che è una ‘variabile di appoggio’.
Selezione
La struttura, o ‘costrutto’ di selezione permette di:
• eseguire una o più istruzioni al verificarsi di una certa condizione; in tal caso si parla di
‘selezione a una via’;
• eseguire una o più istruzioni al verificarsi di una certa condizione ed altre istruzioni se non
si verifica; in questo caso si parla di ‘selezione a due vie’.
Nel linguaggio di progetto il costrutto di selezione a una via si indica con:
se (condizione) {
sequenza di istruzioni
}
Mentre quello di selezione a due vie con:
se (condizione) {
sequenza1 di istruzioni
}
altrimenti {
sequenza2 di istruzioni
}
Osservando questi primi costrutti si può già notare come le strutture di controllo possano essere ‘annidate’ le une nelle altre: infatti all’interno dei rami delle istruzioni di selezione si trovano delle «sequenze» di istruzioni (che possono comunque essere costituite anche da una sola
istruzione).
Come esempio consideriamo il problema dell’individuazione della soluzione di un’equazione di
primo grado, a*x = b, scrivendo il relativo algortimo in linguaggio di progetto:
{
intero
a,b,x;
leggi (a,b);
se ( (a==0) && (b==0) ) {
stampa ("Equazione indeterminata");
}
se ( (a==0) && (b!=0) ) {
stampa ("Equazione impossibile");
}
se (a!=0) {
Gli algoritmi
Figura
x 2.9
=
10
b/a;
stampa (x);
}
}
In questo esempio si è fatto uso di un’importante convenzione per la scrittura dell’algoritmo che è
l’‘indentazione’; si tratta di scrivere leggermente spostate verso destra (di qualche spazio o di
una tabulazione) le sequenze di istruzioni annidate dentro altre strutture allo scopo di migliorare
la leggibilità dell’algoritmo.
Si tratta di una tecnica molto utile, anche e soprattutto quando gli algoritmi diventano programmi
e si devono scrivere sorgenti, spesso molto lunghi e complessi, usando un qualche linguaggio di
programmazione.
Nell’esempio è stata indentata anche la sequenza principale dell’algoritmo, quella contenuta tra le
parentesi graffe di inizio e fine.
Naturalmente l’indentazione non è obbligatoria né nel linguaggio di progetto né usando i
linguaggi di programmazione veri e propri, ma è fortemente consigliata.
I suoi effetti benefici si apprezzano meglio riscrivendo l’algoritmo precedente usando strutture di
selezione a due vie annidate invece che una sequenza di tre strutture di selezione a una via:
{
intero
a,b,x;
leggi (a,b);
se ( (a==0) && (b==0) ) {
stampa ("Equazione indeterminata");
}
altrimenti {
se ( (a==0) && (b!=0) ) {
stampa ("Equazione impossibile");
}
altrimenti
{
x = b/a;
stampa (x);
}
}
}
Il fatto che si siano scritti due algoritmi per la soluzione di uno stesso problema è da considerare del tutto normale (e anzi, ce ne sono anche diversi altri); infatti i metodi di soluzione possono essere vari e differenti l’uno dall’altro, sarà compito del bravo programmatore individuare
la soluzione migliore tra quelle possibili.
Per quanto riguarda i diagrammi di flusso, non esiste un blocco particolare per designare il costrutto di selezione; si fa uso, in modo opportuno, del blocco di decisione, come mostrato nella
figura 2.9 (a sinistra abbiamo la selezione a una via, a destra quella a due vie).
Gli algoritmi
11
Figura 2.9
Nella successiva figura 2.10 vediamo l’algoritmo per la soluzione dell’equazione di primo grado
nella «seconda versione» (quella con le selezioni annidate).
Figura 2.10
Concludiamo questa breve introduzione sul costrutto di selezione osservando come il suo utilizzo
permetta di scrivere algoritmi «completi» secondo la definizione data nel paragrafo 2.1.
In realtà sarebbe meglio dire che tale costrutto permette di «tentare» di scrivere algoritmi che
tengano conto di tutte le eventualità possibili al momento dell’esecuzione, ad esempio al variare
dei dati in input; questa infatti non è un’operazione semplice, specie in presenza di problemi di
una certa consistenza, ed è uno dei principali motivi di malfunzionamento di molti programmi.
Gli algoritmi
12
Iterazione
Figura 2.13
La struttura, o costrutto, di iterazione (o ciclo) permette di ripetere una o più istruzioni in
sequenza per un numero potenzialmente infinito di volte.
Grazie alla sua presenza è possibile che gli algoritmi pur avendo la proprieta della «finitezza»
possano (in linea teorica) prevedere esecuzioni con un numero infinito di passi (come detto nel
paragrafo 2.1).
La ripetizione del gruppo di istruzioni soggette alla iterazione è soggetta al verificarsi di una
certa condizione ed il controllo su di essa può essere in «testa» alla struttura oppure in «coda»
alla stessa.
Ci sono quindi due tipi di iterazione:
• ‘iterazione con controllo in testa’;
• ‘iterazione con controllo in coda’.
Nel linguaggio di progetto i due tipi vengono realizzati nel modo seguente:
mentre (condizione) {
sequenza di istruzioni
}
esegui {
sequenza di istruzioni
}
mentre (condizione);
In entrambi i casi siamo in presenza di un ‘corpo del ciclo’, che corrisponde alla sequenza di
istruzioni (anche una sola) da ripetere, e di un ‘controllo del ciclo’ rappresentato dalla
condizione posta in testa o in coda al costrutto.
Il ‘criterio di arresto’ del ciclo (o l’‘uscita’ dal ciclo) si ha quando la condizione
cessa di essere vera; si dice quindi che entrambi questi tipi di iterazione sono ‘per vero’. La
fondamentale differenza tra i due costrutti di ciclo è nel fatto che il corpo del ciclo con
controllo in coda viene sempre eseguito almeno una volta.
Come per la selezione, anche per l’iterazione non esistono blocchi particolari da usare nei
diagrammi di flusso; si deve ancora una volta usare in modo appropriato il blocco di decisione.
Nella figura 2.13 vediamo a sinistra il ciclo con decisione in testa e a destra quello con decisione
in coda; vengono indicati i componenti dei costrutti, controllo e corpo, e i punti di ingresso e
uscita dai costrutti stessi.
Gli algoritmi
13
Figura 2.13
Prima di vedere esempi riguardanti i cicli introduciamo l’uso di alcuni tipi di variabili che hanno un
ruolo importante in molti algoritmi iterativi:
• si dice ‘contatore’ una variabile che si usa per contare il numero di volte che si è eseguita
una certa iterazione; si inizializza di solito a 0 oppure a 1, secondo come si imposta il ciclo;
• si dice ‘accumulatore’ una variabile che si usa per contenere il risultato di operazioni
(somme o prodotti) successive; si imposta a zero se si devono accumulare somme, a 1 se
si accumulano prodotti.
Come primo esempio vediamo l’algoritmo per il calcolo del prodotto tra due interi positivi a e b
come successione di somme:
1
{
2
intero a,b,p,cont;
3
leggi (a,b);
4
p = 0;
5
cont = 0;
6
mentre (cont < b) {
p = p + a;
7
cont = cont + 1;
8
}
9
stampa(p);
10
11
}
In questo listato sono stati aggiunti i numeri di riga in modo da facilitare la stesura della ‘tavola
di traccia’ che è uno strumento grazie al quale si può esaminare il flusso di elaborazione
dell’algoritmo e verificarne la correttezza.
Si tratta di una tabella in cui si indicano nelle colonne i nomi delle variabili (almeno le più
importanti e significative per l’elaborazione) e nelle righe i passi dell’algoritmo che via via si
svolgono partendo da valori di input scelti a piacere; può essere utile inserire anche una colonna
in cui si indica il valore di verità della condizione da cui dipende l’esecuzione dell’iterazione.
Quando si esegue la tavola di traccia si deve avere l’accortezza di esaminare il comportamento
dell’algoritmo anche quando i valori di ingresso sono ‘critici’ o ‘ai limiti’; nel caso del
Gli algoritmi
14
prodotto avremo tale situazione per a uguale a zero, b uguale a zero o entrambi uguali a zero.
Vediamo una prima tavola di traccia del nostro algoritmo scegliendo due valori «normali» delle
variabili, ad esempio a=5 , b=3 :
Passo
Istr.
cont
p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
3
4
5
6
7
8
6
7
8
6
7
8
6
10
0
0
0
1
1
1
2
2
2
3
3
3
0
0
0
5
5
5
10
10
10
15
15
15
15
contr.
ciclo
V
V
V
V
V
V
V
V
V
F
F
Il valore di output è, correttamente, 15.
Adesso compiliamo la tavola di traccia di uno dei casi critici, ad esempio con a=7 e b=0 :
Passo
Istr.
cont
p
1
2
3
4
5
3
4
5
6
10
0
0
0
0
0
0
0
contr.
ciclo
F
F
Il valore di output è, correttamente, 0.
Infine vediamo un altro caso abbastanza particolare con a=8 e b=1:
Passo
1
2
3
4
5
6
7
8
Istr.
3
4
5
6
7
8
6
10
cont
0
0
0
1
1
1
p
0
0
0
8
8
8
8
contr. ciclo
V
V
V
F
F
Il valore di output è, correttamente, 8.
Come ulteriore esempio vediamo un algoritmo simile per il calcolo dell’elevamento a potenza
come successione di prodotti.
In questo caso introduciamo una prima forma rudimentale di ‘controllo dell’input’; vogliamo cioè verificare che i valori di partenza siano: la base a positiva e l’esponente b non
negativo (il fatto che siano entrambi interi viene invece ancora dato per scontato e non viene
controllato).
Gli algoritmi
1
15
{
2
intero a,b,p,cont;
3
esegui {
leggi (a,b);
4
5
}
6
mentre ( (a <= 0) || (b < 0) );
7
p = 1;
8
cont = 0;
9
mentre (cont < b) {
10
p = p * a;
cont = cont + 1;
11
}
12
stampa(p);
13
14
}
La verifica viene fatta con l’uso di un ciclo con controllo in coda che fa ripetere l’input finché i
valori non sono corretti; si noti come sia indispensabile l’uso di questo tipo di ciclo in quanto
almeno una volta le istruzioni del corpo devono essere eseguite.
Altra osservazione importante riguarda il valore di partenza dell’accumulatore p che è 1 in quanto
l’accumulo avviene tramite dei prodotti.
Anche in questo caso eseguiamo la tavola di traccia con i valori a=5 , b=3 prendendo in esame
solo i passi successivi al controllo dell’input che non influiscono sul calcolo della potenza:
Passo
Istr.
cont
p
1
2
3
4
5
6
7
8
9
10
11
12
13
7
8
9
10
11
9
10
11
9
10
11
9
13
0
0
0
1
1
1
2
2
2
3
3
3
1
1
1
5
5
5
25
25
25
125
125
125
125
contr.
ciclo
V
V
V
V
V
V
V
F
F
Il valore di output è, correttamente, 125.
Adesso compiliamo la tavola di traccia del caso critico più significativo, quello con b=0 (e a
qualsiasi, ad esempio, 12):
Passo
Istr.
cont
p
1
2
3
4
7
8
9
13
0
0
0
1
1
1
1
Il valore di output è, correttamente, 1.
contr.
ciclo
F
F
Gli algoritmi
16
Il successivo esempio riguarda l’individuazione del massimo tra un certo numero di valori
immessi in successione grazie un apposito ciclo.
{
intero
val,
carattere
max;
risposta;
leggi (val);
max = val;
esegui {
leggi (val);
se
(val
>
max)
{
max = val;
}
stampa
("Vuoi
continuare
(s/n)");
leggi (risposta);
}
mentre ( (risposta != ’n’) && (risposta != ’N’) );
stampa(max);
}
In questo esempio si può evidenziare l’uso della tecnica di ‘elaborazione fuori ciclo’ che
permette di inserire un primo valore che viene impostato come massimo; successivamente,
tramite un ciclo con controllo in coda, si inseriscono gli altri valori uno alla volta facendo per
ognuno il confronto con il valore massimo e adeguando quest’ultimo se necessario.
Come ultimo esempio vediamo l’algoritmo per il calcolo della media di n valori.
1
{
2
intero n, val, cont, somma;
3
reale media;
4
esegui {
stampa ("Numero di valori desiderati ");
5
leggi (n);
6
7
}
8
mentre (n <= 0);
9
somma = 0;
10
cont = 0;
11
mentre (cont < n) {
12
stampa("Inserire un valore ");
13
leggi (val);
14
somma = somma + val;
cont = cont + 1;
15
16
}
17
media = somma/n;
stampa (media);
18
19
}
In questo algoritmo si fa uso di un ciclo con controllo in coda per inserire, in modo controllato, la
quantità di valori su cui impostare il ciclo principale (righe da 4 a 8); il ciclo non si conclude se il
valore inserito non è maggiore di zero e questo permette, appunto, di avere un piccolo controllo
sul valore n .
Successivamente si esegue il ciclo, ad ogni passo del quale il valore immesso viene accumulato
nella variabile somma (righe da 11 a 16).
Gli algoritmi
17
Infine, esauritosi il ciclo, si calcola e si stampa a video la media (righe 17 e 18).
In questo caso vediamo anche la tavola di traccia considerando solo la parte più significativa dell’algoritmo, cioè quella che comprende il ciclo principale di elaborazione; i valori della variabile
val sono indicati in tabella al momento dell’esecuzione della relativa istruzione di input, mentre
per n supponiamo venga immesso il valore 3 :
Passo
Istr.
somma
media
val
cont
1
2
3
4
5
6
7
8
9
10
11
12
13
14
16
17
9
10
11
12-13
14
15
11
12-13
14
15
11
12-13
14
15
11
17
0
0
0
0
7
7
7
7
10
10
10
10
18
18
18
18
6
7
7
7
7
3
3
3
3
8
8
8
8
8
0
0
0
0
1
1
1
1
2
2
2
2
3
3
3
contr.
ciclo
V
V
V
V
V
V
V
V
V
V
V
V
F
F
Concludiamo il paragrafo con i diagrammi di flusso relativi all’algoritmo della potenza calcolata
come successione di prodotti e al calcolo della media di n valori, illustrati rispettivanmente nelle
figure 2.24 e 2.25.
Figura 2.24: potenza come succ. di prodotti
Figura 2.25: media di n valori
Strutture di controllo derivate degli algoritmi
Le ‘strutture di controllo derivate’ hanno questo nome in quanto si possono ottenere dal
quelle fondamentali, e ne estendono le potenzialità, ma non sono essenziali per la stesura degli algoritmi.
Esistono due strutture derivate:
• ‘selezione multipla’;
• ‘iterazione calcolata’.
Selezione multipla
Con la selezione multipla si possono eseguire sequenze diverse di istruzioni in base al valore di
una certa variabile intera.
Nel linguaggio di progetto il costrutto da utilizzare è:
valuta
(var) { ca-
so valore1:
sequenza di istruzioni 1
Gli algoritmi
19
caso valore2:
sequenza di istruzioni 2
.
.
.
caso valoreN:
sequenza
di
istruzioni
N de-
fault:
sequenza di istruzioni N+1
}
Nei diagrammi di flusso si può utilizzare il blocco mostrato nella figura 2.27.
Figura 2.27
Il costrutto di selezione multipla non è essenziale perché al suo posto si possono usare opportune
combinazioni di selezioni «normali».
Come esempio di uso di questa struttura derivata vediamo la realizzazione di un semplice ‘menu di
scelta’ che esegue istruzioni diverse in base al valore scelto dall’utente.
{
intero
scelta; ese-
gui {
stampa ("Menu delle scelte");
stampa ("1 - Funzione 1");
stampa ("2 - Funzione 2");
stampa ("3 - Funzione 3"); stampa ("9 - Fine");
leggi
(scelta);
valuta (scelta) {
caso 1:
Gli algoritmi
20
stampa ("Hai scelto la funzione 1");
salta;
caso 2:
stampa ("Hai scelto la funzione 2");
salta;
caso 3:
stampa ("Hai scelto la funzione 3");
salta;
caso 9:
stampa ("Arrivederci e grazie!"); salta;
default:
stampa ("Scelta errata");
}
mentre (scelta != 9);
}
L’istruzione ‘salta’ porta il flusso di esecuzione al termine del costrutto ‘valuta’ e ha lo scopo di
non far eseguire le istruzioni dei casi successivi.
Il caso ‘default’ viene inserito per eseguire un blocco di istruzioni nel caso il valore della variabile valutata non sia nessuno di quelli previsti.
Ovviamente l’esempio non ha alcuna utilità pratica e serve solo a mostrare le modalità di
impostazione e gestione del menu.
Iterazione calcolata
L’iterazione calcolata è solo un modo più compatto di scrivere il ciclo con controllo in testa; per
tale motivo nei diagrammi di flusso non esiste alcun costrutto particolare che la rappresenti.
Nel linguaggio di progetto abbiamo invece:
per (cont=valore1;cont<valore2;cont=cont+1) {
sequenza di istruzioni
}
Il significato dell’istruzione è abbastanza intuibile:
• cont viene inizializzato al valore1;
• il ciclo viene eseguito finché rimane vera la condizione che cont sia minore di valore2 ;
• ad ogni passo del ciclo cont viene incrementato di una unità.
Rispetto all’iterazione con controllo in testa vista precedentemente si notano:
• lo spostamento dell’istruzione di inizializzazione del contatore, che prima avveniva fuori
dal costrutto del ciclo e adesso è inglobata nell’intestazione;
• lo spostamento dell’istruzione di incremento del contatore, che prima avveniva nel cor-
po del ciclo e adesso è inglobata nell’intestazione.
Gli algoritmi
21
Quello mostrato può essere considerato solo un esempio di ciclo calcolato: nulla vieta infatti di
avere valori iniziali, condizioni e incrementi del tutto diversi.
Come esempio vediamo l’algoritmo per la stampa della tabellina pitagoriga per i valori da 1 a 10
dove usiamo due iterazioni calcolate una annidata nell’altra.
{
intero i, j;
per
(i=1;i<=10;i=i+1)
{
per (j=1;j<=10;j=j+1) {
stampa (i*j);
}
}
}
Nella figura 2.31 vediamo anche il relativo diagramma di flusso.
Figura 2.31
La programmazione strutturata
Con il termine ‘programmazione strutturata’ si intende una tecnica di realizzazione de- gli
algoritmi e dei relativi programmi che prevede il solo utilizzo delle strutture di controllo fondamentali.
In realtà si può parlare di algoritmi o programmi ‘strutturati’ anche in presenza di strutture di
controllo derivate, in quanto esse, come abbiamo visto in precedenza, sono ottenibili da quelle
fondamentali.
Gli algoritmi
22
Ogni struttura di controllo può essere immaginata come un blocco, con un solo flusso in entrata
ed uno solo in uscita, che al suo interno può contenere una singola istruzione o una ulteriore
struttura di controllo (annidamento).
Come esempio consideriamo il problema del calcolo del fattoriale di un numero n maggiore di 1
e risolviamolo con i tre diversi algoritmi descritti dai diagrammi a blocchi delle figure 2.32, 2.33 e
2.34; nelle tre soluzioni viene tralasciato il controllo sulla validità dell’input.
Figura 2.32
Figura 2.33
Gli algoritmi
23
Figura 2.34
Questi tre algoritmi sono ‘funzionalmente equivalenti’ cioè danno gli stessi risultati partendo dagli stessi dati di input.
Il primo algoritmo contiene un ciclo con controllo in testa, il secondo un ciclo con controllo in
coda, il terzo è da scartare perché contiene un ciclo non valido che ha il controllo né in testa né
in coda.
Il teorema di Jacopini-Bohm
Ci si potrebbe chiedere se le tre strutture fondamentali formano un insieme di strutture ‘completo’
(cioè sufficiente a descrivere tutti gli algoritmi). A tale proposito esiste il teorema di ‘Jacopini - Bohm
’ del 1966 (che non dimostriamo) che afferma: «ogni algoritmo può essere espresso con le sole tre strutture
di controllo fondamentali». Quindi è lecito pretendere da parte dei programmatori l’uso delle tecniche di
programmazione strutturata, anche perché con esse si hanno i seguenti vantaggi:
• maggiore facilità di uso della metodologia top-down (illustrata nel prossimo paragrafo);
• maggiore leggibilità degli algoritmi;
• maggiore possibilità di individuazione di eventuali errori nella logica risolutiva.
Nella figura 2.35 vediamo: a sinistra una porzione di algoritmo non strutturato (il ciclo ha il controllo né in
testa, né in coda), a destra la sua strutturazione grazie all’uso di una variabile di tipo ‘switch’ (segnalatore),
chiamata sw:
Gli algoritmi
24
Figura 2.35
L’uso di variabili di segnalazione avviene solitamente nel seguente modo:
• la variabile viene «spenta» (assume il valore 0) all’inizio dell’algoritmo;
• viene poi «accesa» (assume il valore 1) al verificarsi di una certa condizione;
• infine viene «testata» in modo da decidere le azioni da intraprendere in base al suo valore.
Le tecniche Top-Down
Un algoritmo si ottiene come risultato di un procedimento di ‘analisi’ che può essere suddiviso
in tre fasi:
• ‘ definizione del problema’ con i suoi dati in ingresso ed in uscita;
• individuazione del ‘metodo’ di soluzione;
• ‘descrizione’ dell’algoritmo, con il linguaggio di progetto o con il diagramma a blocchi.
Una metodologia di analisi molto usata e anche molto conveniente è quella che fa
uso delle tecniche top-down o delle ‘scomposizioni successive’.
Esse si basano appunto su una scomposizione del problema in sottoproblemi che a loro volta
possono essere ulteriormente scomposti fino ad arrivare a sottoproblemi molto semplici costituiti
solo da operazioni molto elementari, direttamente «eseguibili» da parte dell’esecutore.
Naturalmente tale metodologia è utile soprattutto in presenza di problemi abbastanza complessi;
in caso di problemi semplici, come quelli degli esempi finora esaminati, la sequenza di operazioni
proposta nei vari algoritmi risolutivi, può essere considerata già sufficientemente semplice.
Ad ogni sottoproblema viene associato un «sottoalgoritmo» o sottoprogramma, che nei diagrammi di flusso è individuato dal blocco rettangolare con le bande laterali mostrato nella figura 2.2
(tralasciamo invece l’uso dei sottoalgoritmi nel linguaggio di progetto).
Gli algoritmi
25
Il dettaglio del sottoalgoritmo viene descritto con i consueti blocchi scrivendo però nel blocco
iniziale il suo nome e l’eventuale lista dei valori (parametri) su cui deve operare e nel blocco di
fine la parola «ritorno» seguita dall’eventuale valore di ritorno.
Come esempio consideriamo il problema del calcolo del coefficente binomiale «c su k» dove c e
k sono due interi positivi con c >
k.
La soluzione proposta si basa sulla formula di calcolo del coefficente e cioè: c! / (k!*(c-k)!) dove il
simbolo «!» indica il fattoriale; nell’algoritmo si fa uso di un opportuno sottoalgoritmo per il calcolo del fattoriale di un valore n .
Il tutto è mostrato nella figura 2.36.
Figura 2.36
In generale le tecniche di analisi e programmazione top-down sono da consigliare per almeno tre
motivi:
• ‘semplificazione’ dell’algoritmo o del programma che viene scomposto in parti pre-
sumibilmente più semplici; occorre però prestare attenzione all’incremento di complessità
dovuto all’esigenza di integrare i vari sottoprogrammi;
• ‘risparmio di istruzioni’ nel programma; nell’esempio precedente appare abba- stanza
ovvio come, con l’uso del sottoprogramma fat , si evita di scrivere tre volte il pro- cedimento per il calcolo del fattoriale, come sarebbe richiesto dalla formula del coefficente
binomiale;
• ‘riuso del codice’; si parla in questo caso di ‘programmazione modulare’ cioè
del fatto che si possono scrivere sottoprogrammi (o ‘moduli’) in modo che possano essere riutilizzati per programmi diversi e da persone diverse.
Gli algoritmi
26
Questi temi, pur molto interessanti, esulano dagli scopi di questa dispensa e quindi non vengono
ulteriormente approfonditi al pari delle nozioni sui parametri e sui valori di ritorno dei sottoprogrammi; per approfondimenti su questi aspetti si rimanda allo studio di specifici linguaggi di
programmazione.
Concludiamo con un ultimo esempio, mostrato nella figura 2.37, in cui vediamo il diagramma di flusso relativo alla gestione di un menu che permette di scegliere tra tre opzioni, ognuna
corrispondente ad un certo sottoprogramma (non ulteriormante dettagliato).
i
Corso sul Linguaggio C++ Edizione 2010
1
Capitolo
1. Fondamenti del linguaggio C
Caratteristiche del linguaggio, creazione dei primi programmi .
1.1
Caratteristiche e storia del linguaggio C.
Nel 1972, presso i Bell Laboratories, Dennis Ritchie progettava e realizzava la prima versione del linguaggio C.
Ritchie aveva ripreso e sviluppato molti dei principi e dei costrutti sintattici del linguaggio BCPL, sviluppato da
Martin Richards, e del linguaggio B, sviluppato da Ken Thompson, l'autore del sistema operativo Unix. Successivamente gli stessi Ritchie e Thompson riscrissero in C il codice di Unix. Il C si distingueva dai suoi predecessori per il fatto di implementare una vasta gamma di tipi di dati (carattere, interi, numeri in virgola mobile, strutture) non originariamente previsti dagli altri due linguaggi. Da allora ad oggi il C ha subito trasformazioni: la sua
sintassi è stata affinata, soprattutto in conseguenza della estensione object-oriented (C++). Il C++, come messo
in evidenza dallo stesso nome, rappresenta una evoluzione del linguaggio C: il suo progettista (Bjarne Stroustrup) quando si pose il problema di trovare uno strumento che implementasse le classi e la programmazione ad
oggetti, invece di costruire un nuovo linguaggio di programmazione, pensò bene di estendere un linguaggio già
esistente, il C appunto, aggiungendo nuove funzionalità. In questo modo, contenendo il C++ il linguaggio C come sottoinsieme, si poteva riutilizzare tutto il patrimonio di conoscenze acquisito dai programmatori in C (linguaggio estremamente diffuso in ambito di ricerca) e si poteva fare in modo che tali programmatori avessero la
possibilità di acquisire le nuove tecniche di programmazione senza essere costretti ad imparare un nuovo linguaggio e quindi senza essere costretti a disperdere il patrimonio di conoscenze già in loro possesso. Così le estensioni ad oggetti hanno fornito ulteriore linfa vitale al linguaggio C.
Le principali caratteristiche del linguaggio C e C++ sono:
- Si tratta di un linguaggio general purpose, ovvero può essere utilizzato per realizzare programmi di natura
diversa (dai sistemi operativi, ai programmi gestionali e ai videogiochi)
- Produce dei programmi efficienti. E’ stato progettato per scrivere più facilmente i programmi che costituiscono un sistema operativo i quali devono essere molto efficienti. Normalmente, prima della creazione
del linguaggio C, i sistemi operativi venivano scritti in Assembler. Il linguaggio C è un linguaggio di alto
livello (più vicino al linguaggio dell’uomo) che però mantiene anche delle caratteristiche tipiche dei linguaggi di basso livello (più vicini al linguaggio della macchina e quindi più scomodi da utilizzare per il
programmatore).
- Supporta sia il paradigma procedurale che il paradigma della programmazione orientata agli oggetti.
Con il paradigma procedurale il programma consiste in una serie di istruzioni che dicono al computer come acquisire i dati in ingresso e produrre i risultati finali, mentre con il paradigma orientato agli oggetti il
programma è costituito da una serie di oggetti software ognuno di quali ha determinate caratteristiche, il
programmatore mette insieme e/o costruisce questi oggetti.
1.2
Differenze principali tra C e C++
Come detto il linguaggio C++ è un’estensione del linguaggio C e quindi tutti i programmi C possono essere
compilati da un compilatore C++
Corso sul Linguaggio C++ Edizione 2010
Il principale miglioramento del linguaggio C++ rispetto al linguaggio C è stata l’introduzione dei costrutti necessari per realizzare programmi orientati agli oggetti. Nel seguito noi non ci occuperemo della programmazione ad
oggetti ma solo di quella procedurale per la quale le differenze tra C e C++ sono limitate, eccone alcune:
- I commenti. Con il linguaggio C i commenti vanno racchiusi tra i simboli /* ……*/, nel linguaggio C++
è stato introdotto anche il simbolo // che indica al compilatore che tutto quello che segue nella riga è un
commento;
- Dichiarazioni all’interno dei blocchi. Con il C++ sono consentite, con il C no.
- Nuovi nomi di tipi. Sono stati introdotti nuovi nomi di tipi che semplificano la notazione.
- E’ stato introdotto lo specificatore const.
- Più funzioni possono utilizzare lo stesso nome, per distinguerle basta che abbiano un numero o tipo di parametri diverso (overloading di funzioni).
- Si possono richiamare le funzioni con un numero di parametri inferiore a quello dichiarato, i parametri non
passati vengono sostituiti con valori standard
- Sono stati introdotti gli operatori new e delete per il controllo dell’allocazione dinamica della memoria.
1.3
Installazione del Dev-C++
Il linguaggio C è un linguaggio compilato, questo significa che dopo aver scritto il programma sorgente, per poterlo eseguire bisogna prima compilarlo, ovvero tradurlo in linguaggio macchina. Il programma risultante dopo
questa traduzione è detto programma oggetto. La traduzione viene fatta da un altro programma che si chiama
compilatore. Per poter scrivere ed eseguire i programmi in linguaggio C bisogna procurarsi un compilatore C.
Sul server del laboratorio è presente l’ambiente di sviluppo Dev C++ versione 4.01 che comprende un compilatore C++ che è open source e distribuito con licenza GNU ed è quindi liberamente scaricabile e utilizzabile nel
rispetto della licenza. Per gli aggiornamenti si può visitare il sito del produttore:
http://www.bloodshed.net/c/index.html. Attualmente è disponibile anche una versione più recente, la versione
4.9.9.2, (presente anch’essa sul server nella cartella “Aggiornamenti”) ma, sebbene sia più comoda da utilizzare,
è ancora una versione beta ed è diversa da quella ufficialmente utilizzata per le olimpiadi dell’informatica.
Per installare il compilatore e il debugger (programma per facilitare la correzione degli errori) presenti sul server
(insieme a molto altrro materiale) all’indirizzo \\192.168.2.198\CorsoCPP\, bisogna seguire le seguenti istruzioni.
1) Aprire la cartella DevCPP e Copiare la cartella C++ che si trova all’interno sul proprio computer;
2) Decomprimere il file “devcpp4.zip” ed eseguire il file “setup.exe”; ciò installerà la versione 4.0 del
Dev-C++.
3) Sostituire il file “DevCpp.exe” contenuto nella directory di installazione del Dev-C++ (ad esempio
“C:\Dev-C++”) con il file “DevCpp.exe” contenuto dentro l’archivio devcpp401.zip.
4) Decomprimere il file insight5_win32.zip (che contiene il debugger per l’ambiente) e che si trova nella
cartella Debugger; nell’archivio sono contenute due cartelle: “bin” e “share”. Copiare la directory
“share” nella directory di installazione del Dev-C++ (ad esempio “C:\Dev-C++”), e copiare i file contenuti nella cartella “bin” nella cartella “bin” della directory di installazione del Dev-C++ (ad esempio
C:\Dev-C++\bin)
5) A questo punto avete installato l’ambiente di sviluppo. Conviene crearsi una cartella personale sul disco
rigido del computer dove mettere tutti i programmi che scriverete e impostarla come cartella di default
per l’ambiente di sviluppo. Per farlo prima create la cartella (per esempio la cartella
c:\<vostronome>\cpp); poi lanciate l’ambiente di sviluppo da start programmiDevC++Dev-C++;
rispondete si alla richiesta se abbinare le estensioni .cpp, .dev ecc. al Dev-C++; dopo aver avviato
l’ambiente di sviluppo fate click su OptionsEnvironment Options sulla barra dei menù e nella scheda
Preferences, impostate la Default directory con il percorso della directory che avete creato per i programmi C++ (vedi figura che segue).
2
Corso sul Linguaggio C++ Edizione 2010
1.4
Gli elementi di un programma C++
Un qualsiasi programma C++ è costituito dai seguenti elementi:
Direttive al preprocessore
Dichiarazioni di variabili globali, funzioni e prototipi di funzioni
Programma principale (funzione main)
Funzione 1
Funzione N
Tutti gli elementi sono facoltativi esclusa la funzione main (il programma principale) che deve sempre esserci.
Le regole con cui deve essere scritto un programma o un’istruzione di un programma sono dette regole sintattiche in analogia per le regole del nostro linguaggio naturale. Per descrivere queste regole si utilizzano spesso, nei
manuali dei linguaggi di programmazione, i grafi sintattici che rappresentano tutti i modi in cui si possono costruire le frasi valide del linguaggio. Per esempio, utilizzando un grafo sintattico per descrivere quanto detto
prima, un programma C++ deve essere costruito nel seguente modo:
3
Corso sul Linguaggio C++ Edizione 2010
Programma C++
Funzione
main()
Direttive al
preprocessore
Variabili
globali
Funzioni e
Prototipi di
funzioni
Dichiarazioni altre
funzioni
Il programma Principale (così come qualsiasi funzione) deve avere la seguente struttura:
[<Tipo restituito>] main ([elenco parametri])
{
[istruzione1;]
[istruzione2;]
……..
[istruzione n;]
}
<Tipo restituito> indica il tipo di dati restituito dalla funzione, in mancanza di indicazione si
sottintende int ovvero intero. Una delle istruzione della funzione è normalmente:
return <valore>
che fa terminare la funzione restituendo il valore indicato (se non viene inserita il compilatore inserisce automaticamente return 0).
Il più semplice programma C è quindi il seguente:
int main()
{
return 0
}
Se una funzione non restituisce nessun valore si può anche indicare, come tipo, la parola chiave void che rappresenta un tipo di dato particolare. Questo è il modo in cui si dichiarano nel linguaggio C le procedure. Per esempio se il programma principale non deve restituire nessun valore si può scrivere:
void main()
{
}
Nel seguito, utilizzeremo principalmente questa seconda sintassi per la specificazione della funzione che costituisce il programma fondamentale main.
1.5
Dal codice sorgente al codice eseguibile
Vediamo ora come si realizza praticamente un programma C++. I passi da seguire per creare un programma eseguibile sono i seguenti:
1. Scrittura del programma sorgente; Apriamo l’ambiente di sviluppo Dev-C++ e scegliamo New Source
File dal menù File. Verrà visualizzato il file default.txt che contiene la base per un qualsiasi programma
C. A questo punto scriviamo il programma, per esempio quello della figura che segue. Alla fine salviamo
il file usando la voce Save Unit o Save Unita s .. dal menù File. Il programma verrà salvato con il nome
che scegliamo e normalmente con l’estensione .cpp (verificare che la cartella sia quella giusta);
2. Compilazione e linking. Un programma scritto in linguaggio C non può essere eseguito così com’è perché il computer capisce solo il linguaggio macchina. Per eseguire un programma bisogna compilarlo (tradurlo in linguaggio macchina) e unirci i sottoprogrammi standard del linguaggio (operazione di link), dopo queste due operazioni viene creato un programma in linguaggio macchina eseguibile con estensione
4
Corso sul Linguaggio C++ Edizione 2010
.exe, questo secondo programma può essere mandato in esecuzione. Per eseguire la compilazione e il linking bisogna scegliere Compile dal menù Execute. Se non vengono trovati errori verrà creato il programma eseguibile con lo stesso nome del programma sorgente ma con estensione “.exe”. Nella scheda Compiler che si trova nel riquadro posto nella parte inferiore della finestra del Dev-C++, vengono visualizzati
i messaggi del compilatore. Se la compilazione è andata a buon fine verrà viaulizzato il messaggio <nome
programma> compiled successfully, altrimenti vengono visualizzati gli errori trovati con l’indicazione
della riga dove sono stati trovati;
3. Esecuzione. Scegliere Run dal menù Execute. Viene aperta la finestra di esecuzione dove va l’output del
programma e il programma viene eseguito. Al termine la finestra di esecuzione viene chiusa automaticamente.
Per esempio provate a scrivere il seguente programma:
Vediamo che cosa significa quello che abbiamo scritto. Dalla parola main, seguita da parentesi tonda aperta e
chiusa, inizia l'esecuzione del programma. Il corpo del programma, che comincia dalla parentesi graffa aperta e
finisce alla parentesi graffa chiusa, è composto da una serie di istruzioni. Le istruzioni cout << …. sono operazioni che riguardano l’invio di stringhe (sequenze di caratteri racchiuse fra doppi apici) verso una unità di output:
nel nostro caso il monitor. Nell’esempio proposto cout sta ad indicare il canale di output e il simbolo << è
l’operatore di output. L’istruzione cout << “abc”; si potrebbe così tradurre: inserisci nel canale di output la
stringa specificata. Per quanto riguarda l’instradamento verso il canale, per il momento, si può pensare come ad
una conduttura che porta al monitor e nella quale si inseriscono una di seguito all’altra le stringhe specificate.
L’istruzione system("PAUSE") impedisce alla schermata di esecuzione di scomparire senza darci il tempo di
vedere il risultato del nostro programma, questa istruzione manda in esecuzione il comando PAUSE del sistema
operativo. Questo comando invia nel canale di output la stringa “Premere un tasto per continuare …” e aspetta
fino a quando non viene premuto un tasto. Se aprite una finestra di comandi
(StartProgrammiAccessoriPrompt dei comandi) potete provare a scrivere il comando PAUSE e vederne
l’effetto. Con la funzione system si può mandare in esecuzione qualsiasi comando del sistema operativo o programma. Per esempio:
• system (“CLS”) pulisce la finestra di output (CLS=Clear Screen).
5
Corso sul Linguaggio C++ Edizione 2010
•
system(“Color 1F”) imposta il colore blu come sfondo e il colore bianco come primo piano (il comando generale è color xy dove x e y sono due cifre esadecimali con x=colore di sfondo e y=colore di primo piano)
Per utilizzare la funzione system nei nostri programmi dobbiamo inserire all’inizio
#include <stdlib.h>
Ogni istruzione deve terminare con un carattere di punto e virgola. Se paragoniamo un programma ad un testo, le
istruzioni sono le frasi e ogni istruzione è costituita da una serie di parole dette token separate le une dalle altre
da spazi o simboli (per esempio la punteggiatura). Così come il significato di una frase non cambia in base al
numero di spazi tra le parole, anche il numero di spazi inserito tra le parole di una istruzione C++ è irrilevante,
mentre cambia il significato della frase se si inserisce uno spazio tra i caratteri di una parola spezzandola.
Per poter utilizzare cout, come le altre funzioni di entrata/uscita, si deve inserire all'inizio del testo la direttiva al
preprocessore
#include <iostream.h>
che avverte il compilatore di includere i riferimenti alla libreria dei canali standard di input/output (iostream sta
per canali di input/output). Il C++ è un linguaggio case-sensitive ovvero distingue tra lettere maiuscole e minuscole; dunque occorre fare attenzione, se si scrive MAIN() o Main() non si fa riferimento a main().
Proviamo a compilare ed eseguire il programma che abbiamo scritto, se non ci sono errori nella finestra di esecuzione compare la scritta :
abcdefghilmnopqrstuvzPremere un tasto per continuare …
Se si desidera che ogni stringa venga prodotta su una linea separata, si deve inserire \n nel canale subito dopo la
stringa e prima della chiusura dei doppi apici (\n è un carattere speciale che fa andare a capo il cursore sul monitor, si tratta del carattere new line), come nel seguente listato.
#include <iostream.h>
#include <stdlib.h>
main(){
cout << "abc"
<< “\n”;
cout << "def"
<< “\n”;
cout << "ghi"
<< “\n”;
cout << "lmn"
<< “\n”;
cout << "opqrs" << “\n”;
cout << "tuvz" << “\n”;
system(“PAUSE”);
}
Eseguendo il programma si otterrà la visualizzazione delle seguenti stringhe di caratteri
abc
def
ghi
lmn
opqrs
tuvz
Premere un tasto per continuare ….
In questo caso la prima stringa (abc) viene stampata su video a partire dalla posizione attuale del cursore. Se si
vuole cominciare la stampa delle stringhe da una nuova riga basta inserire \n anche all’inizio o in qualsiasi punto
anche all’interno della stringa da stampare come nei seguenti esempi:
cout << “\n” << “abc” << “\n”;
cout << “\nabc\ndef”;
6
Corso sul Linguaggio C++ Edizione 2010
Qui prima si passa ad una nuova riga, poi si stampa la stringa specificata e quindi si posiziona il cursore in una
nuova riga. In generale è bene tenere presente che l’effetto di ogni cout è quello di stampare a partire dalla posizione in cui si trovava il cursore (in generale a destra dell’ultima stampa). Per poter modificare tale comportamento è necessario inserire gli opportuni caratteri di controllo. In effetti la sequenza \n corrisponde ad un solo
carattere, quello di nuova linea (newline). Esistono altri caratteri di controllo, tutti vengono indicati con una sequenza che inizia con la barra rovesciata(\), di seguito vengono riportati quelli che hanno utilizzo più frequente:
Tabella 1. Sequenze di escape
Sequenza
Effetto
porta il cursore all’inizio della riga successiva (ASCII 10 = new line)
porta il cursore al prossimo fermo di tabulazione (ogni fermo di tabulazione è fissato ad 8 caratteri) (ASCII
9)
\a
(alert) fa un beep
\f
(form feed) nuova pagina (ASCII 12)
\r
(cariage return) ritorno del carrello nella stampa (ASCII 13)
\’
stampa un apice
\”
stampa le virgolette
\\
barra rovesciata
\?
Punto di domanda
\b
(Bakspace) Indietro di un carattere. (ASCII 8)
\<cifreOttali>
Il carattere corrispondente nel codice ASCII al numero ottale
\x<cifreEsadec> Il carattere corrispondente nel codice ASCII al numero esadecimale
\n
\t
La sequenza costituita dalla barra rovesciata seguita da uno o più caratteri viene detta sequenza di escape e corrisponde ad un solo carattere. Utilizzando una sequenza di escape si può inserire anche il carattere corrispondente ad un qualsiasi codice ASCII in due modi: utilizzando la barra rovesciata (\) seguita dal codice ASCII scritto
in ottale oppure la barra rovesciata e la x (\x) seguita dalle cifre esadecimali del codice ASCII (per esempio \x41
corrisponde al carattere ‘A’). Quando si fanno delle modifiche al programma sorgente per provarlo bisogna
compilarlo ed eseguirlo ma attenzione a chiudere l’eventuale finestra di un esecuzione precedente perché, se è
ancora aperta, il programma eseguibile è bloccato e il compilatore non può sostituirlo con quello nuovo. Purtroppo, il dev-c++ non segnala nessun errore in questo caso, semplicemente rimanda in esecuzione il vecchio
programma.
1.6
le direttive al preprocessore
A differenza di altri linguaggio, la compilazione di un programma in linguaggio C prevede una prima fase detta
precompilazione, svolta da un apposito modulo detto preprocessore (che non è un dispositivo hardware ma solo una particolare funzione del programma compilatore). Il Preprocessore è normalmente incluso nel compilatore. Nella fase di precompilazione il programma viene preparato per la successiva compilazione.
E’ possibile inserire in un programma C delle istruzioni che verranno eseguite dal preprocessore, queste istruzioni sono dette direttive al preprocessore o dichiarative di precompilazione, tutte le direttive al preprocessore
hanno la seguente struttura:
#<nome direttiva> <dichiarazione della direttiva>
con i grafi sintattici:
direttive al preprocessore
#
nome direttiva
dichiarazione direttiva
Una delle direttive più utilizzate è la direttiva include per esempio:
#include <iostream.h>
7
Corso sul Linguaggio C++ Edizione 2010
oppure
#include “iostream.h”
Questa direttiva dice al preprocessore di inserire nel file sorgente il file “iostream.h” che è un file che contiene i
riferimenti alle funzioni standard di Input e Output. Se si usano le parentesi angolari (<…>) il preprocessore ricercherà il file nelle cartelle di sistema (che si possono modificare intervenendo nella scheda Directories visualizzata con la voce di menù OptionsCompiler Options), se si usano i doppi apici cercherà prima nella cartella
corrente. Se invece di inserire la direttiva si copiasse il contenuto del file “iostream.h” nel programma sorgente il
risultato della compilazione sarebbe lo stesso. I file con estensione .h sono file di intestazione e contengono le
dichiarazioni necessarie per richiamare, all’interno del codice sorgente, i sottoprogrammi inclusi in librerie esterne. Molte librerie di sottoprogrammi sono incluse nell’ambiente di sviluppo e, inoltre, qualsiasi programmatore può creare ulteriori librerie personali. Il programmatore che ha creato la libreria deve anche creare uno o più
file di intestazione che dovranno essere inclusi nei programmi che utilizzeranno la libreria.
Con l’introduzione nel linguaggio C++ di nuove funzionalità, come i template e i namespace, che non esistevano nel linguaggio C, le librerie standard sono state riscritte. Nei programmi C++, comunque, si possono usare sia
le vecchie versioni delle librerie che le nuove. In genere i file da includere per le nuove versioni hanno lo stesso
nome dei vecchi con una ‘c’ davanti e senza l’estensione .h. Per esempio <stdio.h> diventa <cstdio> e <stdlib.h>
diventa <cstdlib>. Se si decide di utilizzare le nuove versione delle librerie bisogna anche specificare che nel
programma si userà il namespace std che è quello in cui sono inclusi tutti i nomi di funzione e gli identificatori
delle librerie standard. Per esempio si può scrivere:
#include <cstdlib>
#include <iostream>
using namespace std;
invece di
#include <stdlib.h>
#include <iostream.h>.
Attenzione, il nuovo file <iostream> richiama al suo interno tutte le librerie standard (quelle che iniziano con
“st”) per cui nella maggior parte dei programmi si può anche includere solo la libreria <iostream>:
#include <iostream>
using namespace std;
Nel seguito ho utilizzato la vecchia sintassi che è compatibile con tutte le versioni del linguaggio.
1.7
Le funzioni di I/O
Fra le istruzioni più importanti di un programma ci sono quelle di input/output cioè quelle che servono per far
leggere i dati da elaborare (input) e visualizzare i risultati dell’elaborazione (output). Per ora utilizzeremo due
sole istruzioni con sintassi semplificata, l’istruzione cout, che abbiamo già visto, per l’output e l’istruzione cin
per l’input. La sintassi è la seguente:
per l’output
cout
<<
espressione
per l’input
cin
>>
variabile
cin sta per console input e dice al computer mandare i dati provenienti dall’unità di input standard (normalmente
la tastiera) dentro una o più variabili (se ci sono più variabili, durante l’esecuzione bisogna inserire più dati con
8
Corso sul Linguaggio C++ Edizione 2010
un ritorno a capo oppure uno spazio tra uno e l’altro). Il simbolo >> può essere tradotto con “memorizza in” e
dice al computer di prelevare un dato dalla console di input e memorizzarlo nelle variabile.
1.8
Il debug dei programmi con Dev C e Dev Pas
Dopo aver installato il compilatore C o pascal e il debugger seguendo le note di installazione riportate sopra,
normalmente il compilatore viene impostato automaticamente per generare un programma eseguibile il più possibile efficiente. Purtroppo per poter eseguire il debug è necessario inserire nel programma eseguibile riferimenti
al programma sorgente che aumentano le dimensioni del file eseguibile e lo rendono meno efficiente. Quando il
compilatore viene installato viene automaticamente impostato per non inserire queste informazioni nel file eseguibile e quindi il debug non funziona!
Per farlo funzionare bisogna seguire la seguente procedura:
1. Scegliere Options - Compiler options dal menu
2. Spuntare la casella "Generate debugging information" dalla scheda "Linker" della finestra delle opzioni
del compilatore e fare click su OK
3. Compilare il programma (ricompilare se era stato già compilato) ed eseguire il programma facendo
click sull'apposita icona della barra degli strumenti oppure sulla voce del menù Execute oppure con F8.
9
Corso sul Linguaggio C++ Edizione 2010
4. Comparirà la finestra del debug con una copia del programma sorgente. Facendo click su Run (o sull'icona corrispondente della barra degli strumenti
) il programma si avvierà, comparirà la finestra
del programma (la console per i programmi non windows) e la prima istruzione eseguibile verrà evidenziata nella finestra del debug.
5.
Dalla finestra del sorgente è possibile:
inserire e togliere punti di interruzione facendo clik con il mouse a sinistra dell'istruzione corrispondente (un punto di interruzione viene visualizzato con un quadratino nero);
eseguire un'istruzione alla volta facendo click su l’icona Step (
) della barra degli strumenti oppu-
re su Next (
), la differenza tra le due si nota solo se l’istruzione è una chiamata ad un sottoprogramma (con Step si passa alla prima istruzione del sottoprogramma, con Next verrà eseguito l’intero
sottoprogramma e si passa all’istruzione successiva);
far proseguire il programma fino al prossimo punto di interruzione (
cali (
), visualizzare le variabili lo-
), ecc.
Note per l’esecuzione del debugger
Se si vuole eseguire un programma utilizzando il debug bisogna avere le seguenti accortezze:
1. la cartella che contiene il file sorgente deve trovarsi sullo stesso volume dove è installato il dev-pas o il
dev-c;
2. prima di eseguire il debug accertarsi di aver compilato il programma con l’opzione “Generate debug-
10
Corso sul Linguaggio C++ Edizione 2010
3.
ging information” attivabile dal menù OptionsCompiler Options;
se si utilizzano dei file in input questi devono trovarsi nella cartella BIN contenuta nella cartella di installazione del del C (per es: C:\Dev-C++\Bin).
Note per la versione 4.9.9.2
L’ultima versione del DEV C, presenta dei bug per quanto riguarda i nomi dei file. Se si salva il programma con
nomi di file lunghi che contengono caratteri speciali come spazi e parentesi tonde, possono verificarsi problemi.
Per esempio se si salva un programma con il nome “Prova programma” e poi si salva nella stessa cartella un altro programma con nome “Prova programma (1)” quando si manda in esecuzione il secondo parte l’eseguibile
del primo invece del secondo.
Si raccomando di usare nomi corti e non contenenti caratteri speciali.
11
Corso sul Linguaggio C++ Edizione 2010
12
2
Capitolo
Corso sul Linguaggio C++ Edizione 2010
2. Tipi di dati, variabili e costanti, le espressioni
2.1
Calcolo dell’area di un rettangolo
Un qualsiasi programma, in genere, legge dei dati e produce dei risultati. Tutti i dati utilizzati, sia quelli iniziali
che quelli intermedi e finali vengono conservati in memoria. I dati di un programma si distinguono in due categorie: variabili e costanti. I dati variabili sono quelli che possono cambiare da un’esecuzione all’altra dello stesso
programma o durante una stessa esecuzione, i dati costanti, invece, assumono sempre lo stesso valore ogni volta
che si esegue il programma. Supponiamo di voler calcolare l'area di un rettangolo i cui lati hanno valori interi.
Entrano in gioco due variabili, la base e l'altezza, il cui prodotto è ancora un valore intero, l'area appunto. Il programma potrebbe essere il seguente:
#include <iostream.h>
#include <stdlib.h>
// Calcolo area di un rettangolo
int main()
{
int base, altezza; int area;
cout << "Calcolo AREA RETTANGOLO \n \n";
cout << "Valore base: "; cin >> base;
cout << "\nValore altezza: "; cin >> altezza;
area = base*altezza;
cout << "\nBase: " << base << " Altezza: " << altezza;
cout << "\nArea: " << area << "\n\n";
system("PAUSE");
return 0;
}
Per rendere evidente la funzione espletata dal programma si è inserito un commento:
// Calcolo area rettangolo
Il doppio simbolo // indica inizio di commento. Tutto ciò che segue fino alla fine della riga non viene preso in
considerazione dal compilatore: serve solo a scopo documentativo. I commenti possono estendersi su più linee e
apparire in qualsiasi parte del programma. Naturalmente, per quanto notato prima, il doppio // deve precedere
ogni commento in una nuova riga. Se il commento si estende su più righe, può essere più comodo adottare un
altro sistema: fare precedere il commento da /* e inserire */ alla fine del commento. Tutto ciò che appare nelle
zone così racchiuse non viene preso in considerazione dal compilatore e non ha alcuna influenza sul funzionamento del programma, è però importantissimo per chi legge il programma: è infatti nelle righe di commento che
13
Corso sul Linguaggio C++ Edizione 2010
viene specificato il senso delle istruzioni che seguiranno. Cosa, questa, non immediatamente comprensibile se si
leggono semplicemente le istruzioni del linguaggio.
Subito dopo main() sono presenti le dichiarazioni delle variabili intere necessarie:
int base, altezza; int area;
La parola chiave int specifica che l'identificatore che lo segue si riferisce ad una variabile di tipo intero; dunque
base, altezza e area sono variabili di questo tipo. Anche le dichiarazioni così come le altre istruzioni devono terminare con un punto e virgola. Nel nostro esempio alla dichiarazione del tipo della variabile corrisponde anche
la sua definizione che fa sì che le venga riservato uno spazio in memoria centrale. Il nome di una variabile la
identifica, il suo tipo ne definisce la dimensione e l'insieme delle operazioni che vi si possono effettuare. Le dichiarazioni delle variabili dello stesso tipo possono essere scritte in sequenza separate da una virgola:
int base, altezza, area;
Dopo la dichiarazione di tipo sono specificati gli identificatori di variabile, che possono essere in numero qualsiasi, separati da virgola e chiusi da un punto e virgola.
L’istruzione cout << "Calcolo AREA RETTANGOLO \n \n"; visualizza su video la stringa di caratteri "Calcolo
AREA RETTANGOLO” seguita da due righe vuote.
Le due istruzioni
cout << "Valore base: ";
cin >> base;
servono per leggere il valore della base. Prima viene visualizzata la frase “Valore base: “, poi
il sistema aspetta che l’operatore batta un numero sulla tastiera e lo trasferisce nella variabile
base.
La forma grafica data al programma è del tutto opzionale; una volta rispettata la sequenzialità
e la sintassi, la scrittura del codice è libera. In particolare più istruzioni possono essere scritte
sulla stessa linea, si possono inserire spazi e righe vuote dove si desidera. Non bisogna dimenticare, però, di inserire il carattere ; (punto e virgola) dopo ogni istruzione. Lo stile grafico facilita enormemente il riconoscimento dei vari pezzi di programma e consente una diminuzione di tempo nelle modifiche, negli ampliamenti e nella correzione degli errori.
Analogamente la riga successiva serve per far leggere il valore dell’altezza e inserirlo nella
variabile altezza.
L’istruzione
area = base*altezza;
è un’istruzione di assegnazione. Essa dice al computer di calcolare il valore dell’espressione a
destra dell’uguale e assegnare il risultato alla variabile a sinistra del simbolo =; cioè inserisce
nello spazio di memoria riservato a tale variabile il valore indicato.
L'operatore asterisco effettua l'operazione di prodotto tra la variabile che lo precede e quella
che lo segue, è dunque un operatore binario.
Nel linguaggio C++ è possibile assegnare lo stesso valore a più variabili contemporaneamente. Per esempio
base = altezza = 5;
14
Corso sul Linguaggio C++ Edizione 2010
In questo caso prima verrebbe assegnato il valore 5 alla variabile altezza e quindi, il risultato
dell’assegnazione (cioè 5), viene assegnato alla variabile base.
Le ultime istruzioni visualizzano su video i valori delle variabili in input e il risultato.
2.2
Definizione di variabili
Tutte le variabili utilizzate in un programma devono essere preventivamente definite. La definizione di una variabile è un’istruzione che serve per creare la variabile in memoria.
In generale la definizione di variabili ha la seguente forma:
,
Modificatore della
modalità di memorizzazione
Modificatore di tipo
Tipo di dato
Identificatore
=
Valore iniziale
Opzionalmente si può premettere ad una definizione di variabile la modalità di memorizzazione che indica al computer come e dove memorizzare la variabile.
Le modalità di memorizzazione sono le seguenti:
Modalità di memorizzazione Dichiara al compilatore che l’elemento va memorizzato …..
auto
Nelle celle della memoria centrale dell’elaboratore (è quella di
default)
register
Negli elementi hardware dell’elaboratore che costituiscono la
memoria più veloce. In genere i compilatori cercano di memorizzare queste variabili nei registri interni della CPU o nella
memoria cache.
extern
È definito in un altro file sorgente
static
Nella memoria centrale ma il valore non viene mai cancellato
durante l’esecuzione del programma
volatile
Nella memoria centrale, ma il valore può essere modificato, oltre che dal programma anche direttamente dall’hardware. Questa
modalità viene utilizzata in casi particolari come quando si devono gestire i dati provenienti o da inviare ad un MODEM o ad
altre periferiche.
Per esempio:
static int a
dice al computer di creare una variabile di tipo intero in uno spazio che non deve essere cancellato quando il sottoprogramma che contiene la definizione termina (normalmente tutte le
variabili di un sottoprogramma vengono cancellate quando il sottoprogramma termina
l’esecuzione).
Oltre alla modalità di memorizzazione si può inserire in una definizione di variabile un modificatore di tipo, i principali modificatori di tipo sono i seguenti:
modificatore di tipo
const
Descrizione
Indica al compilatore che il dato definito è un dato costante che
quindi non potrà subire variazione durante l’esecuzione del pro15
Corso sul Linguaggio C++ Edizione 2010
signed
unsigned
Short
Long
gramma
Il valore è un numero con il segno
Il valore è un numero senza segno
Riduce il numero di byte riservati a contenere il dato
Aumenta il numero di byte riservato a contenere il dato
Per esempio la definizione
short int num;
dichiara una variabile che occupa solo due byte (numeri da -32768 a + 32767);
short unsigned int num;
dichiara una variabile sempre di due byte ma senza segno e quindi i valori possono andare da
0 a 65536
Il tipo di dato indica al sistema la dimensione della variabile e l'insieme delle operazioni che
si possono effettuare su di essa. La dimensione può variare rispetto all'implementazione; molte versioni del C++, come quelle sotto il sistema operativo MS DOS per esempio, riservano
per gli int uno spazio di due byte, il che permette di lavorare su interi che vanno da -32768 a
+32767, altre implementazioni, come per esempio quelle per ambiente Windows, riservano
uno spazio di quattro byte permettendo valori compresi fra -2.147.483.648 e +2.147.483.647.
Tra le operazioni permesse fra int vi sono: la somma (+), la sottrazione (-), il prodotto (*) e la
divisione (/). Nel seguito esamineremo i principali tipi di dato.
Ogni variabile deve avere un nome che identifica la variabile all’interno del programma e per
questo viene detto identificatore. Esistono delle regole da rispettare nella costruzione degli
identificatori: devono iniziare con una lettera o con un carattere di sottolineatura _ e possono
contenere solo lettere, cifre e _ (non possono contenere simboli di punteggiature, segni di operazione e altri caratteri speciali). Per quanto riguarda la lunghezza occorre tenere presente
che soltanto i primi trentadue caratteri sono significativi, anche se nelle versioni del C meno
recenti questo limite scende a otto caratteri. Sarebbe comunque opportuno non iniziare il nome della variabile con il carattere di sottolineatura ed è bene tenere presente che le lettere accentate, permesse dalla lingua italiana, non sono considerate lettere ma segni grafici e le lettere maiuscole sono considerate diverse dalle rispettive minuscole.
Oltre a rispettare le regole precedentemente enunciate, un identificatore non può essere una
parola chiave del linguaggio, né può essere uguale ad un nome di funzione in libreria o scritta
dal programmatore.
In generale è inoltre bene dare alle variabili dei nomi significativi, in modo che, quando si
debba intervenire a distanza di tempo sullo stesso programma, si possa facilmente ricostruire
l'uso che si è fatto di una certa variabile.
A differenza di altri linguaggi di programmazione, nel linguaggio C, l’istruzione di definizione di una variabile può essere utilizzata anche per inizializzarla ovvero per assegnargli il valore iniziale. Per esempio:
int somma = 0, prod=1;
crea la variabile somma e ci mette dentro il valore 0, poi crea la variabile prod e ci mette 1.
16
Corso sul Linguaggio C++ Edizione 2010
2.3
Costanti
Nel paragrafo 2.1 abbiamo esaminato il programma per calcolare l’area di un rettangolo. Supponiamo ora di voler scrivere un programma per calcolare l’area del cerchio che sappiamo si
calcola con la formula area = πr2, dove π è un numero fisso che corrisponde approssimativamente a 3,14. Quindi per calcolare l’area del cerchio bisogna indicare al computer anche il valore di π che è un dato costante del problema. Il programma potrebbe essere il seguente:
#include <iostream.h>
#include <stdlib.h>
#define PIGRECO 3.14
// Calcolo area di un cerchio
int main()
{
float raggio, area;
cout << "Calcolo AREA Cerchio \n \n";
cout << "Raggio: "; cin >> raggio;
area = PIGRECO*raggio*raggio;
cout << "\nArea: " << area << "\n\n";
system("PAUSE");
return 0;
}
In questo caso le variabili raggio e area sono state dichiarate di tipo float perché questo tipo
può contenere anche numeri con la virgola, inoltre è stata inserita la direttiva al preprocessore
#define
PIGRECO 3.14
Questo dice al preprocessore che ogni volta che trova la serie di caratteri PIGRECO deve sostituirli con il numero 3.14. In effetti se nel programma sorgente togliamo la direttiva define e
sostituiamo l’istruzione area=PIGRECO*raggio*raggio con
area = 3.14 * raggio * raggio
il programma eseguibile sarebbe esattamente lo stesso.
In pratica una costante è una valore fisso nel programma e noi possiamo indicarlo nelle istruzioni o scrivendo il valore o definendo un nome simbolico che rappresenta il valore.
I valori costanti che si possono inserire in un programma C++ possono essere di vario tipo:
• Numeri decimale interi (per esempio 10, 30, -56, 4545445 …)
• Numeri ottali interi (per esempio 064, 010, … ) iniziano sempre con uno zero e contengono le cifre da 0 a 7;
• Numeri esadecimali interi (0x23F, 0x30, 0xBB …) iniziano sempre con 0x e contengono le cifre da 0 fino a F;
• Numeri decimali con la virgola o in virgola mobile (per esempio 3.14, -2.0, -0.1,
6.567E+7, 0.450123e-3 …). Per rappresentare la virgola si usa il punto;
• Un carattere singolo (per esempio ‘a’, ‘B’, ‘7’…), deve essere racchiuso tra due apici;
• Una stringa di caratteri (“Mario”, “Buongiorno mondo”, ….), la stringa è un gruppo di
caratteri racchiuso tra virgolette (“); tutte le stringhe terminano con il carattere speciale \0 (codice ascii 0) che viene inserito automaticamente dal compilatore;
17
Corso sul Linguaggio C++ Edizione 2010
Quando un certo valore viene utilizzato in modo ricorrente è opportuno rimpiazzarlo con un
nome simbolico; inoltre spesso conviene definire le costanti per aumentare la leggibilità di un
programma perché in alcuni casi il nome di un dato indica più chiaramente, a chi legge il programma, il significato dell’istruzione. Per esempio se abbiamo codificato le mansioni svolte
dagli impiegati di una azienda con dei numeri e 1 significa operaio, 2=impiegato, 9=dirigente
l’istruzione
mansione=DIRIGENTE
è più chiara di
mansione =9
per sostituire la costante 9 con la parola DIRIGENTE dobbiamo inserire la direttiva
#define DIRIGENTE 9
nel programma prima dell’uso della costante.
Il nome di una costante può essere qualsiasi identificatore valido in C++, comunque è uso
comune utilizzare esclusivamente caratteri maiuscoli per le costanti e caratteri minuscoli per
le variabili per distinguere chiaramente le une dalle altre.
In sintesi, l'uso delle costanti migliora due parametri classici di valutazione dei programmi:
flessibilità e possibilità di manutenzione.
Con il linguaggio C++, oltre alla direttiva define (presente nel linguaggio C) per definire le
costanti si può utilizzare il modificatore di tipo const e quindi si può anche scrivere:
const float PIGRECO=3.14;
2.4
I tipi di dati e le operazioni permesse
Una delle caratteristiche che differenziano il linguaggio C rispetto agli altri linguaggi è la
maggiore disponibilità di tipi di dati e di operatori. Per quanto riguarda i tipi di dati abbiamo
già visto la possibilità di utilizzare i modificatori di tipo e di modalità di memorizzazione che
di fatto amplia la gamma dei tipi disponibili.
Negli esempi che abbiamo visto finora abbiamo utilizzato due tipi di dato, numeri interi (tipo
int) e numeri con la virgola (tipo float). Ogni linguaggio di programmazione mette a disposizione dei programmatori un insieme di tipi di dati che il programmatore può utilizzare come
contenitori per i dati dei suoi programmi.
Nel linguaggio C++ i tipi di dato fondamentali sono:
18
Corso sul Linguaggio C++ Edizione 2010
void
bool
elementari
char
int
float
Tipi di dato
double
enum
array
strutturati
strutture
union
puntatori
Il tipo int
Il tipo int viene utilizzato per memorizzare numeri interi relativi. Il numero viene scritto nella
memoria del computer in binario, i numeri negativi (non ammessi se si utilizza il modificatore
di tipo unsigned) vengono rappresentati con il complemento a 2. A seconda del modificatore
di tipo utilizzato abbiamo:
short int
signed short int
unsigned short int
long int
signed long int
Unsigned long int
int
numeri da - 32768 a +32 767, occupa 2 byte,
Numeri da 0 a 65535 occupa 2 byte
numeri da -2147483648 a +2147483647 occupa 4 byte
Numero da 0 a 4294967295, 4 byte
La dimensione può coincedere con short int o con long int a
seconda dell’implementazione. In genere quelle sotto il sistema MS-DOS corrispondevano a short int, quelle sotto
Windows come quella che utilizziamo noi (Dev-C++) corrispondono a long int
Le operazioni permesse sono: la somma (+), la sottrazione (-), il prodotto (*), e la divisione
(/), il modulo o resto della divisione (%), l’assegnazione (=), l’incremento (++) e il decremento (--), oltre alle combinazioni delle quattro operazioni con l’assegnazione (+=, -=, *=,
/=).
19
Corso sul Linguaggio C++ Edizione 2010
Il tipo char
Una variabile di tipo char può memorizzare un solo carattere. In realtà nella memoria viene
memorizzato il codice ASCII del carattere e quindi un numero intero da 0 a 255. Per questo
motivo una variabile char occupa un solo byte ed è compatibile con il tipo intero. Per questo
tipo di variabili sono definite tutte le operazioni definite per i numeri interi, in più è possibile
assegnare alla variabile anche un singolo carattere, per esempio, visto che il codice ASCII
della lettera ‘B’ è 66, le due istruzioni seguenti sono equivalenti::
e
char lett=’B’;
char lett = 66;
Una variabile di tipo char può essere utilizzata anche per contenere numeri interi senza segno
da 0 a 255 oppure numeri interi con segno da -128 a + 127, a seconda se si definisce come
unsigned char o signed char ed è compatibile con i tipi numerici, ma se viene inviata in
output con cout verrà visualizzato sempre il carattere corrispondente al codice ASCII memorizzato e non il numero.
Come detto prima sulle variabili di tipo char sono definite tutte le operazioni definite con il
tipo intero, quindi le quattro operazioni principali (+, -, *, /) ma anche incremento e decremento (++, --), il modulo (%) e le combinazioni delle quattro operazioni con l’assegnazione
(+=, -=. *=, /=). Quindi è corretta sintatticamente un’istruzione del tipo:
lett=lett +2;
Dopo questa operazione, supponendo che la variabile lett sia stata creata con l’istruzione precedente di questo paragrafo, il codice ASCII presente nella variabile viene aumentato di 2 e
quindi la lettera contenuta non sarà più ‘B’ ma ‘D’. Da notare che le costanti di tipo char, ovvero costituite da un solo carattere, si racchiudono tra due apostrofi (‘), mentre le costanti
stringa sono sempre racchiuse utilizzando il doppio apice (“), per esempio ‘A’, ‘k’ sono costanti di tipo char mentre “Mario” è una costante di tipo stringa.
Oltre al tipo char, in molte versioni del linguaggio, è stato inserito anche il tipo wchar_t che
occupa 2 bytes (16 bit) invece di uno e viene utilizzato per memorizzare i caratteri con il codice Unicode invece del codice ASCII. Il codice Unicode permette di rappresentare un numero di caratteri molto maggiore di 255, praticamente tutti i caratteri esistenti nelle lingue del
mondo.
I tipi float e double
I due tipi float e double servono per memorizzare i numeri reali. La rappresentazione interna è quella
binaria virgola mobile. Ogni variabile di tipo float occupa 4 bytes e può contenere numeri fino a 10±38
ma di tutte le cifre del numero solo le sette più significative sono memorizzate esattamente. Il tipo
double occupa 8 bytes, la rappresentazione interna è sempre binario virgola mobile, può memorizzare
numeri fino a 10±308 con esatte le 15 cifre più significative. Per questi tipo non si può utilizzare i modificatori short,long, signed e unsigned, in pratica i numeri vengono memorizzati sempre con il segno e il tipo
float corrisponde a un ipotetico short double, solo per il tipo double si può anche utilizzare il modificatore
long con il DEV-C++, le variabili di tipo long double occupano 12 bytes.
Il fatto che i numeri sono memorizzati in binario virgola mobile permette di memorizzare numeri molto grandi e anche molto piccoli in modo abbastanza preciso e in poco spazio ma crea anche un problema, infatti, i numeri frazionari che in decimale hanno un numero finito di cifre spesso in binario diventano periodici e quindi con infinite cifre, per questo motivo anche un numero come 0.1 non può
essere memorizzato esattamente in questo tipo di variabili.
20
Corso sul Linguaggio C++ Edizione 2010
Il tipo bool
Le variabili di tipo bool occupano un solo byte e possono contenere solo i due valori: booleani true e
false. Per questo tipo sono definite solo le operazioni logiche ovvero && (AND), || (OR), ! (NOT).
È importante notare che queste operazioni sono diverse dalle corrispondenti operazioni che operano
sui bit e che si applicano a tutte i tipi di variabili ovvero & (AND), | (OR), e ~(inversione dei bit).
Un esempio di dichiarazione di variabile bool è il seguente:
bool finito = false;
Nel linguaggio C il tipo bool non esiste, in sua vece si può utilizzare il tipo int infatti, come qualsiasi altro dato, anche il risultato di un espressione logica è un numero binario e precisamente il numero 0
corrisponde al valore falso mentre il numero 1 o un numero diverso da zero corrisponde al valore vero. Questo significa che, sia nel linguaggio C che nel linguaggio C++, si possono utilizzare come variabili logiche anche le variabili intere.
L’operatore speciale sizeof
Per ottenere esattamente il numero di byte occupati da una variabile o da un tipo di dati si può ricorrere all’operatore speciale sizeof ovvero “misura di”. La sintassi è la seguente:
sizeof(<TipoDato>)
oppure
sizeof(<NomeVariabile>)
L’esempio che segue è un programma che mostra la quantità di memoria occupata da alcune variabili
elementari.:
#include <iostream.h>
#include <stdlib.h>
int main()
{
/* visualizza il numero di bytes assegnati in memoria ad alcune
variabili */
int i; short int si; long int li;
char c; wchar_t wc; float f; bool b;
double d; long double ld;
cout << "bytes occupati da char: " << sizeof(c)<< "\n";
cout << "bytes occupati da float: " << sizeof(f)<< "\n";
cout << "bytes occupati da double: " << sizeof(d)<< "\t long double: "
<< sizeof(ld) <<"\n";
cout << "bytes occupati da bool: " << sizeof(b)<< "\n";
cout << "bytes occupati da int: " << sizeof(i)<< "\t long int: " <<
sizeof(li) << "\t short int: " << sizeof(si)<<"\n";
cout << "bytes occupati da wchar_t: " << sizeof(wc) << "\n" ;
system("PAUSE");
return 0;
}
Mandandolo in esecuzione con il DEV-C++ si otterrà questo risultato:
21
Corso sul Linguaggio C++ Edizione 2010
2.5
Le espressioni
Un espressione è una sequenza di variabili, costanti e operatori, eventualmente racchiusi
a gruppi tra parentesi tonde () per modificare l’ordine dei calcoli.
Per esempio:
x = a * 3 / (somma - 5.3)
Nell’esempio x, a e somma sono variabili, 3 e 5.3 sono costanti, gli altri segni, escluse le parentesi che servono solo per modificare l’ordine dei calcoli, sono operatori. Un’espressione
può anche essere definita come una sequenza di operazioni elementari dove ogni operazione è
definita da un segno detto operatore e da uno o più valori che possono essere variabili o costanti detti operandi.
Nel linguaggio C sono definiti molti più operatori rispetto agli altri linguaggi. Le espressioni
hanno molte più possibilità e questa è una delle caratteristiche che lo rende preferibile per alcune applicazioni. Per esempio sono stati introdotti operatori come quello dell’incremento o
decremento di una variabile che, in genere, sono presenti in tutti i linguaggi macchina ma non
nei linguaggi ad alto livello. In questi linguaggi operazioni come a++ o a-- del linguaggio C,
vengono sostituite con istruzioni del tipo a=a+1 o a = a-1, le quali però, tradotte in linguaggio
macchina, in genere, danno luogo a programmi meno efficienti.
Gli operatori del linguaggio C++ si possono raggruppare nelle seguenti categorie:
• Operatori di assegnamento
• Operatori aritmetici
• Operatori relazionali
• Operatori logici
• Operatori sui bit
• Operatori speciali
Gli operatori di assegnamento
L’operatore principale di assegnamento è il carattere =. Per esempio:
base = 3;
Con questa istruzione viene inserito 3 all’interno della variabile con nome ‘base’, questo significa che si inserisce il numero 3 nello spazio di memoria riservato alla variabile base.
x = y;
In questo caso il valore della variabile y vene copiato nella variabile x, le due variabili continueranno ad occupare spazi di memoria separati e indipendenti.
Nella maggior parte dei casi l’istruzione di assegnamento ha la seguente sintassi:
<identificatore> = <espressione>
e serve per inserire il risultato dell’espressione a destra del simbolo = nella variabile con nome <identificatore>.
Per il linguaggio C++ questa è un’operazione che produce un valore come risultato. Il valore
risultante corrisponde al valore assegnato alla variabile.
Per questo motivo, in questo linguaggio, sono possibili le assegnazioni multiple del tipo:
prezzo = importo = q * pu;
in questo caso viene prima calcolato il prodotto delle variabili q e pu, il risultato viene assegnato alla variabile importo, lo stesso risultato viene assegnato anche alla variabile prezzo.
Esistono anche altri operatori di assegnazione, precisamente gli operatori +=, -=, *=, /=, %=,
<<=, >>=, &=, !=, ^=.
22
Corso sul Linguaggio C++ Edizione 2010
Si tratta della combinazione di operazioni aritmetiche o logiche con l’assegnazione. Sono state introdotte in C perché possono essere tradotte in modo efficiente in linguaggio macchina.
Per esempio le istruzioni:
km += 37;
k1 += k2;
a += (b/2);
equivalgono rispettivamente a:
km = km+37;
k1 = k1+k2;
a = a+(b/2);
Chi è pratico di programmazione avrà notato che questa è l’istruzione tipica degli accumulatori.
Ma l’operatore = si può anche utilizzare insieme ad altri operatori aritmetici, per esempio:
km -= 6;
// toglie 6 ai km percorsi ovvero km = km - 6
lato *= 2;
// moltiplica il lato per 2 ovvero lato = lato * 2
volume /= 3; // divide il volume per 3 ovvero volume = volume / 3
quindi, più in generale vale la seguente sintassi:
<identificatore> <operatore>= <espressione>
corrisponde a
<identificatore> = <identificatore> <operatore> <espressione>
La sintassi completa per l’assegnamento è
<lvalue> <operatore di assegnamento> <rvalue>
dove <lvalue> sta per left value,ovvero valore di sinistra e può essere un identificatore ma anche un elemento di un vettore o un’espresssione che da come risultato un puntatore dereferenziato; <operatore di assegnamento> è uno degli operatori che abbiamo visto (=, +=, -=, *=, /=,
%=, <<=, >>=, &=, !=, ^=); <rvalue> (right value) è una qualsiasi espressione che restituisce un valore compatibile con il tipo di <lvalue>.
Operatori aritmetici
Naturalmente sono presenti nel linguaggio C++ gli operatori aritmetici disponibili negli altri
linguaggi per le operazioni fondamentali: addizione (+), sottrazione(-), moltiplicazione (*),
divisione (/), resto della divisione (%). Questi operatori vengono detti binari perché
l’operazione viene compiuta su due operandi, per esempio:
b+c
a * 10
5/3
7-c
in generale per far eseguire l’operazione al computer si scrivono i due operandi con il segno
di operazione (l’operatore) in mezzo. Gli operatori binari non cambiano il valore dei due operandi ma memorizzano il risultato.
L’operazione % corrisponde al resto della divisione e si può eseguire solo con numeri interi,
per esempio: 10 % 3 da come risultato 1 e 20 % 5 da come risultato 0.
Oltre agli operatori visti sopra sono disponibili nel C++ anche l’operatore ++ e -- detti rispettivamente di incremento e decremento. Questi due operatori si applicano ad un solo operando
modificandone il valore per cui vengono detti unari. Il loro effetto è rispettivamente quello di
incrementare o decrementare di 1 il valore dell’operando.
23
Corso sul Linguaggio C++ Edizione 2010
Per esempio dopo l’esecuzione del seguente pezzo di programma:
int a=10, b=-20;
a++; b--;
nella variabile a ci sarà il valore 11 e nella variabile b il valore -21.
Per aggiungere uno alla variabile z si può scrivere in due modi:
++z;
z++;
cioè mettere l'operatore ++ prima o dopo del nome della variabile. Le due notazioni vengono
dette rispettivamente prefissa e postfissa
In generale, le due forme sono equivalenti. La differenza importa solo quando si scrive una
espressione che contiene z++ o ++z.
Scrivendo z++, il valore di z viene prima usato poi incrementato:
int x,z;
z = 4;
x = z++;
// due variabili intere
// z vale 4
// anche x vale 4 ma z vale 5
Difatti, prima il valore di z (4) è stato assegnato ad x, poi il valore di z è stato incrementato a
5.
Scrivendo ++z, il valore di z viene prima incrementato e poi usato:
int x,z;
z = 4;
x = ++z;
// due variabili intere
// z vale 4
// ora x vale 5 come z
Difatti, prima il valore di z (4) è stato incrementato a 5, poi il nuovo valore di z (5) è stato assegnato ad x.
Bisogna stare attenti e cercare di evitare che in una stessa espressione compaia più volte la
stessa variabile con applicate operazioni di incremento o decremento postfisse perché i risultati dipendono da quando il compilatore fa eseguire l’incremento o decremento, per esempio
dopo l’esecuzione delle seguenti istruzioni:
int b=5, a ; a=b++ + b++ ;
in b ci sarà il valore 7 e in a ci sarà il valore 10 perché i due incrementi di b verranno eseguiti
dopo l’addizione, mentre dopo l’esecuzione delle seguenti istruzioni:
int b=5; bool a; a= b++ < b;
in a ci sarà il valore vero perchè il compilatore Dev-C++ fa eseguire l’incremento di b prima
di eseguire il confronto <.
Operatori relazionali
Gli operatori relazionali sono operatori binari, servono per confrontare due valori e producono
un risultato booleano ovvero il valore true o false a seconda se il risultato del confronto è vero
o falso. Sono i seguenti:
>
maggiore
>=
maggiore o uguale
<
minore
<=
minore o uguale
==
uguale
!=
diverso
Per esempio: 5 >= 3 da risultato true; 2 == (10 / 5) da risultato true; 7 != 7 da risultato false;
24
Corso sul Linguaggio C++ Edizione 2010
Operatori logici
Gli operatori logici richiedono come operandi valori o espressioni booleane e il risultato è
ancora un valore logico di tipo vero o falso. Gli operatori logici sono:
||
OR
Risultato falso solo se entrambi gli operandi sono falsi
&&
AND Risultato vero solo se entrambi gli operandi sono veri
!
NOT
inverte il valore di verità dell’unico operando
Gli operatori || e && non valutano l’operando di destra se non è necessario, ovvero se il
primo operando è vero il risultato di || sarà vero indipendentemente dal valore del secondo operando e quindi non serve valutarlo, nel caso del && se il primo operando è falso il risultato
sarà falso senza bisogno di valutare il secondo operando.
Per esempio:
(5 > 1) || (3>6) è vero mentre (5>1) && (3 > 6) è falso
Operatori sui bit
A tutte le variabili viene assegnato uno spazio di memoria dove viene memorizzato il valore
contenuto nella variabile. Il valore è sempre rappresentato tramite una serie di bit. Ogni bit è
una cifra binaria e quindi può assumere solo i due valori 0 e 1 che nell’algebra booleana rappresentano rispettivamente i valori falso e vero, per cui il valore di una qualsiasi variabile può
essere visto come una sequenza di valori vero e falso. Le operazioni sui bit sono operazioni
effettuate sui singoli bit che costituiscono il valore degli operandi considerati indipendentemente uno dall’altro.
Tutte queste operazioni, come pure quella di incremento e decremento, sono in genere disponibili in tutti i linguaggi macchina e quindi possono essere tradotte in modo molto efficiente
in questo linguaggio avvalorando la tesi che vuole il linguaggio C più vicina al linguaggio
macchina rispetto agli altri linguaggi di alto livello.
Gli operatori sui bit sono:
Operatore
Descrizione
&
AND bit a bit, il risultato è 1 solo se entrambi i bit sono 1
|
OR bit a bit, il risultato è 1 solo se almeno uno dei due bit è 1
^
XOR (OR esclusivo) bit a bit, il risultato è 1 solo se uno solo dei due bit
è=1
~
Complemento a 1 ovvero inversione di tutti i bit
>>
Shift a destra. Spostamento a destra di tutti i bit con propagazione del
bit del segno (il primo bit più a sinistra), in pratica corrisponde a dividere il numero binario per 2x dove x è il numero dei bit di scorrimento. Se
non si vuole la propagazione del segno bisogna utilizzare le variabili
unsigned.
<<
Shift a sinistra ovvero spostamento a sinistra di tutti i bit e riempimento
dei bit che si liberano a destra con zeri. In pratica corrisponde a moltiplicare il numero per 2x dove x è il numero dei bit di scorrimento.
Per esempio supponiamo di aver definito due variabili con l’istruzione short int a=25,b=20,
considerato che il tipo short int ha una rappresentazione interna in binario a 16 bit con complemento a 2 per i numeri negativi, il contenuto delle due variabili corrisponderà ai due numeri 25 e 20 scritti in binario e sarà il seguente:
25
Corso sul Linguaggio C++ Edizione 2010
a = 0000000000011001
b = 0000000000010100
per cui si avrà che:
a & b  0000000000010000 = 16
a | b  0000000000011101 = 29
a ^ b  0000000000001101 = 13
~a  1111111111100110 = -26
~b  11111111111111101011 = -21
a >> 2  0000 0000 0000 0110 = 6 (risultato dello spostamento a destra di due bit)
a << 2  0000 0000 0110 0100 = 100 (risultato dello spostamento a sinistra di due bit)
Per verificarlo basta scrivere il seguente programma:
#include <iostream.h>
#include <stdlib.h>
int main()
{
short int a=25, b=20 ;
cout << " a = " << a << "\t b = "<< b << endl;
cout << " a & b = " << (a & b) << "\t a | b = " << (a | b)
<< "\t a ^ b = " << (a ^ b) << endl;
cout << "~a = " << (~a) << "\t ~b = " << (~b) << endl;
cout << " a << 2 = "<<(a << 2)<<"\t a >> 2 = "<<( a >> 2)<< endl;
system("PAUSE");
return 0;
}
Si noti che invece della sequenza di escape \n si può utilizzare la costante predefinita endl per
inviare in output il carattere new line, mandandolo in esecuzione si otterrà il seguente output:
Regole di valutazione di un’espressione
Le espressioni possono essere combinazione di operatori e operandi anche complesse ma comunque portano sempre ad un risultato univoco. Per capire come il computer calcola una espressione bisogna sapere che i calcoli vengono effettuati dall’unità aritmetico-logica (ALU)
che è una specie di calcolatrice contenuta nell’unità centrale di elaborazione (CPU) di ogni
computer. L’unità aritmetico-logica può eseguire una sola operazione alla volta e quindi per
calcolare un’espressione bisogna eseguire in sequenza le varie operazioni memorizzando i risultati parziali così come faremmo noi con una calcolatrice normale.
Se per esempio consideriamo l’espressione 5 + 3 * 6, il risultato sarà diverso se eseguiamo
prima l’addizione e poi la moltiplicazione o viceversa prima la moltiplicazione e poi
l’addizione. Nel primo caso esce 48 nel secondo esce 23. Il programmatore deve conoscere
l’ordine con cui il computer eseguirà i calcoli, per questo in ogni linguaggio di programmazione sono state fissate delle regole che indicano come verrà calcolato il valore di una qualsia26
Corso sul Linguaggio C++ Edizione 2010
si espressione, stabilendo esattamente l’ordine con cui devono essere applicati gli operatori
agli operandi. Le regole per il linguaggio C++ sono le seguenti:
1. L’impiego di parentesi. Se si utilizzano le parentesi tonde, le espressioni racchiuse
all’interno delle parentesi vengono calcolate prima di eseguire i calcoli posti fuori dalle parentesi. Il compilatore traduce l’espressione in modo da far calcolare prima le espressioni racchiuse nelle coppie di parentesi più interne, poi sostituisce tutto il blocco
che inizia con “(“ fino a “)” con il risultato dell’espressione, e procede man mano fino
a quelle più esterne;
2. Livello di precedenza degli operatori. Gli operatori sono divisi in gruppi con livelli
di precedenza diverso, in mancanza di parentesi vengono prima eseguite prima le operazioni con livello più alto, poi le altre in ordine fino a quelle di livello più basso. Per
conoscere il livello di precedenza delle operazioni si veda la Tabella 2 che segue, dove
sono riportati tutti gli operatori divisi in gruppi in base al livello di priorità. Per esempio le moltiplicazioni e le divisioni vengono eseguite prima delle addizioni e sottrazioni, quindi l’espressione precedente 5 + 3 * 6 produce il risultato 23;
3. Le regole di associatività. Quando c’è una serie di operandi e operatori con lo stesso
livello di precedenza, si seguono le regole di associatività. Per la maggior parte degli
operatori vale l’associatività a destra, cioè le operazioni vengono eseguite partendo da
quella più a sinistra fino a quella più a destra, fanno eccezione gli operatori di assegnamento per i quali vale l’associatività a sinistra.
Quindi, quando viene calcolata un espressione scritta in C++ vengono prima calcolate le espressioni racchiuse tra parentesi, poi vengono eseguiti in calcoli in ordine di precedenza degli operatori, se ci si trova con una sequenza di operandi e operatori con lo stesso livello di
precedenza si segue la regola dell’associatività, questo significa che se si trova un espressione
del tipo:
operando1 op1 operando2 op2 operando3
dove <op1> e <op2> sono operatori con lo stesso livello di priorità allora il computer segue la
regola dell’associatività, se l’associatività è a destra, come avviene normalmente per la maggior parte delle operazioni, si procede da sinistra verso destra e precisamente si calcolerà:
(operando1 op1 operando2) op2 operando3
se invece l’associatività è a sinistra (come avviene per l’operatore =) si procederà da destra
verso sinistra e quindi si calcolerà:
operando1 op1 (operando2 op2 operando3)
Operatori speciali
Vedremo solo due operatori speciali:
?
utilizzato con tre operandi (ternario), una espressione logica e due espressioni qualsiasi dello stesso tipo, produce il risultato di una delle due espressioni
scegliendolo in base al valore dell’espressione logica;
,
utilizzato con due operandi (binario), ha il più basso livello di priorità, inferiore anche agli operatori di assegnamento, è associativo a destra, fa calcolare dal computer in sequenza le due espressioni, rendendo possibile l’utilizzo
del risultato della prima nella seconda;
La sintassi dell’operazione ? è la seguente:
<espressione logica>?<espressione1>:<espressione2>
27
Corso sul Linguaggio C++ Edizione 2010
si utilizza quando vogliamo sceglier tra due risultati diversi in base ad una condizione. Per esempio se lo sconto è del 10% quando si acquista una quantità di prodotto che va da 1 a 5
mentre è del 20% se si acquistano più di 5 unità del prodotto, per assegnare il valore alla variabile sconto possiamo utilizzare la seguente espressione:
sconto = quant > 5 ? 20 : 10;
Si noti che nell’espressione precedente non servono le parentesi perché in base alla precedenza degli operatori (vedi Tabella 2 sotto) viene eseguita prima l’operazione >, poi l’operazione
? ed infine l’operazione =.
Se i possibili sconti fossero stati tre, per esempio 5% fino a due pezzi, 10% da tre pezzi a cinque e 20% oltre i 5 pezzi, avremmo potuto calcolare lo sconto sempre con una sola istruzione
nel modo seguente:
sconto = quant > 5 ? 20 : (quant >2 ? 10 :5);
La sintassi dell’operazione , (virgola) è la seguente:
<espressione1>,<espressione2>
potendosi ripetere l’operazione quante volte si vuole si possono scrivere anche espressioni del
tipo:
<espressione1>,<espressione2>,<espressione3>, …….,<espressioneN>
L’effetto di queste operazioni è semplicemente quello di far calcolare al computer le espressioni nell’ordine partendo dalla prima più a sinistra fino all’ultima a destra. Se qualcuna di
queste espressioni contiene operazioni di assegnamento, nelle seguenti verranno eventualmente utilizzati i valori delle variabili così come modificate delle espressioni precedenti. Per esempio, con l’istruzione:
k = (i=10, j=20, i+j);
alla variabile k verrà assegnato il valore 30.
Si noti che in questo caso l’utilizzo delle parentesi è indispensabile per far eseguire
l’operazione , (virgola) prima dell’assegnamento (operazione =), senza parentesi a k sarebbe
stato assegnato il valore 10.
Riassunto degli operatori del linguaggio C++
Tabella 2. Gli operatori disponibili nel linguaggio C++ raggruppati in ordine di precedenza da
quelli con precedenza maggiore fino a quelli con precedenza minore (le linee più marcate segnano
la divisione tra un gruppo e l’altro).
Simbolo
::
.
->
[]
()
()
sizeof
sizeof
++
++
--~
Rappresenta l’operazione ….
visibilità
Selezione elemento
Selezione elemento
Indicizzazione
Chiamata di funzione
Conversione di tipo
Dimensione di un oggetto
Dimensione di un tipo di dati
Incremento prefisso
Incremento postfisso
Decremento prefisso
Decremento postfisso
Inversione dei bit
Sintassi
<nome_calsse>::<elemento>
<oggetto>.<elemento>
<puntatore>-><elemento>
<identificatore>[<espressione>]
<nome_funz.>(<lista parametri>)
<tipo dati>(<espressione>)
sizeof(<espressione>)
sizeof(<tipo dati>)
++<variabile>
<variabile>++
--<variabile>
<variabile>-~<espressione>
28
Corso sul Linguaggio C++ Edizione 2010
Simbolo
!
+
&
*
new
delete
delete[]
()
Rappresenta l’operazione ….
Sintassi
NOT
!<espressione logica>
Cambiamento di segno
-<espressione numerica>
Più unario
+<espressione numerica>
Indirizzo di
&<identificatore>
Dereferenziazione
*<espressione>
Allocazione dinamica
new <tipo dato>
Deallocazione dinamica
delete <puntatore>
Deallocazione di un array
delete[] <puntatore>
Conversione di tipo nella notazione
(<tipo dato>)<espressione>
cast
Selezione indirizzo elemento
<oggetto>.*<puntatore_elemento>
Selezione indirizzo elemento
<puntatore>->*<puntatore_elemen.>
Moltiplicazione
<espressione>*<espressione>
Divisione
<espressione>/<espressione>
Resto della divisione
<espressione>%<espressione>
Addizione
<espressione>+<espressione>
Sottrazione
<espressione>-<espressione>
Shift a sinistra
<espressione> << <numero bit>
Shift a destra
<espressione> >> <numero bit>
Minore di
<espressione> < <espressione>
Minore o uguale a
<espressione> <= <espressione>
Maggiore di
<espressione> > <espressione>
Maggiore o uguale a
<espressione> >= <espressione>
Uguale a
<espressione> == <espressione>
Diverso da
<espressione> != <espressione>
And bit a bit
<espressione> & <espressione>
XOR (OR esclusivo) bit a bit
<espressione> ^ <espressione>
OR bit a bit
<espressione> | <espressione>
AND logico
<espr.logica> && <espr.logica>
OR logico
<espr.logica> || <espr.logica>
Espressione condizionale
<espr.logica>?<espr1>:<espr2>
Assegnazione
<identificatore>=<espressione>
Assegnazione composta
<identificatore><operatore>=<espressione>
.*
->*
*
/
%
+
<<
>>
<
<=
>
>=
==
!=
&
^
|
&&
||
?
=
*= /=
%= += = <<=
>>= &=
|= ^=
,
Virgola (esegue le due espressioni in
sequenza)
2.6
<espressione1>,<espressione2>
La conversione di tipo
Abbiamo visto che una espressione può consistere in più calcoli e che il computer esegue questi calcoli uno alla volta memorizzando i risultati intermedi in variabili temporanee che poi u29
Corso sul Linguaggio C++ Edizione 2010
tilizza nei calcoli successivi. Il tipo di queste variabili temporanee dipende dall’operazione e
in genere è sempre dello stesso tipo degli operandi utilizzati.
Per esempio consideriamo il seguente programma:
int main()
{
int uno=1, due=2;
float tre;
tre = uno/due;
cout << tre <<endl;
system("PAUSE");
return 0;
}
Ci aspetteremmo di vedere in output il risultato 0.5, visto che la variabile tre è di tipo float,
invece verrà visualizzato 0. Questo dipende dal fatto che con il linguaggio C++, il calcolo uno/due, produce un risultato di tipo int essendo i due operandi uno e due entrambi di tipo int.
Il risultato sarà quindi 0 e non 0.5.
Il problema si verifica quando in un’espressione gli operandi sono di tipo diverso, infatti ci
sono operazioni che permettono, entro certi limiti, l’utilizzo di operandi di tipo diverso, per
esempio possiamo eseguire operazioni aritmetiche con un operando di tipo double e un altro
di tipo int oppure, come nel programma precedente, possiamo assegnare ad un variabile di tipo float un valore intero.
Quando gli operandi di un’operazione sono di tipo diverso, il linguaggio C++ sceglie per il
risultato temporaneo dell’operazione, il tipo più capiente tra quello degli operandi in modo da
evitare perdita di informazioni, e, prima di fare il calcolo, converte l’altro operando nel tipo
scelto. Si dice in questo caso che viene effettuata una conversione implicita di tipo detta cast
implicito.
Il tipo double è più capiente del tipo float ed entrambi sono più capienti del tipo int perché
qualsiasi numero intero può essere memorizzato in una variabile di tipo float o double (che
accettano rispettivamente numeri fino a 1038 e 10308) mentre se spostiamo una variabile di tipo
float in una intera perderemo la parte dopo la virgola. Ogni volta che si assegna un valore di
un tipo più capiente ad una variabile meno capiente si può avere una perdita di informazioni,
per esempio se assegniamo un valore double ad una variabile float è molto probabile che ci
sarà una diminuzione del numero di cifre significative corrette.
Se mettiamo in ordine i tipi numerici dal meno capiente al più capiente abbiamo la seguente
sequenza:
charshort intintlong intfloatdoublelong double
per esempio dopo l’esecuzione delle seguenti istruzioni:
int uno=1, float due=2.0, tre;
tre = uno/due;
alla variabile tre verrà assegnato correttamente il valore 0.5 perché il computer quando esegue
l’operazione uno/due convertirà automaticamente il valore di uno in float, eseguirà una divisione tra numeri con la virgola e memorizzerà il risultato temporaneo in una variabile di tipo
float.
30
Corso sul Linguaggio C++ Edizione 2010
È anche possibile indicare al computer il tipo di dati in base al quale deve essere svolta
l’operazione usando il cosiddetto cast esplicito la cui sintassi è:
(<tipo dati>)<espressione>
Per esempio per far funzionare correttamente il programma precedente, senza modificare il
tipo delle variabili, si può modificare l’istruzione per la divisione nel seguente modo:
int main()
{
int uno=1, due=2;
float tre;
tre = (float) uno/due;
cout << tre <<endl;
system("PAUSE");
return 0;
}
premettendo la parola (float) tra parentesi all’espressione, si forza il computer ad eseguire i
calcoli con il tipo float.
È possibile convertire i dati da un formato ad un altro anche utilizzando le funzioni di conversione la cui sintassi è:
<tipo dati>(<espressione>)
per esempio: int(10.5*3) → 31 invece di 31.5, char(6*10+5) produce il numero 65 in uno
spazio di un solo byte.
Ma in questo caso l’espressione viene calcolata normalmente e, solo dopo, il risultato viene
convertito nel formato specificato. Quindi se nel programma precedente avessimo scritto float(uno/due) invece di (float)uno/due avremmo ottenuto come risultato 0 invece di 0.5, perché
l’espressione uno/due tra due numeri interi da 0 e questo risultato, trasformato in float, è sempre 0.
2.7
Le enumerazioni
Fra i tipi elementari introdotti nel paragrafo 2.4 c’è anche il tipo enum. In realtà non si tratta
di un vero tipo di dati ma di un insieme di possibili tipi che il programmatore può definire. In
pratica il programmatore può definire un nuovo tipo attraverso un elenco di identificatori che
rappresentano i possibili valori che questo tipo può assumere. Per esempio:
enum colori {giallo, arancione, rosso, verde, blu};
// (1)
In questo modo viene creato un nuovo tipo di dati che si chiama colori e che può assumere solo cinque valori diversi a cui vengono assegnati i nomi giallo, arancione, rosso, verde, blu.
Dopo aver definito il tipo colori, come per qualsiasi altro tipo, per utilizzarlo dobbiamo definire le variabili, per esempio:
colori a,b;
a e b diventano variabili di tipo colori. A queste variabili, nel programma, si potrà assegnare
solo valori di tipo colori.
a = rosso; b = verde;
// (2)
31
Corso sul Linguaggio C++ Edizione 2010
In realtà il compilatore associa ogni identificatore dell’elenco fornito dal programmatore con
un numero intero e nel programma considera questi identificatori come costanti dichiarate dal
programmatore.
Nell’esempio dell’istruzione (1), le parole giallo, arancione, rosso, verde, blu, diventano costanti esattamente come se fossero state definite con l’istruzione:
const int giallo=0, arancione=1, rosso=2, verde=3, blu=4;
Quindi le variabili di tipo colori sono in realtà delle variabili di tipo intero a cui, però, si possono assegnare solo i numeri da 0 a 4. Con l’istruzione (2) viene assegnato il numero 2 alla
variabile a e il numero 3 alla variabile b. Se si visualizza in output un variabile di tipo colori
verrà visualizzato il numero contenuto al suo interno e non il nome del colore associato a quel
numero!
Le operazioni ammesse con le variabili di tipo dichiarato attraverso l’istruzione enum sono le
stesse ammesse con il tipo intero, quindi ++, --, +, -, *, /, %. Inoltre queste variabili sono
compatibili con il tipo intero.
Abbiamo visto che il computer assegna automaticamente i numeri da zero in poi agli identificatori definiti nelle enumerazioni ma il programmatore può anche decidere lui quali numeri
assegnare, per esempio:
enum mesi {gennaio=1, febbraio, luglio=7, agosto};
mesi mese1, mese2;
si avrà gennaio = 1, febbraio =2, luglio = 7, agosto =8
inoltre, si possono anche dichiarare le variabili con la stessa istruzione enum:
enum mesi {gennaio=1, febbraio, luglio=7, agosto} mese1,mese2;
2.8
Esercizi
1) Calcolare le seguenti espressioni scritte nel linguaggio C++
a) 3*(15-2*5)/(4*10/8)
b) 2+5*(40 / (2*4) /2)+(10/3)
[Risultato:3; 15;]
2) Scrivere le espressioni logiche per:
• Vedere se un numero x è positivo
• Vedere se un numero x è pari;
• Vedere se un numero x è compreso nell'intervallo [A, B] chiuso;
• Vedere se un numero x è esterno all'intervallo [A, B] chiuso;
• Vedere se un numero intero x è multiplo di 5 e, contemporaneamente, è dispari;
3) Calcolare ili risultato delle seguenti espressioni logiche (1 per "vero", 0 per "falso")
5>2;
10<3;
5>7 || 3>4;
10<2 || 5>4;
7<1 || 4<5;
3>0 && 5>2;
8>10 && 5>2; 7>8 != 7>10;
3>2 != 2<3;
5<5 == 4>3;
4) Sia dato il seguente pezzo di programma:
long int d = 1267894; float media;
int k = 10;
media = d/k;
cout << (float) media;
qual è il valore che viene stampato?
32
Corso sul Linguaggio C++ Edizione 2010
5) Indicare il valore assunto dalla variabile f nelle seguenti espressioni:
int a=1, b=2, c=3, d=4, e=5, f;
a) f = 5 + --a - e / b++;
b) f = ++a - c-- + e/--b;
c) f = d + c * b-- + e / a++;
6) Indicare il valore assunto dalla variabile c nelle seguenti espressioni:
int a=5, b=6, c;
a) c = (a == b) ? ++a : ++b;
b) c = (a++ == b) ? b++ : b--;
c) c = (++a == b) ? ++b : --b;
7) Determinare quale è la relazione che assume valore vero quando x è esterno all'intervallo
[A,B] e y è interno allo stesso intervallo?
a) (x<A) && (x>B) && (y>=A) && (y<B);
b) ((x<A)||(x>B)) && ((y>=A)&&(y<=B));
c) ((x<A)||(x>B)) && ((y>=A)||(y<=B));
d) ((x<A)||(x>B)) || ((y>=A)||(y<=B));
e) ((x<A)&&(x>B)) && ((y>=A)||(y<=B));
f)
((x<A)||(x>B)) || ((y>=A)&&(y<B));
8) Sia data una variabile dichiarata come char c;. Ipotizzando che essa contenga un carattere
compreso tra '0' e '9', come si trasferisce in una variabile intera v il valore decimale della
cifra rappresentata da c?
a) v = atoi(c); (atoi- ascii to int- trasforma una stringa contenente cifre in un numero
int - come val() del Basic- ma il parametro deve essere un puntatore)
b) v = (int) c ;
c) v = c - '0';
d) v = c ;
e) nessuna delle precedenti
9) Quale dei seguenti valori di a e b produce il valore vero per la condizione:
(a > 0) && ((b < 0) || (b > 1))
Risposte: a)a = 5; b = 0
b)a = 5; b = 2
c)a = -1; b = 5
d)a = 1; b = 1
10) Data una misura di tempo espressa in ore minuti e secondi descrivere un algoritmo che la
trasformi in secondi.
11) Descrivere un algoritmo per trasformare una misura espressa in secondi in un'altra espressa in ore minuti e secondi.
12) Dato un numero n compreso tra 1 e 365, considerandolo come giorno n-esimo dell'anno,
descrivere un algoritmo per determinare a quale giorno della settimana corrisponde sapendo che il primo giorno dell'anno era domenica. (output un numero da 1 a 7, 1=lunedì
7 = domenica.)
33
Corso sul Linguaggio C++ Edizione 2010
13) Assegnata la paga oraria di un operaio calcolare la paga settimanale sapendo che si lavora
7 ore al giorno, il sabato si lavora solo 4 ore pagate però il doppio e la domenica non si
lavora.
14) Trovare l’algoritmo per calcolare il numero medio di medici per paziente in un giorno
qualsiasi dell’anno in un ospedale di cui si conosce il numero dei medici, il numero totale
dei pazienti che circolano in un anno e il numero medio di giorni di degenza di ogni paziente.
15) Convertire lire in Euro (1 euro = 1936,27 lire)
16) Data una misura espressa in metri trovare l’equivalente in piedi e yarde sapendo che:
3 piedi = 1 yarda = 0,91439 metri
17) Sapendo che si può passare dai gradi Celsius ai gradi Fahrenheit con la seguente formula:
gradi Fahrenheit = gradi Celsius * 9/5 + 32
Scrivere un programma per trasformare una misura della temperatura da gradi Celsius a Fahrenheit e uno per fare il contrario.
Risposte esercizi
Esercizio 4) = 126789
Esercizio 5) a) = 3; b) = 4; c) = 15
Esercizio 6): a) = 7; b) = 6; c) = 7
Esercizio 7) = b)
Esercizio 8) = c) c-‘0’
Esercizio 9) = b)
34
3
Capitolo
Corso sul Linguaggio C++ Edizione 2010
3. Le istruzioni e le strutture di controllo
Un programma C++ è un insieme di istruzioni. Le istruzioni possono essere di due tipi:
• Istruzioni eseguibili, corrispondono ad un azione eseguita dal computer;
• Istruzioni di dichiarazione, non corrispondono a nessuna operazione ma servono per definire il modello dei dati o gli oggetti utilizzati dal programma oppure danno indicazioni al compilatore sulle funzioni utilizzate nel programma.
Nel programma sorgente è possibile alternare a piacere istruzioni dichiarative ed eseguibili purché
quando si utilizza un oggetto quest’ultimo sia stato già definito in precedenza.
Le istruzioni possono essere anche classificate nel seguente modo:
Di assegnazione
Espressioni
semplici
chiamata
funzione
di
Di dichiarazione e
definizione
return
Istruzioni del
C++
break
Di salto
continue
goto
blocco
strutturate
selezione
if ... else
switch
while
cicli
do…while
for ….
35
Corso sul Linguaggio C++ Edizione 2010
3.1
La programmazione strutturata
Abbiamo visto che il linguaggio C++ permette di implementare sia i programmi realizzati con
il modello procedurale che quelli realizzati secondo il modello della programmazione object
oriented, entrambi, però, prevedono la traduzione di algoritmi nel linguaggio di programmazione, anche se nella programmazione procedurale, questa è l’operazione base che realizza
l’attività del “programmare”.
Sappiamo che un algoritmo è la descrizione di un procedimento ovvero di una serie finita di
azioni che opera su un’insieme di dati in ingresso per produrre un insieme di risultati. Questa
descrizione deve essere non ambigua ovvero un qualsiasi esecutore che esegue l’algoritmo, se
parte dagli stessi dati, deve eseguire esattamente le stesse azioni e deve arrivare esattamente
agli stessi risultati. Un algoritmo, inoltre, rappresenta sempre un procedimento generale che si
può eseguire più volte cambiando i dati in input. Le azioni che verranno svolte, possono essere diverse a seconda dei dati iniziali. Quindi, nell’algoritmo, oltre alla descrizione chiara e
univocamente interpretabile delle azioni da eseguire, viene anche indicato l’ordine con cui
queste azioni devono essere eseguite, inoltre ci può essere l’indicazione che una o più azioni
devono essere ripetute e/o eseguite solo in alcuni casi. Queste indicazioni non sono descrizioni di azioni che l’esecutore deve compiere ma piuttosto servono a indicare esattamente la sequenza con cui le azioni devono essere eseguite e quando devono essere eseguite. Se
l’algoritmo viene descritto con un diagramma di flusso (detto anche diagramma a blocchi) le
azioni sono descritte in rettangoli mentre la sequenza di esecuzione (flusso) e le condizioni
che determinano l’esecuzione o meno delle azioni sono rappresentate dalle frecce e dai rombi,
dando luogo a schemi grafici diversi quando si cambiano le indicazioni relative a come si susseguono le azioni. I vari modi con cui mettere insieme le azioni per formare un algoritmo
vengono definiti da alcuni autori 1 modelli di composizione o schemi di composizione ma
sono noti in generale come strutture di controllo della programmazione. I principali modelli di composizione sono: La sequenza, la selezione e la ripetizione:
La sequenza consiste nella descrizione di una serie di azioni che devono essere eseguite una
dopo l’altra nell’ordine indicato, per esempio:
Inizio
Azione 1
…
Azione 2
Azione N
Fine
La selezione consiste nella descrizione di una o più azioni che devono essere eseguite solo se
si verifica una determinata condizione, per esempio:
no
si
no
condizione
azione
A) Selezione ad una via
1
condizione
Azione1
B) Selezione a due vie
A. Andronico e altri, Manuale di Informatica, Zanichelli 1979
36
si
Azione2
Corso sul Linguaggio C++ Edizione 2010
La ripetizione o ciclo consiste nella descrizione di una o più azioni che devono essere ripetute
un numero di volte variabile in base ad una condizione fissata. Per esempio:
si
Azione
condizione
no
si
azione
condizione
no
A) Ripetizione precondizionale
B) Ripetizione postcondizionale
Se consideriamo tutti i diagrammi di flusso che si possono realizzare utilizzando solo gli
schemi di composizione di sequenza, selezione e ripetizione sopra indicati e supponiamo che
un azione rappresentata nello schema possa essere o un’azione elementare che l’esecutore capisce o un azione complessa corrispondente ad una serie di azioni elementari rappresentabile
sempre con uno dei tre schemi base su indicati, otteniamo un sottoinsieme di tutte le descrizioni di algoritmi possibili. Questo sottoinsieme è quello degli schemi di flusso strutturati.
In base al famoso teorema enunciato dai due matematici italiani Böhm e Jacopini 2, un qualsiasi algoritmo può sempre essere descritto utilizzando uno schema di flusso strutturato e quindi
usando solo le strutture di controllo della sequenza, selezione e ripetizione. Anzi il teorema
afferma che basta anche solo la struttura della sequenza, quella della selezione ad una via e un
solo tipo di ciclo (per esempio quello postcondizionale).
Per far eseguire un algoritmo da un computer bisogna descriverlo utilizzando un linguaggio
che possa essere, in qualche modo, compreso dal computer. Hanno tale caratteristica i linguaggi di programmazione.
Ogni azione elementare dell’algoritmo deve corrispondere ad un’istruzione elementare del
linguaggio di programmazione utilizzato. Per comunicare al computer gli schemi di composizione delle azioni si utilizzano le istruzioni di controllo. Il teorema di Böhm-Jacopini ci assicura che in un linguaggio, per poter tradurre un qualsiasi algoritmo, bastano le istruzioni di
controllo per la sequenza, selezione e ripetizione purché all’interno di queste istruzioni si
possa inserire una qualsiasi altra istruzione (elementare o di controllo). I linguaggi di programmazione che possiedono le istruzioni di controllo con le caratteristiche su menzionate
sono detti linguaggi strutturati.
Si è visto che l’utilizzo di un linguaggio strutturato rende più semplice individuare gli errori
nei programmi perché permette di evitare l’utilizzo dell’istruzione di salto (l’istruzione goto).
Quest’ultima istruzione era molto utilizzata nei primi linguaggi di programmazione e permette
la traduzione di un qualsiasi schema, ma rende molto più complesso, per il programmatore,
tenere sotto controllo il flusso del programma ovvero la sequenza delle azioni che verranno
eseguite in tutti i casi possibili.
Il primo linguaggio strutturato fu il Pascal, oggi tutti i linguaggi moderni di programmazione
sono linguaggi strutturati.
2
vedi http://it.wikipedia.org/wiki/Teorema_di_Jacopini-Bohm.
37
Corso sul Linguaggio C++ Edizione 2010
I programmi che utilizzano solo le istruzioni di controllo per i tre schemi base visti sopra sono
detti programmi strutturati 3.
Il linguaggio C (e di conseguenza anche il linguaggio C++) è un linguaggio strutturato. Nei
prossimi paragrafi vedremo le istruzioni utilizzate per tradurre le tre strutture di controllo fondamentali della programmazione in questo linguaggio.
3.2
Le istruzioni semplici
La principale istruzione semplice è quella di assegnazione che abbiamo già visto. Abbiamo
già visto anche le istruzioni per la dichiarazione di variabili, le istruzioni per dichiarare e definire oggetti di altro tipo le vedremo man mano che studieremo gli oggetti in questione.
Ogni istruzione semplice finisce sempre con il carattere ; (punto e virgola), esiste anche
l’istruzione nulla costituita dal solo carattere ;
;
Se viene inserita in un programma serve a dire al computer che il quel punto non deve fare
niente.
In questo paragrafo vedremo le istruzioni di salto:
break
continue return
goto
Normalmente le istruzioni di un programma vengono eseguite una di seguito all’altra,
nell’ordine in cui sono scritte, inoltre è possibile utilizzare le istruzioni di controllo come
quelle di selezione per far eseguire alcune istruzioni si e altre no, oppure quelle di ripetizione
per far ripetere più volte una serie di istruzioni.
In ogni istante è in esecuzione una sola istruzione del programma ed esiste nella CPU un puntatore che indica la posizione della prossima istruzione che sarà eseguita, questo puntatore si
chiama program counter. Con le istruzioni di salto è possibile modificare la sequenza con cui
dovrebbero essere svolte le istruzioni del programma e, quindi, tutte le istruzioni di salto producono la modifica del program counter.
Le istruzioni break e continue servono per modificare la normale sequenza con cui vengono
eseguite le istruzioni in un blocco interno ad una selezione o ad un ciclo e le vedremo nei
prossimo paragrafi. L’istruzione return determina la fine dell’esecuzione di un sottoprogramma e fa tornare il punto di esecuzione al programma chiamante. Come abbiamo già visto
return serve anche per terminare il programma principale main.
L’istruzione goto (go to = vai a) è detta istruzione di salto incondizionato e produce come effetto il trasferimento del punto di esecuzione ad una posizione definita dal programmatore.
La sintassi è:
goto <etichetta>
Questo comando ordina al computer di portare il punto di esecuzione nella posizione in cui il
programmatore ha posizionato l’etichetta. Una etichetta è un qualsiasi identificatore valido
seguito dal carattere : (duepunti). Per esempio:
inizio:
contatore++;
……….
goto inizio;
L’uso di questa istruzione, come detto nel paragrafo precedente, può e dovrebbe essere evitato
perché rende difficile il controllo del programma durante l’esecuzione. Al suo posto si possono utilizzare le istruzioni strutturate elencate nei prossimi paragrafi.
3
Vedi http://it.wikipedia.org/wiki/Programmazione_strutturata.
38
Corso sul Linguaggio C++ Edizione 2010
3.3
La sequenza
Per dire al computer di eseguire una serie di istruzioni in un determinato ordine nel linguaggio
C si utilizza il blocco:
{ <istruzione1>;<istruzione2>; ………[<istruzioneN]; }
In pratica si scrivono le istruzioni nell’ordine in cui devono essere eseguite (se si trattta di istruzioni semplici devono finire con il carattere ; -punto e virgola-) e si racchiude l’elenco tra
parentesi graffe, un tale insieme di istruzioni si dice blocco.
Le istruzioni interne ad un blocco possono essere istruzioni elementari o istruzioni di controllo. Se si utilizza il linguaggio C++ si possono inserire in un blocco anche istruzioni di dichiarazione.
Per esempio tutte le istruzioni della funzione main nei programmi che abbiamo visto sono un
blocco. Un blocco può contenere al suo interno una qualsiasi istruzione di controllo e quindi
anche altri blocchi. Per esempio:
int main() {
int i=1;
{ int j=20; k=30;
……
}
i++;
……..
return 0; }
}
blocco interno
il blocco contenente tutte le istruzioni della funzione main contiene al suo interno un altro
blocco che inizia con le dichiarazioni int j=20; k=30.
Il raggruppamento delle istruzioni in blocchi è importante per l’area di validità delle variabili, infatti ogni variabile dichiarata in un blocco può essere utilizzata solo nelle istruzioni del
blocco stesso e in quelle di tutti i blocchi contenuti in esso che seguono la dichiarazione, ma
non può essere utilizzata al di fuori del blocco.
Quindi nell’esempio precedente la variabile i può essere utilizzata sia nel blocco che inizia
dopo di int main() che in quello interno, mentre le variabili j e k possono essere utilizzate solo
nelle istruzioni racchiuse tra le due parentesi graffe del blocco interno.
Area di validità delle variabili
Le variabili dichiarate in un blocco si dicono variabili locali appunto perché possono essere
utilizzate solo all’interno del blocco. È possibile però definire anche variabili globali che
valgono in tutti i blocchi, per farlo bisogna dichiarare le variabili fuori da qualsiasi blocco,
subito dopo le direttive al preprocessore, per esempio:
#include <iostream.h>
// variabili globali
int totale=0;
int main() {
float prezzo; int q;
}
la variabile totale è una variabile globale mentre le variabili prezzo e q sono variabili locali
della funzione main.
39
Corso sul Linguaggio C++ Edizione 2010
Ciclo di vita delle variabili
Tutte le variabili vengono create quando viene eseguita la dichiarazione ma le variabili globali
vengono cancellate solo alla fine del programma mentre, normalmente, le variabili locali
vengono cancellate quando termina il blocco in cui sono state dichiarate. È possibile evitare la
cancellazione delle variabili locali alla fine del blocco utilizzando il modificatore della modalità di memorizzazione static che abbiamo visto nel paragrafo 2.2. Per esempio consideriamo
il seguente programma:
int main()
{ int i;
for (i=1; i<10; i++) /*ripete 9 volte il blocco sottostante */
{ static int contatore=1;
cout << contatore << ";" ;
contatore++;
}
cout << endl;
system("PAUSE");
return 0;
}
Sapendo che l’istruzione for è una delle istruzioni di controllo per i cicli che vedremo fra poco e che in questo caso ha semplicemente l’effetto di far ripetere 9 volte le istruzioni del blocco interno, il risultato in output sarà:
1;2;3;4;5;6;7;8;9;
Premere un tasto per continuare ...
Mentre se si toglie la parola static nella dichiarazione della variabile contatore il risultato sarà:
1;1;1;1;1;1;1;1;1;
Premere un tasto per continuare ...
perché alla fine del blocco la variabile verrà cancellata e quindi, quando il blocco viene ripetuto, la variabile contatore viene ricreata con il valore 1.
Anche le variabili globali possono essere definite static, in questo caso non viene modificato
il ciclo di vita, perché tutte le variabili globali vengono cancellate alla fine del programma, ma
viene modificata l’area di validità ed ha senso quando il programma è costituito da più moduli
ovvero più file sorgenti, in quanto la variabile sarà visibile solo nel modulo dove è stata definita mentre normalmente le variabili globali sono visibili in tutti i moduli dal momento in cui
sono state definite in poi.
3.4
La Selezione
Il linguaggio C++ prevede due istruzioni per la selezione: l’istruzione if per la selezione
semplice a una o due vie e l’’istruzione switch per la selezione a più vie.
La sintassi dell’istruzione if è la seguente:
if
(
espressione logica
)
Istruzione1
else
Istruzione2
durante l’esecuzione il computer valuta l’espressione logica, se il risultato è vero (diverso da
zero) verrà eseguita l’istruzione1 altrimenti, se è presente la clausola else, verrà eseguita
40
Corso sul Linguaggio C++ Edizione 2010
l’istruzione2. Se non c’è la clausola else la selezione è ad una sola via, altrimenti è a due vie.
L’istruzione o le istruzioni presenti dentro l’if possono essere sia istruzioni semplici che strutturate, quindi possono essere anche un blocco ({ ……}) oppure un’altra istruzione if, in questo caso si dice che la seconda istruzione if è annidata nella prima. Il blocco deve obbligatoriamente essere utilizzato se le istruzioni da eseguire nel caso vero o nel caso falso sono più di
una.
Per esempio:
If (a > b) max = a ;
oppure
if (a > b) {tmp=a ; a=b ; b=tmp ;}
sono entrambe selezioni ad un via, l’istruzione max=a nel primo caso e il blocco costituito da
tre istruzioni nel secondo caso, verranno eseguiti solo se a>b.
Le seguenti istruzioni if sono esempi di selezione a due vie.
If (b == 0) cout << "Indetermintata !"; else
cout << "Impossibile!";
If (a >10)
{e++ ; a =\ 10;}
else
{e-- ; a=*10 ;}
Problema: equazione di primo grado
Scrivere un programma per far risolvere al computer una qualsiasi equazione di primo grado
in una variabile.
Analisi del problema
Si definisce equazione di primo grado in una variabile una qualsiasi equazione matematica riconducibile alla forma:
ax+b=0
dove a e b sono due numeri qualsiasi. Risolvere un'equazione significa trovare tutti i numeri
che sostituiti alla x verificano l'uguaglianza. Si può dimostrare che
Se a<> 0 allora esiste una sola soluzione : x = -b/a
Se a= 0 e b<>0 allora l'equazione non ammette soluzioni
Se a=0 e b=0 allora le soluzioni sono infinite
Noi ci limiteremo alle equazioni nel campo dei numeri reali. Per risolvere un equazione di
primo grado è necessario conoscere il valore di a e di b.
Per questo la prima cosa che il computer deve fare e procurarsi i valori dei coefficienti a e b.
Dopo, se a è diverso da zero, deve calcolare e visualizzare la soluzione, altrimenti visualizza
la frase "Equazione impossibile!" se b è diverso da zero oppure "infinite soluzioni" se b è uguale a zero. Il semplice programma che fa fare queste cose al computer è il seguente:
#include <iostream.h>
#include <stdlib.h>
// Programma Eqaz1 : Soluzione delle equazioni di primo grado
int main()
{
float a,b,x;
cout << "Inserisci i due coefficienti: ";
cin >> a; cin >> b;
if (a==0)
if (b==0) cout << "Equanzione indetrminata!\n";
41
Corso sul Linguaggio C++ Edizione 2010
else cout << "Equazione impossibile!\n";
else
{x= -b/a; cout << "\nLa soluzione è: " << x << endl;}
system("PAUSE");
return 0;
}
Si noti che quando due istruzioni if sono annidate, come nell’esempio precedente, il primo else che si trova si riferisce sempre all’ultimo if, perché prima di proseguire l’istruzione if esterna bisogna completare quella interna.
Quello che segue è il diagramma a blocchi corrispondente al programma.
Inizio
Scrivi “Inserisci i coefficienti:”
Leggi a,b
a == 0
Si
x = -b/a
Scrivi x
b == 0
Scrivi “Equazione impossibile!”
Si
Scrivi “Equazione
indeterminata!"
Fine
3.5
Selezione multipla
La selezione si utilizza quando in un algoritmo c’è un gruppo di azioni che deve essere eseguito solo in un determinato caso. Se i casi possibili, e i relativi gruppi di azioni, sono due utilizza la selezione a due vie. Se i casi sono più di due si possono utilizzare le istruzioni if annidate:
If (caso == 1)
{ \\ primo gruppo di istruzioni
…………
}
else
if (caso == 2)
{ \\ secondo gruppo di istruzioni
…………
}
else
if (caso == 3)
42
Corso sul Linguaggio C++ Edizione 2010
{ \\ terzo gruppo di istruzioni
…………
}
ma per questo tipo di algoritmi la maggior parte dei linguaggi di programmazione mette a disposizione anche un’istruzione apposita, quella per la selezione multipla, che per il linguaggio
C++ è l’istruzione switch. La sintassi è la seguente:
istruzione switch
switch
(
espressione numerica
)
{
blocco switch
}
blocco switch
case
costante
:
istruzione
default
:
istruzione
L’espressione numerica in base alla quale viene effettuata la scelta deve essere di tipo intero
(quindi può essere anche di tipo char). Il computer valuterà l’espressione e poi farà proseguire
il programma dal punto in cui c’è scritto case x: con x = risultato dell’espressione. Se non c’è
nessuna costante nel blocco switch che corrisponde al risultato dell’espressione verranno eseguite le istruzioni poste dopo la parola chiave default, se non c’è nemmeno la clausola default il controllo passa all’istruzione successiva al blocco switch. La traduzione di questo costrutto in linguaggio macchina è più efficiente rispetto alla traduzione degli if annidati. Se fra
i vari gruppi di istruzioni possibili ne deve essere eseguito solo uno, bisogna utilizzare anche
l’istruzione break che fa saltare il punto di esecuzione all’istruzione successiva al blocco
switch saltando quindi tutte le istruzioni rimanenti inserite nel blocco.
Per esempio se vogliamo tradurre con l’istruzione switch le istruzioni if annidate viste
all’inizio di questo paragrafo dobbiamo scrivere:
switch (caso) {
case 1:
// primo gruppo di istruzioni
…………
break
case 2:
// secondo gruppo di istruzioni
…………
break
case 3:
// terzo gruppo di istruzioni
…………
}
Problema: una semplice calcolatrice
Vogliamo fare un programma per realizzare una semplice macchina calcolatrice che fa le 4
operazioni fondamentali. Il computer deve leggere due numeri, chiedere quale operazione effettuare (+, -, *, /) e visualizzare il risultato.
Il programma potrebbe esser il seguente:
43
Corso sul Linguaggio C++ Edizione 2010
#include <iostream.h>
#include <stdlib.h>
/* programma SCalc: Una semplice calcolatrice */
int main()
{double a,b; char op;
cout << "Inserisci il primo operando: "; cin >> a;
cout << "Inserisci il secondo operando: "; cin >> b;
cout << "Quale operazione (+ - * /)? "; cin >> op;
switch (op) {
case '+': cout << "Risultato = " << a+b << endl; break;
case '-': cout << "Risultato = " << a-b << endl; break;
case '*': cout << "Risultato = " << a*b << endl; break;
case '/': cout << "Risultato = " << a/b << endl; break;
default : cout << "Errore! bisogna inserire uno dei segni + - * /\n";
}
system("PAUSE");
return 0;
}
Mandandolo in esecuzione e provando, per esempio, la moltiplicazione tra i due numeri 5 e 3
si ottiene:
Inserisci il primo operando: 5
Inserisci il secondo operando: 3
Quale operazione (+ - * /)? *
Risultato = 15
Premi un tasto per continuare ...
3.6
I cicli
Il principale vantaggio che ha un computer rispetto ad un essere umano nell’eseguire un compito è la velocità di esecuzione. Un personal computer, per esempio, è in grado di eseguire
qualche centinaio di milioni di operazioni aritmetiche al secondo, cosa che nessun essere umano è in grado di fare. Però per fargli eseguire una operazione bisogna dargli il comando
opportuno e per fargli eseguire più operazioni bisogna scrivere un programma dove sono elencati i comandi per ciascuna operazione. Per fortuna anche se il computer esegue milioni di
operazioni al secondo non è necessario scrivere programmi con milioni di istruzioni per ogni
secondo di elaborazione, infatti la maggior parte dei programmi contiene relativamente poche
istruzioni che però vengono fatte eseguire più volte, in alcuni casi anche milioni o miliardi di
volte. Per far risolvere un problema complesso al computer bisogna spesso progettare un procedimento che preveda la ripetizione meccanica di una serie di azioni semplici e questo è un
modo di affrontare i problemi a cui noi umani non siamo abituati. Per questo motivo le istruzioni per i cicli, cioè quelle che servono per far ripetere una serie di azioni al computer, sono
molto importanti in ogni linguaggio di programmazione. Il linguaggio C ha tre istruzioni per i
cicli: l’istruzione do (ripetizione postcondizionale), l’istruzione while (ripetizione precondizionale) e l’istruzione for (ripetizione con contatore).
Istruzioni do e while
Entrambe le istruzioni do e while servono per far ripetere una serie di azioni fin quando la condizione
prefissata è vera, appena la condizione diventa falsa il ciclo termina. La differenza sta solo nel momento in cui la condizione viene valutata, nel caso del do la condizione viene controlla solo dopo
l’esecuzione dell’istruzione del ciclo mentre nel caso del while prima. Per questo motivo il ciclo do
viene detto postcondizionale mentre il ciclo while viene detto precondizionale. La condizione che
44
Corso sul Linguaggio C++ Edizione 2010
viene valutata ogni volta nel ciclo può essere una qualsiasi espressione logica e quindi anche un espressione con risultato intero (0 = falso, un numero diverso da zero = vero).
Le azioni eseguite dal computer nei due casi sono quelle esplicitate nel seguente diagramma a blocchi:
si
istruzione
condizione
si
istruzione
no
condizione
no
A) ciclo while (precondizionale)
B) Ciclo do (postcondizionale)
La sintassi delle due istruzioni è la seguente:
Istruzione do
do
istruzione
while
(
condizione
)
;
Istruzione while
while
(
condizione
)
istruzione
Per esempio con il seguente programma
int main()
{
int i=5;
do cout << i-- << "\t"; while (i>0);
system("PAUSE");
return 0;
}
l’istruzione cout << i-- < “\t” viene eseguita mentre la condizione i>0 è vera e quindi viene
ripetuta fin quando il valore di i arriva a zero. Il risultato in output sarà:
5
4
3
2
1
Premere un tasto per continuare
In questo caso se avessimo usato l’istruzione while invece di do il risultato non sarebbe cambiato. Cambia qualcosa tra do e while nei casi in cui la condizione è vera già la prima volta
che viene eseguita l’istruzione perché in questo caso con while il ciclo non viene eseguito
mentre con do viene eseguito sempre almeno una volta.
Istruzione for
Il più comune tipo di ciclo è quello in cui si fa ripetere dal computer una serie di azioni un numero prefissato di volte. Questo tipo di ciclo si può realizzare utilizzando un contatore che parte da zero e le istruzioni do e while con la condizione per far continuare il ciclo impostata nel seguente modo:
contatore < numero volte che il ciclo deve essere ripetuto
Questo tipo di ciclo viene detto ciclo con contatore o ciclo enumerativo, considerato che è un tipo
di ciclo molto utilizzato, la maggior parte dei linguaggi di programmazione ha un’istruzione apposita
per esso: l’istruzione for.
45
Corso sul Linguaggio C++ Edizione 2010
La sintassi dell’istruzione for per il linguaggio C++ è la seguente:
Istruzione for
for
inizializzazione
(
;
Condizione
;
incremento
)
istruzione
che corrisponde al seguente diagramma a blocchi:
inizializzazione
vera
condizione
falsa
incremento
istruzione
prosegue il programma
questo significa che, prima di tutto viene eseguita l’istruzione di inizializzazione, poi inizia il
ciclo e viene valutata la condizione (quindi si tratta di un ciclo precondizionale), poi, se la
condizione è vera, viene eseguita l’istruzione e l’incremento che è l’ultima operazione del ciclo. Per esempio se vogliamo far scrivere dieci volte su video la parola “ciao” basta inserire la
seguente istruzione for:
for (int i=0; i<10; i++) cout << "ciao\n" ;
il computer prima crea la variabile i con zero dentro, poi inizia il ciclo in cui controlla se i<10
e, fin quando è vero, scrive ciao su video e incrementa i.
Da notare che quando l’istruzione di inizializzazione comprende anche la dichiarazione della
variabile come nell’esempio precedente, la variabile sarà creata appositamente per il ciclo e
poi cancellata alla fine del ciclo, quindi si tratterà di una variabile locale con area di validità e
ciclo di vita ristretta alle sole istruzioni del ciclo.
Se le istruzioni del ciclo sono più di una bisogna inserire un blocco al posto dell’istruzione del
ciclo. Per esempio supponiamo di voler scrivere il programma per far calcolare la media di n
numeri letti da tastiera, il programma può chiedere “Quanti numeri?” e poi eseguire un ciclo
per leggere i numeri e sommarli. Nel ciclo devono essere inserite più istruzioni, quelle per
leggere un numero e quella per sommare il numero letto all’accumulatore:
#include <iostream.h>
#include <stdlib.h>
// Programma media. Calcola la media di una serie di numeri in input
int main()
{ float somma=0, num, media; int i,n;
cout << "Quanti numero? "; cin >> n;
for (i=0; i<n; )
{cout << "inserisci il numero " << ++i << ": "; cin >> num;
somma += num;}
media = somma / n;
46
Corso sul Linguaggio C++ Edizione 2010
cout << "La media e' = " << media << endl;
system("PAUSE");
return 0;}
In questo caso l’istruzione di incremento è stata omessa perché conveniva includerla tra le istruzioni del ciclo:
cout << "inserisci il numero " << ++i << ": « ;
si noti l’uso della notazione prefissa (++i) che produce in output il risultato dell’incremento.
Mandando in esecuzione il programma con tre numeri in input (3, 5, 6) si ottiene in output :
Quanti numeri ? 3
Inserisci il numero 1: 3
Inserisci il numero 2: 5
Inserisci il numero 3: 6
La media e’ = 4.66667
Premere un tasto per continuare ...
Con tutti i tipi di cicli si possono usare anche le istruzioni di salto break e continue.
L’istruzione break interrompe l’esecuzione del ciclo e fa proseguire il programma con
l’istruzione successiva al ciclo.
L’istruzione continue interrompe l’iterazione corrente senza completare le istruzioni presenti
nel corpo del ciclo e, a differenza di break, fa proseguire il ciclo con l’iterazione successiva,
nel caso del ciclo for il punto di esecuzione passa all’istruzione di incremento che è l’ultima
istruzione del ciclo, nel caso dei cicli do e while passa al controllo della condizione.
Problema numeri primi
Visualizzare i numeri primi compresi tra due limiti a e b dati in input.
I numeri primi sono quelli divisibili solo per 1 e per se stesso. Si può dimostrare che per vedere se un numero è primo basta controllare che non è divisibile per nessun numero compreso
tra 2 e la radice del numero. Per fare questo possiamo utilizzare un ciclo ma se ad un certo
punto troviamo un divisore è inutile continuare il ciclo fino alla fine perché già sappiamo che
il numero non è primo. Il programma è il seguente:
#include <iostream.h>
#include <stdlib.h>
#include <math.h>
int main(){
/* programma numprimi.
Visualizza tutti i numeri primi compresi tra due numeri a e b dati in
input*/
int a,b,p,i;
cout << "Inserisci il limite inferiore: "; cin >> a;
cout << "Inserisci il limite superiore: "; cin >> b;
for (p=a; p<=b; p++)
{for (i=2;i<= sqrt(p);i++) //se c'è un divisore <= sqr(p) ferma il ciclo
if (!(p % i)) break;
// se il ciclo non è stato fermato il numero p è primo
if (i > sqrt(p)) cout << p << " - ";
}
system("PAUSE");
return 0;
}
Per utilizzare la funzione sqrt (square root= radice quadrata) bisogna includere il file di header
math.h.
47
Corso sul Linguaggio C++ Edizione 2010
3.7
Documentazione
Il linguaggio C++ include poche operazioni e istruzioni base, molte altre sono disponibili in
librerie esterne in genere fornite insieme al compilatore del linguaggio, per utilizzarle bisogna
collegare le librerie che le contengono. Per esempio l’operazione radice quadrata ( x
=sqrt(x)) o la potenza (xy = pow(x,y)) sono incluse in una libreria a parte e devono essere dichiarate prima di essere utilizzate. Per inserire la dichiarazione di tutte le funzioni matematiche basta includere all’inizio del programma il file di intestazioni math.h. Per farlo si può utilizzare la direttiva:
#include <math.h>
La quantità di funzioni esterne incluse nelle librerie fornite insieme al linguaggio è molto elevata, un elenco di quelle standard del linguaggio C, fornite insieme al compilatore g++ incluso nel pacchetto dev-C++, si può trovare nel file libc.htm presente sul server del laboratorio,
scaricato dal sito ufficiale dell’organizzazione gnu:
http://www.gnu.org/software/libc/manual
e su internet all’indirizzo:
http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html
oppure, più comodamente in una pagina con frame all’indirizzo:
http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html
Per avere informazioni sulle peculiarità del linguaggio C++ implementato nel compilatore
g++ della GCC (Gnu Compiler Collection) incluso nel DEV C++ si deve visitare il sito:
http://www.gnu.org/software/gcc/onlinedocs/
purtroppo solo in lingua inglese. Una copia del manuale in formato pdf (gcc.pdf) è presente
sul server nel laboratorio.
Se avete installato il visual basic potete ottenere molte informazioni sul C++ e le sue librerie
consultando la Microsoft Developer Network (ovvero la guida in linea di visual basic), nel
caso della versione 6.0, per avere un elenco alfabetico di tutte le funzioni disponibili nel C++
si può andare al capitolo Visual C++ documentation  Using visual C++  Visual C++
programmer guide  Run-Time Library reference  Alphabetic functions references. Anche
se questa guida descrive il linguaggio Microsoft Visual C++ la maggior parte delle informazioni in essa contenute sono valide anche per il linguaggio Dev-C++ visto che entrambi aderiscono allo standard ANSI.
Anche su Wikipedia trovate molte informazioni sul linguaggio C++ e sui compilatori disponibili. Trovate informazioni e spiegazioni anche per le librerie standard del C++ all’indirizzo:
http://en.wikipedia.org/wiki/C%2B%2B_Standard_Library
Per approfondire la conoscenza del linguaggio C++ un ottimo manuale è quello di Bruce Eckel, Thinking in C++, in due volumi, disponibile gratuitamente sul sito:
http://www.mindview.net/,
il primo volume disponibile anche in italiano, tradotto da Umberto Sorbo e altri, è presente sul
server del laboratorio e all’indirizzo http://www.umbertosorbo.it.
3.8
Esercizi
18) Individuare gli errori di sintassi nelle seguenti istruzioni:
a) cin >> a+2:
b) cout >> “Prova”;
c) FOR(a=1, a>10, a++);
48
Corso sul Linguaggio C++ Edizione 2010
d) a=b=c
19) Siano dati i seguenti cicli:
a) for (i = 0; i<n; i=i+2) ;
b) for (i = 0; i<n; i++) {i++; i++;}
c) i=n/2; while (i < n) i *=2;
d) i=n; do i-=n/2; while ( i >=0);
Completare la seguente tabella indicando quante volte verrà eseguito il ciclo ed il valore della
variabile i alla fine dello stesso supponendo che la variabile n = 10
Ciclo
a)
b)
c)
d)
N° di volte
Valore di i
20) Completare la tabella indicando cosa conterrà la variabile C dopo l'esecuzione delle seguenti istruzioni if annidate tenendo conto dei valori iniziali di A e B indicati nelle prime
due colonne:
if (A + B >10)
if (A > 10 && B < 10) C=A - B; ELSE C=A - B - 100;
else
if (A < 0 || B < 0) C= A + B; ELSE C= A - B + 100;
A
10
-10
5
60
B
20
-20
4
-10
C
21) Cosa fa il seguente frammento di programma?
int i=0;
while(i<50)
{if (i%2) cout << i << “\t”; i++;}
22) Considerate il seguente frammento di programma:
t=-1;
for (i=1; i<=n; i++) if (f(i)) t=i;
if (t>=0) cout << t ;
Quale delle seguenti affermazioni è corretta?
a) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale
intero non esiste, il programma entra in un ciclo infinito.
b) Il programma cerca e stampa il più grande intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito.
c) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale
intero non esiste, il programma non stampa nulla.
d) Il programma cerca e stampa il più grande x fra 1 e n tale che f(x)!=0; se tale intero
non esiste, il programma non stampa nulla.
49
Corso sul Linguaggio C++ Edizione 2010
23) Cosa scrivono in output i seguenti cicli?
a) for (int i=1; i<7;i++) cout << i %5<<’ ‘;
………………………………………….
b) for (int i=1; i<7;i++) cout << i*2-1 <<’ ‘;
………………………………………….
c) int i=10; do {cout << i<<’ ‘; i -=5;} while (i>0); ………………………………………
d) int j=5,i=5; while(i>0) {cout << j <<’ ‘; if (i%2) j +=i else j-=i; i--;} ….……………..
24) Considerate il seguente frammento di codice:
float i,f;
...
f=1/50.0;
for (i=0.0; i!=1.0; i+=f) printf("A");
Quale dei seguenti effetti ha il ciclo for indicato?
a)Stampa 50 volte il carattere A;
b)Stampa 51 volte il carattere A;
c)Stampa 49 volte il carattere A;
d) Il ciclo potrebbe non terminare, stampando infinite volte il carattere A; e)Il compilatore segnala un errore;
Solo selezione
25) Data la misura dei tre lati di un triangolo dire se è isoscele scaleno o equilatero.
26) Un lanificio acquista lana dai pastori della zona. La lana acquistata è di tre tipi diversi (che
si acquistano a prezzi diversi), il prezzo inoltre cambia in base all'umidità, se questa supera il 30% il prezzo di acquisto si riduce del 15%. Fare un programma che aiuti l'impiegato a calcolare la somma da versare ai pastori che portano la lana.
27) Una cooperativa agricola vende ai propri soci il vino che produce al prezzo di € 1 al litro
se bianco, 1.5 se rosso e 2 se D.O.C. E' possibile prendere il vino sfuso o imbottigliato, in
questo secondo caso il prezzo aumenta ulteriormente di €. 0.20 a litro. Descrivere un
programma per calcolare il prezzo del vino ogni volta che viene un cliente.
28) Un commerciante, in un mese, ha acquistato merce per X lire e ne ha venduto per Y lire.
Sapendo che l'aliquota IVA sulla merce trattata è il 20% calcolare l'IVA da versare o l'IVA a credito a fine mese.
29) Fare un programma per calcolare l'IRPEF su un reddito assegnato. L’IRPEF, in base alla
finanziaria 2007, si calcola così:
a. Se il reddito è inferiore o uguale a 15.000€ si calcola il 23% del reddito
b. se supera i 15.000 ma é inferiore a 28.001 si calcola il 23% sui primi 15.000 (=€.3450) + il
27% sulla parte eccedente i 15.000€.
c. Se supera i 28.000 ma è inferiore ai 55001€ l’imposta dovuta è data da 6960€ (pari al massimo dello scaglione precedente) + il 38% sulla parte di reddito eccedente 28.000€
d. d. da
55.000€
fino
a
75.000€
l’imposta
è
pari
a
€
17.220
(15000*0,23+13000*0,27+27000*0,38) + il 41% della parte di reddito eccedente 55.000€
e. e. oltre € 75.000 l’imposta è pari a 25420€ + il 43% della parte di reddito che supera 75000
euro.
graficamente gli scaglioni possono essere rappresentati così:
27%
23%
15000
38%
43%
41%
28000
55000
50
75000
43%
Corso sul Linguaggio C++ Edizione 2010
30) Far calcolare il prezzo di una quantità assegna di vino che costa € 2.1 al litro supponendo
che se si acquista una quantità >= 20 litri si ha diritto al 10% di sconto.
31) Data in input una cifra qualsiasi, dopo averla arrotondata alle decine (per eccesso o per difetto a seconda che il resto superi 5 o no), indicare quanti biglietti da 10€, quanti da 50 €
e quanti da 100€ sono necessari per pagare la somma inserita in input utilizzando il minor
numero possibile di biglietti.
Cicli
32) Dato in input un numero intero N calcolare N fattoriale.
33) Dato in input un numero qualsiasi x e un numero intero n positivo, calcolare il risultato di
xn.
34) Assegnati n e d calcolare la somma dei primi n termini della progressione geometrica di
ragione d: 1 + d + d2 + d3 + …. + dn
35) Assegnati n e d calcolare la somma dei primi n termini della progressione aritmetica di ragione d:
1+ 2d+ 3d+ 4d+ …+ nd
36) Calcolare il prodotto di due numeri A e B interi effettuando solo addizioni.
37) Calcolare il quoziente e il resto della divisione tra due numeri interi A e B effettuando solo sottrazioni.
38) Dato in input un numero intero qualsiasi controllare se è primo oppure no e visualizzare la
risposta.
39) Dati due numeri interi A e B trovare il minimo comune multiplo.
40) Dati due numeri interi A e B trovare il massimo comun divisore con l’algoritmo di Euclide. Euclide ha dimostrato che, detti A e B due numeri interi con A>=B, se A è divisibile
per B allora MCD(A,B) = B altrimenti MCD(A,B) = MCD(B, A%B). In pratica si può
cercare il MCD tra due numeri più piccoli. Ripetendo questa operazione fino a quando i
due numeri diventano divisibili si trova il Massimo Comun Divisore.
41) Dato in input un numero intero N visualizzare la somma dei suoi divisori.
42) Calcolare la somma dei numeri interi da 1 a n (n in input): 1+ 2 + 3 + 4 + ….. + n (verificare che corrisponde a n*(n+1)/2)
43) Far calcolare la somma dei primi n termini (N in input) della seguente successione:
1+ ½+ 1/3+ ¼ + 1/5 + 1/6 + 1/7+ ……+ 1/n
44) Far calcolare la somma dei primi n numeri dispari e verificare che corrisponde sempre a
n*n
45) Scrivere le istruzioni per far visualizzare i primi n termini (n in input) delle seguenti successioni di numeri:
51
Corso sul Linguaggio C++ Edizione 2010
a)
b)
c)
d)
2
1
1
2
4 6 8 10 12 14 ...
-3 5 -7 9 -11 13 ...
2 9 4 25 6 49 8…
3 4 5 8 9 16 17 32 33…
46) Far visualizzare su video un menù con una serie di voci tipo quelle dell’esempio sottostante, chiedere la scelta e controllare che venga inserito un valore valido, visualizzare
l’ozione corrispondente alla scelta, far terminare il programma quando viene scelta la
funzione corrispondente a Fine.
Possibili scelte
1) Funzione 1
2) Funzione 2
3) Funzione 3
4) Funzione 4
5) Fine
Quale scegli?
47) Far calcolare la somma dei quadrati dei primi n numeri interi e verificare che corrisponde
a n*(n+1)*(2*n+1)/6
48) Far calcolare la somma della seguente successione di numeri interi:
1*N + 2*(N-1)+ 3*(N-2)+ …… +(N-1)*2 + N*1
si tratta di sommare una serie di prodotti di due numeri dove il primo fattore varia da 1 a N
mentre il secondo diminuisce da N a 1. (Questa somma, qualunque sia N è sempre =
N*(N+1)(N+2)/6 )
49) Far Calcolare la somma della seguente successione di numeri interi:
1*2 + 2*3 + 3*4 + 4*5 + …….. + (n-1)*n
il primo fattore va da 1 a n-1 mentre il secondo da 2 a N. (Questa somma è sempre uguale
a n*(n+1)(2*n-2)/6)
50) Calcolare la somma della seguente successione e verificare che corrisponde a
n*(n+1)*(2*n+1)/3
n*(n+1) + (n-1)*(n+2)+ (n-2)*(n+3)+ ……….+ 2*(n+n-1) + 1*(n+n)
51) Far visualizzare la tabella dei codici ASCII.
52) Calcolare la somma della successione:
Nel ciclo fermarsi quando l’ultimo termine sommato è inferiore ad un numero E dato in
input.
in( x) = y −
y2
2
+
y3
3
−
52
y4
4
+
y5
5
+ ....
Corso sul Linguaggio C++ Edizione 2010
supponendo di avere in input x, con 0 <x < 2 e che y = x-1. Fermare il ciclo quando il valore assoluto dell’ultimo termine sommato è minore dell’errore E dato anch’esso in input.
Il risultato è il logaritmo naturale di x approssimato con un errore massimo = E.
53) Dato in input un numero intero n trasformarlo in binario.
54) Far visualizzare i primi n termini della successione di Fibonacci:
1; 1; 2; 3; 5; 8; 13; 21; ….. (ogni numero è la somma dei due precedenti e i primi due sono
entrambi = 1).
55) Dato in input un numero intero n e trasformarlo nel sistema di numerazione a base b (b in
input)
56) Dato un numero x scritto in un sistema a base b (x e b in input) trasformarlo in decimale.
57) Scrivere il programma che, dato in input x, calcoli la somma della seguente successione:
sen( x) = x −
−
+
+
+ ....
x9
x5
x7
x3
7!
9!
5!
3!
come si vede alcuni i termini vengono sommati o sottratti alternativamente. Il risultato, se
si continua all’infinito, è la funzione trigonometrica sen(x). Considerato che non si può
continuare all’infinito, fermarsi quando il valore assoluto dell’ultimo termine sommato è
minore dell’errore E dato in input (si può dimostrare che la somma di tutti i restanti termini è sempre minore del valore assoluto dell’ultimo termine sommato).
58) Come nell’esercizio precedente scrivere il programma che, dato in input un numero reale
x, calcoli la somma della seguente successione:
cos(x) = 1 −
+
−
+
+ ....
x2
x4
x6
x8
2!
4!
6!
8!
Sommando gli infiniti termini di questa successione si calcola la funzione coseno(x). Nel
ciclo fermarsi quando il valore assoluto dell’ultimo termine è minore dell’errore E dato in
input.
59) Come per gli esercizi precedenti far calcolare la somma della seguente successione (x in
input):
ex = 1 + x +
+
+
+
+ ....
x2
x3
x4
x5
2!
3!
4!
5!
x
in questo caso viene calcolato e , fermarsi quando si somma il termine n, tn = xn/n! con
n>x e abs(tn)*(n/(n-x))< E, dove E è l’errore massimo dato in input.
60) Scrivere un programma che dato in input un numero qualsiasi lo visualizzi invertendo
l’ordine delle cifre.
Doppio ciclo
61) Far visualizzare la tavola pitagorica fino ad un numero N assegnato.
53
Corso sul Linguaggio C++ Edizione 2010
62) Far visualizzare i primi n numeri primi (può essere utilizzato il programma per controllare
se un numero è primo oppure no).
63) Far calcolare X elevato a N utilizzando solo addizioni (si possono utilizzare i programmi
per fare la potenza e la moltiplicazione).
64) Far visualizzare tutti gli elementi della serie di Fibonacci (vedi esercizio 54) che siano anche numeri primi (composto dai due programmi Fibonacci + controllo numeri primi).
65) Scomporre un numero qualsiasi in fattori primi.
66) Assegnata una frazione A/B qualsiasi ridurla ai minimi termini
67) Cercare e far visualizzare tutti i numeri perfetti minori o uguali ad un numero N dato in
input. I numeri perfetti sono quelli per i quali la somma dei divisori (escluso il numero
considerato) è uguale al numero stesso, per esempio 6 (6=1+2+3) oppure 28
(28=1+2+4+7+14).
68) Scrivere un programma che faccia funzionare un computer come un registratore di cassa.
Deve permettere di inserire i prezzi dei prodotti acquistati da ciascun cliente e stampare
lo scontrino. A fine giornata deve visualizzare il totale incassato, il numero degli scontrini emessi, l'importo delle vendite diviso per quattro categorie merceologiche: Alimentari,
Detersivi, Salumeria, Altro.
69) Assegnato N far visualizzare tutti i numeri primi minori di N che fanno parte della serie di
Fibonacci.
70) Far leggere una serie di numeri in input e fare la media solo dei numeri primi.
71) Far generare tutte le possibili combinazioni di risultati di 4 partite di calcio. (es; 1111,
111X, 1112, 11X1, 11XX, 11X2, 1121 ecc. Si tratta delle permutazioni con ripetizione di
3 elementi a 4 a 4. Si può, per esempio, usare il sistema di num. ternario incrementando
una variabile e trasformandola in ternario).
Risposte esercizi
Esercizio 18):
a) dopo >> ci deve essere una variabile e non un’espressione
b) dopo cout è permesso solo l’operatore <<
c) Il linguaggio C++ è case sensitive, FOR non è la stessa cosa di for, inoltre
per separare l’istruzione di inizializzazione dalla condizione e dall’istruzione di
incremento bisogna usare il punto e virgola (;) e non la virgola.
d) manca il punto e virgola finale
Esercizio 19)
Ciclo
a)
b)
c)
d)
N° di volte
5
4
1
3
Esercizio20)
54
Valore di i
10
12
10
-5
Corso sul Linguaggio C++ Edizione 2010
A
B
C
10
20
-110
-10
-20
-30
5
4
101
60
-10
70
Esercizio 21): scrive su video tutti i numeri dispari minori di 50.
Esercizio 22) esatta d)
Esercizio 23)
a)
b)
c)
d)
for (int i=1; i<7;i++) cout << i %5<<’ ‘; …1 2 3 4 0 1 ……….…….
for (int i=1; i<7;i++) cout << i*2-1 <<’ ‘; ………………1 3 5 7 9 11 …………………………..
int i=10; do {cout << i<<’ ‘; i -=5;} while (i>0); …………10 5……………………………………
int j=5,i=5; while(i>0) {cout << j <<’ ‘; if (i%2) j +=i else j-=i; i--;} …5 10 6 9 7……………….. .
Esercizio 24) esatta d): il tipo double ha una rappresentazione interna in binario virgola mobile e un numero con la virgola tipo 1/50 potrebbe diventare periodico quando viene trasformato
in binario - anzi 1/50 è periodico in binario perché non danno luogo a numeri binari periodici
solo le frazioni che si possono rappresentare con un denominatore che è una potenza del 2 - e
quindi ha infinite cifre per cui non può essere rappresentato esattamente, questo significa che
sommando 50 volte 1/50 scritto in binario in maniera non esatta, probabilmente non otterremo
1 e quindi il ciclo potrebbe non finire mai perché non si verifica mai la condizione i=1.0 che
fa terminare il ciclo.
Esercizio 40) Il cuore del programma per trovare il MCD potrebbe essere il seguente:
if (A < B) {r=A; A=B; B=r;} // ordina i due numeri
r=A%B; while (r > 0) {A=B; B=r; r=A%B;}
MCD = B;
Esercizio 46) Il programma potrebbe essere il seguente:
int main(){
short int scelta=0;
/* ripete la visualizzazione del menù fino a quando non si sceglie la
funzione FINE */
do {
// visualizza il menù
system("CLS"); // cancella la finestra di output
cout << endl << "\t\tPossibili scelte" << endl;
cout << "\t\t1) Funzione 1"<< endl;
cout << "\t\t2) Funzione 2"<< endl;
cout << "\t\t3) Funzione 3"<< endl;
cout << "\t\t4) Funzione 4"<< endl;
cout << "\t\t5) Fine"<< endl;
cout << "\n\n\t\t\tQuale Scegli (1-5)?" ;
cin >> scelta; // chiede la scelta
// visualizza messaggio di errore se la scelta non è valida
if (scelta <1 || scelta > 5) {
cout << "\n\t\tErrore! Inserire un numero da 1 a 5!\n";
system("PAUSE");
} else {
// esecuzione della funzione corrispondente
switch (scelta) {
case 1: cout <<"E' stata scelta la funzione 1\n"; break;
case 2: cout <<"E' stata scelta la funzione 2\n"; break;
case 3: cout <<"E' stata scelta la funzione 3\n"; break;
case 4: cout <<"E' stata scelta la funzione 4\n"; break;
}
if (scelta < 5) system("PAUSE");
}
55
Corso sul Linguaggio C++ Edizione 2010
} while (scelta != 5);
return 0;}
Esercizio 51) Si può sfruttare la compatibilità tra il tipo di dati char e il tipo int più il fatto che
l’istruzione cout visualizza una variabile char come carattere e una int come numero. Il programma potrebbe essere il seguente:
int i; char x;
for (i=1; i<=255; i++) {x = i; cout << i << '=' << x << '\t';}
cout << endl;
56
4
Capitolo
Corso sul Linguaggio C++ Edizione 2010
4. I sottoprogrammi
I circuiti del computer sanno fare solo poche operazioni elementari le quali possono essere utilizzate nei programmi per costruire operazioni più complesse. Una volta scritto un programma, il computer diventa capace di compiere un'operazione in più rispetto a quelle che sapeva fare prima, quella realizzata attraverso il programma che, quindi, corrisponde a più istruzioni elementari.
Per esempio i circuiti del computer non sono in grado di eseguire la radice quadrata di un numero ma, come abbiamo visto, esiste il programma sqrt, incluso nelle librerie di programmi
fornite con il compilatore C++, che permette di svolgere questa operazione. Quando il computer calcola una espressione che comprende, per esempio, ‘sqrt(x)’, verrà prima eseguito il
programma per calcolare la radice quadrata di x, poi verrà calcolato il resto dell’espressione
sostituendo alle parole ‘sqrt(x)’ il risultato di questo programma. Non ha nessuna importanza,
per i programmatori, il fatto che il calcolo della radice sia stata fatta con l’uso di un programma invece che direttamente dai circuiti dell’unità aritmetico-logica inclusa nel processore, il
programma resta lo stesso.
Se un programma viene inserito in un altro programma che lo manda in esecuzione e dopo
prosegue con le proprie istruzioni, allora il programma viene chiamato sottoprogramma
mentre l’altro programma viene chiamato programma principale. Un sottoprogramma può
essere a sua volta programma principale se richiama altri sottoprogrammi. Qualsiasi programma può essere utilizzato come sottoprogramma da un altro programma.
Tutti i programmi scritti per un computer fanno aumentare le cose che il computer può fare e i
programmatori possono, nei programmi successivi, utilizzare tutte le operazioni complesse
fatte dai programmi precedenti come se fossero operazioni elementari.
Le istruzioni sono i mattoni che ci permettono di costruire i programmi, usare i sottoprogrammi significa usare delle parti prefabbricate e quindi ridurre la complessità dei programmi.
Conviene sempre usare i sottoprogrammi ma in particolar modo conviene usarli quando lo
stesso gruppo d'istruzioni deve essere inserito in più punti del programma. In questo caso, invece di scrivere ogni volta queste istruzioni, conviene costruire un sottoprogramma e inserire
nel programma principale, ogni volta che serve, una chiamata al sottoprogramma.
Nella maggior parte dei linguaggi di programmazione esistono due tipi di sottoprogrammi: le
funzioni e le procedure. La differenza tra i due sta nel fatto che le prime restituiscono al programma principale un risultato che viene sostituito alla chiamata della funzione (come abbiamo visto per la funzione sqrt()) mentre le procedure non restituiscono nessun valore, semplicemente sono un gruppo di istruzioni che possono essere mandate in esecuzione in qualsiasi
punto del programma principale.
57
Corso sul Linguaggio C++ Edizione 2010
Con il linguaggio C++ esiste un solo tipo di sottoprogramma, le funzioni, ma una funzione
può anche non restituire alcun valore e quindi corrispondere alle procedure degli altri linguaggi.
4.1
Le funzioni
Per inserire una funzione in un programma bisogna rispettare la seguente sintassi
Definizione di una funzione
tipo di dato
nome funzione
void
elenco parametri
formali
(
Intestazione
)
blocco
istruzioni
Corpo
L’ultimo pezzo, il blocco istruzioni, corrisponde ad un elenco di istruzioni e dichiarazioni racchiuse tra un coppia di parentesi graffe, quelle che verranno eseguite ogni volta che dal programma principale si richiama la funzione, e viene anche detto corpo della funzione.
Tutta la prima parte della definizione, ovvero quella che si ottiene togliendo il blocco istruzioni, viene anche detta intestazione della funzione.
Il tipo di dato che si premette al nome della funzione indica il tipo del risultato calcolato dalla
funzione e può essere uno qualsiasi dei tipi definiti compreso il tipo void che significa nessun
tipo o assenza di tipo. Le funzioni definite con il tipo void non forniscono risultati e quindi
corrispondono alle procedure degli altri linguaggi. Se si omette il tipo di dato verrà assunto il
tipo int.
Nel corpo della funzione ci deve essere sempre l’istruzione return eventualmente seguita da
un’espressione, a meno che la funzione non sia stata definita come void nel qual caso
l’istruzione return si può omettere perché il compilatore la inserisce automaticamente dopo
l’ultima istruzione del blocco.
L’istruzione return serve sia per far terminare l’esecuzione della funzione che per definire il
risultato della stessa. Infatti, dopo una qualsiasi esecuzione della funzione, il risultato restituito sarà proprio quello fornito dall’espressione presente dopo la parola return. Questo risultato
deve essere conforme al tipo di dati specificato prima del nome della funzione
nell’intestazione.
Il nome della funzione è un normale identificatore e può essere un nome qualsiasi inventato
dal programmatore, bisogna però rispettare le regole già viste per i nomi delle variabili nel paragrafo 2.2, quindi deve iniziare con una lettera, può contenere solo lettere e cifre, non può
contenere spazi, simboli di punteggiatura e altri simboli speciali con l’eccezione del simbolo
“_”, non può essere una parola chiave del linguaggio, non può contenere lettere accentate e, in
genere, solo i primi 32 caratteri verranno considerati (in alcune versione del linguaggio C solo
i primi 8 caratteri). Conviene scegliere nomi brevi ma significativi in modo da ricordarci,
quando rileggiamo il programma, a cosa serve la funzione.
Per esempio le seguenti righe sono tutte intestazioni valide di funzioni:
double media(double num1, double num2, double num3);
int calcolo(int x=0, int y=0);
58
Corso sul Linguaggio C++ Edizione 2010
void ordina(float x, float y);
Una funzione, normalmente, esegue un calcolo utilizzando i dati forniti dal programma principale, per esempio se nel programma principale scriviamo sqrt(4) il risultato sarà 2.0, se invece scriviamo sqrt(25) il risultato sarà 5.0. I dati forniti dal programma principale si chiamano parametri della funzione. La funzione sqrt richiede un solo parametro ovvero il valore di
cui vogliamo calcolare la radice quadrata. Quando si definisce una funzione bisogna anche
indicare il tipo e il numero dei parametri che il programma principale gli fornirà ogni volta
che la manda in esecuzione, per questo motivo nella dichiarazione delle funzioni si inserisce
anche un elenco parametri formali racchiuso tra parentesi tonde. L’elenco ha la seguente sintassi:
elenco parametri formali
tipo di dato
nome parametro formale
=
valore di default
,
È possibile anche definire funzioni che non hanno bisogno di parametri in questo caso si può
omettere l’elenco parametri formali o utilizzare in sua vece la parola chiave void. Per esempio le due intestazioni seguenti sono equivalenti:
int prova();
int prova(void);
Quando si manda in esecuzione una funzione per la quale sono stati definiti dei parametri
formali il programma principale deve fornire i valori che devono essere utilizzati dalla funzione durante l’esecuzione e questi valori, detti parametri attuali, devono corrispondere per
numero e tipo ai parametri formali definiti e devono essere forniti nello stesso ordine con cui
sono elencati i parametri formali.
Il richiamo di una funzione, ovvero il comando per mandarla in esecuzione, può essere inserito in qualsiasi punto nel corpo del programma principale. La sintassi è:
Chiamata di funzione
nome funzione
(
Parametro attuale
)
,
Se per un parametro si specifica il valore di default vuol dire che il parametro è facoltativo e
quindi può essere omesso quando si richiama la funzione. Se un parametro facoltativo viene
omesso, al suo posto viene utilizzato il valore di default specificato nella definizione. Se si
omette un parametro facoltativo ma non i successivi, bisogna in ogni caso inserire le virgole
nell’elenco dei parametri attuali anche per i parametri omessi.
Problema scrivere una funzione per calcolare la potenza intera di un numero qualsiasi
L’operazione di elevamento a potenza, come abbiamo già detto, non fa parte delle operazioni
di base presenti nel linguaggio C++. Viene fornita, però, insieme al Dev-C++, inclusa nella
libreria di funzioni matematiche, anche la funzione pow la cui definizione è
59
Corso sul Linguaggio C++ Edizione 2010
double pow(double x, double y)
e che quindi calcola la potenza xy con y esponente reale qualsiasi.
Supponiamo però di volerci creare una nostra funzione potenza, semplificata rispetto a quella
fornita con il Dev-C++, che esegua l’elevazione a potenza di un numero reale qualsiasi con un
esponente intero relativo.
Oltre a definire la funzione, che potremmo chiamare pow come l’analoga fornita con il linguaggio, bisogna scrivere anche un programma principale main() per provarla.
Il programma completo potrebbe essere il seguente:
#include <iostream.h>
#include <stdlib.h>
double pow( double x, int e=1)
{/*programma potenza. Calcola x elevato a e, con e intero relativo qualsiasi */
double ris=1.0;
for (; e>0; e--) ris *=x; // se e è positivo
while (e<0) {ris /=x; e++;}
// se e è negativo
return ris; }
int main()
{ // chiede base ed esponente e calcola la potenza
double base; int esp;
cout.precision(14); //visualizza i risultati con 14 cifre diprecisione
cout << "Inserisci la base: "; cin >> base;
cout << "Inserisci l'esponente: "; cin >> esp;
cout << "Potenza = " << pow(base, esp) << "\tbase = " << pow(base) <<
endl;
system("PAUSE");
return 0;
}
La funzione è stata definita con due parametri formali (base ed e). Arbitrariamente, per mostrare come si utilizzano i parametri facoltativi, è stato impostato come facoltativo il secondo
parametro, l’esponente. Nel programma principale la funzione viene richiamata due volte
nell’istruzione:
cout << "Potenza = " << pow(base, esp) << "\tbase = " << pow(base) <<
endl;
la prima volta con entrambi i parametri e la seconda con un solo parametro.
Si noti l’istruzione cout.precision(14) che porta a 14 le cifre complessive visualizzate per ogni
numero in output dall’oggetto cout, normalmente sono solo 7.
4.2
Il passaggio dei parametri
Le istruzioni che costituiscono una funzione vengono eseguite quando la funzione viene richiamata e, solo in quel momento, vengono create tutte le variabili locali definite nel corpo
della funzione. Anche i parametri sono variabili locali e quindi possono essere utilizzate solo
all’interno del blocco che costituisce il corpo della funzione dove sono state dichiarate, ma
non è detto che vengano create nel momento in cui la funzione viene richiamata, infatti, il
passaggio dei parametri dal programma principale ad un sottoprogramma può avvenire in due
modi: per valore o per riferimento.
Se il passaggio di un parametro avviene per valore, quando il sottoprogramma parte viene
creata una nuova variabile locale che viene inizializzata con il valore del parametro attuale
fornito dal programma principale. Quindi il sottoprogramma utilizzerà una sua variabile locale che occupa in memoria uno spazio diverso rispetto a quello della variabile eventualmente
fornita come parametro attuale dal programma principale e che, solo inizialmente, conterrà lo
60
Corso sul Linguaggio C++ Edizione 2010
stesso valore. Tutte le modifiche che il sottoprogramma effettuerà sul valore della variabile
non modificheranno il valore della corrispondente variabile del programma principale. Quando termina l’esecuzione del sottoprogramma questa variabile locale verrà cancellata come tutte le altre variabili locali e il suo valore verrà perso, mentre la variabile del programma principale fornita come parametro attuale non sarà stata toccata.
Quando, invece, il passaggio avviene per riferimento il programma principale non passa al
sottoprogramma il valore della variabile ma la posizione in memoria della stessa. In questo
caso il sottoprogramma, quando parte, non crea una nuova variabile locale ma usa quella del
programma principale semplicemente chiamandola con un altro nome. Quindi se il sottoprogramma modifica il valore del parametro questa modifica si ripercuote sul corrispondente valore della variabile corrispondente del programma principale. In pratica, durante l’esecuzione
del sottoprogramma, la variabile corrispondente al parametro formale e quella corrispondente
al parametro attuale condividono lo stesso spazio di memoria e quindi, in realtà, corrispondono ad un'unica variabile che però ha due nomi diversi.
Normalmente il passaggio dei parametri avviene sempre per valore salvo quando si premette
al nome di un parametro formale l’operatore unario &. Questo operatore, posto davanti al
nome di una variabile, produce come risultato la posizione della variabile in memoria.
Per esempio se una variabile intera di nome somma contiene il valore 10 vuol dire che in memoria, nei quattro bytes assegnati alla variabile, è memorizzato il numero 10 scritto in binario.
La memoria di ogni computer è divisa in celle numerate progressivamente e il numero assegnato ad ogni cella è detto indirizzo. Supponendo che ogni cella sia costituita da otto bit (un
byte) e che alla variabile somma siano assegnate le celle con indirizzo 10320, 10321, 10322 e
10323 il valore dell’espressione &somma sarà 10320 e non dipenderà dal valore della variabile anzi, qualunque sia il valore della variabile, il risultato dell’operazione
&<nome variabile>
corrisponderà sempre alla posizione in memoria che è stata scelta per la variabile al momento
della sua creazione.
Quindi il compilatore assume che un parametro sarà passato per riferimento quando il programmatore ha posto, nella lista dei parametri formali, davanti al nome del parametro
l’operatore &, mentre, in mancanza di questo operatore, assume il passaggio per valore. Per
esempio:
int calcolo(int x, int y); // entrambi i parametri saranno passati per valore
void somma(int &ris, int n); // ris per riferimento
n per valore
void scambia(double &a, double &b); // entrambi i parametri per riferimento
Problema: scrivere un sottoprogramma per scambiare il contenuto di due variabili
Il sottoprogramma, che chiameremo scambia, riceve come parametri le due variabili da scambiare e ne scambia il contenuto.
#include <iostream.h>
#include <stdlib.h>
void scambia(int &a, int &b);
int main()
{ // Programma scambia. Prova del sottoprogramma scambia
int prima=10, seconda=20;
cout << prima << ", " << seconda << endl;
scambia(prima, seconda);
cout << prima << ", " << seconda << endl;
system("PAUSE");
return 0;
61
Corso sul Linguaggio C++ Edizione 2010
}
void scambia(int &a, int &b)
{// sottoprogramma che scambia il contenuto di a con b
int tmp;
tmp = a; a=b; b=tmp; }
Mandandolo in esecuzione verranno visualizzati su video prima i due numeri “10, 20” e poi,
dopo l’esecuzione della funzione scambia, gli stessi numeri in ordine invertito “20, 10”.
In questo caso, affinché il sottoprogramma svolga la funzione per la quale è stato progettato,
ovvero quella di scambiare il contenuto di due variabili, il passaggio dei parametri deve avvenire per riferimento perché solo in questo modo nel sottoprogramma verranno utilizzate proprio le due variabili del programma principale e non due copie delle stesse. Se, invece, i parametri venissero passati per valore, l’esecuzione del sottoprogramma non comporterebbe
nessuna modifica del contenuto delle variabili del programma principale e quindi la sue esecuzione non avrebbe nessun effetto.
4.3
I prototipi delle funzioni
Si noti che nel programma precedente l’intestazione della funzione scambia è stata messa due
volte, una volta all’inizio e la seconda dopo la funzione main, quando è stata inserita la definizione completa della funzione stessa. La riga che si trova all’inizio e che contiene solo
l’intestazione della funzione seguita dal punto e virgola si chiama prototipo della funzione.
Questo dipende dal fatto che ogni oggetto che si utilizza in un programma C++ deve essere
dichiarato prima del suo utilizzo, perché il compilatore ogni volta che incontra un identificatore deve sapere di cosa si tratta, questo vale sia per le funzioni che per le variabili. Nel caso che
stiamo esaminando, la funzione scambia veniva utilizzata all’interno della funzione main che
precede la definizione della funzione scambia. Se non avessimo inserito il prototipo della funzione scambia prima della definizione della funzione main il compilatore avrebbe segnalato
l’errore di mancata dichiarazione dell’oggetto in corrispondenza della riga:
scambia(prima, seconda);
dove viene richiamata la funzione, per evitare questo errore è stato necessario inserire il prototipo:
void scambia(int &a, int &b);
prima della definizione del programma principale (funzione main), questo dà al compilatore
tutte le informazioni che gli sono necessarie sull’oggetto scambia dicendogli anche che sarà
definito dopo.
Avremmo potuto evitare di inserire il prototipo scrivendo la definizione della funzione scambia prima del programma principale main così come è stato fatto nel programma per il calcolo
della potenza visto al paragrafo 4.1. Quando il programma contiene più funzioni può capitare
che una funzione A richiami al suo interno una funzione B, in questo caso la definizione della
funzione B o il suo prototipo deve precedere la funzione A, ma se capita che anche la funzione B richiama la funzione A allora si deve ricorrere obbligatoriamente al prototipo di almeno
una delle due funzioni inserendolo prima della definizione di entrambe le funzioni.
I programmatori C, in genere, premettono all’inizio del programma i prototipi di tutte le funzioni definite all’interno dello stesso, prima di tutto perché in questo modo all’inizio del programma c’è un elenco di tutte le funzioni che vale come documentazione e poi perché così
non è più necessario stare attenti all’ordine con cui devono essere inserite le definizione delle
funzioni nel programma.
62
Corso sul Linguaggio C++ Edizione 2010
4.4
Le principali funzioni presenti nelle librerie
standard del linguaggio C
Come già detto nel paragrafo 3.7 le funzioni predefinite incluse nelle librerie fornite con il
linguaggio C e con il linguaggio C++ sono moltissime, un elenco dettagliato di quelle standard del linguaggio C si può trovare nel file libc.htm presente sul server e scaricabile
dall’indirizzo:
http://www.gnu.org/software/libc/manual
oltre a quelle elencate in questo file sono disponibili per il linguaggio C++ altre funzioni e
oggetti, come quelli inclusi nel file di header <iostream.h>, che sono presenti nelle versioni
standard del C++ ma non nel linguaggio C.
Nella tabella che segue si fornisce un piccolo elenco con le funzioni utilizzate in questo libro,
con indicato il file di header che è necessario includere per utilizzarle e la pagina dove è possibile trovare esempi o una descrizione.
Tabella 3 Elenco di alcune funzioni e costanti definite nelle librerie standard del C e utilizzate in
questo libro
Nome
header
sqrt
pow
math.h
math.h
log
exp
ceil
math.h
math.h
math.h
floor
math.h
trunc
math.h
rint
M_PI
RAND_MAX
rand
math.h
math.h
stdlib.h
stdlib.h
srand
stdlib.h
system
stdlib.h
strcpy
string.h
strncpy
string.h
strlen
string.h
strcat
string.h
strncat
string.h
Descrizione
radice quadrata di un numero x
Elevazione a potenza. Calcola x elevato a y
prototipo
pag.
double sqrt (double x)
47,48
double pow (double base, double
48
esponente)
Calcola il logaritmo naturale di un numero x
double log (double x)
Calcola e elevato a x (funzione inversa di log())
double exp (double x)
Arrotonda per eccesso un numero al primo intedouble ceil (double x)
ro più grande (es 1.5  2)
Arrotondo per difetto un numero al primo intero
double floor (double x)
più piccolo (es 1.5  1 e -1.5 -2).
Elimina la parte dopo la virgola da un numero
double trunc (double x)
(es 1.5  1.0 e -1.5  -1.0)
arrotonda al più vicino numero intero
double rint (double x)
Costante= Π (pi greco)
Costante=numero massimo generato con rand
76
genera un numero pseudocasuale nell’intervallo
int rand (void)
76
0, RAND_MAX
imposta il numero che verrà utilizzato per gene- void srand (unsigned int num)
77
rare il prossimo numero casuale
Manda in esecuzione un qualsiasi comando del int system (const char *comando)
5
sistema operativo o un qualsiasi programma eseguibile
copia stringa2 in stringa1
char * strcpy (char *stringa1,
84
const char *stringa2)
copia esattamente n caratteri da stringa2 in char * strncpy (char *stringa1,
84
stringa1. Se stringa2 è più corta aggiunge ‘\0’.
const char *stringa2, unsigned int
n)
calcola l’effettiva lunghezza di una stringa, in unsigned int strlen (const char
84
pratica la posizione al suo interno del carattere
*s)
‘\0’
Concatena due stringhe, stringa2 viene aggiunta
char * strcat (char *stringa1,
85
a stringa1
const char *stringa2)
Aggiunge esattamente n caratteri a stringa1
char * strncat (char *stringa1,
85
const char *stringa2, unsigned int
63
Corso sul Linguaggio C++ Edizione 2010
Nome
header
strcmp
string.h
strstr
string.h
printf
stdio.h
fprintf
stdio.h
fopen
stdio.h
feof
stdio.h
scanf
stdio.h
fscanf
stdio.h
puts
stdio.h
fputs
stdio.h
gets
stdio.h
fgets
stdio.h
getchar
stdio.h
fgetc
stdio.h
time
time.h
Descrizione
prototipo
n)
confronto tra due stringhe il risultato è zero se int strcmp (const char *s1, const
sono uguali, altrimenti è la distanza tra I primi
char *s2)
due caratteri diversi.
cerca stringa2 in stringa1 e se la trova restituichar * strstr (const char
sce un puntatore alla posizione altrimenti resti- *stringa1, const char *stringa2)
tuisce il puntatore NULL
scrive in output il risultato di una serie di e- int printf (const char *schema,
spressioni, formattandolo in base ad uno sche...)
ma. Restituisce il numero di caratteri stampati
come printf ma può essere utilizzata per scrive- int fprintf (FILE * stream, const
re in un file su disco
char *schema, ...)
apre il flusso ovvero crea un collegamento tra il
FILE * fopen (const char
programma e il file su disco indicato nel primo *filename, const char *opentype)
parametro; restituisce un puntatore all’oggetto
FILE corrispondente
Restituisce un valore diverso da zero se e solo
int feof (FILE *stream)
se è stata raggiunta la fine del file; alcune funzioni, come fgetc, restituiscono EOF se si raggiunge la fine del file ma anche se si verifica un
errore!
Legge una serie di dati da un fil di input e li tra- int scanf (const char *schema, ...)
sferisce in una serie di variabili. Il risultato è il
numero delle assegnazioni effettuate
Come scanf ma può essere utilizzata per leggere int scanf (FILE *stream, const
da un file su disco
char *schema, ...)
Scrive una stringa nel file stdout (normalmente
int puts (const char *s)
il video)
Come puts() ma invia i dati nel file specificato
int fputs (const char *s, FILE
senza il carattere ‘\n’
*stream)
legge una stringa dal file stdin (normalmente la
char * gets (char *s)
tastiera), legge tutti I caratteri fino al carattere
‘\n’ che non considera
legge fino alla fine della riga ma al massimo char * fgets (char *s, int count,
count-1 caratteri da un file e li strasferisce nella
FILE *stream)
stringa s che deve essere di dimensione count
perché alla fine viene aggiunto ‘\0’. Legge anche gli eventuali spazi e il carattere di fine riga
legge un carattere dal file stdin, se c’è un errore
int getchar (void)
viene restituita la costante EOF (normalmente =
-1)
come getchar() ma viene utilizzata per leggere
int fgetc (FILE *stream)
un carattere da un file su disco
restituisce il numero di secondi trascorsi dalla
time_t time (time_t *result)
mezzanotte del 1/1/1970 considerati in base al
tempo UTC(Universal Time Coordinated che
corrisponde all’ora di Greenwich) , restituisce
un tipo di dati time_t equivalente a int. Se viene
fornito il parametro facoltativo il risultato viene
memorizzato anche nella variabile puntata dal
parametro
64
pag.
85
85
88,
106,
122
151
120
89
124,
151
90
121,
121
90,
122
121,
124
90
121
77
Corso sul Linguaggio C++ Edizione 2010
4.5
Meccanismo di esecuzione di un sottoprogramma e funzioni inline
Quando un programma è in esecuzione sia le istruzioni che lo costituiscono, sia le variabili
che utilizza, occupano spazio di memoria. Normalmente il programma e tutte le sue funzioni
occupano una parte di memoria detta segmento codice, le variabili globali e quelle statiche
(visibili solo localmente ma che non vengono cancellate quando il blocco dove sono state definite termina) occupano un'altra area di memoria detta segmento dati, le variabili locali vengono inserite nel segmento stack. Quest’ultima area viene gestita dinamicamente come una
pila in cui, durante l’esecuzione, quando parte un programma, vengono inseriti sia i parametri
attuali che tutte le variabili locali e, quando il sottoprogramma finisce, vengono eliminati.
Quindi lo spazio utilizzato dello stack si allunga e si accorcia continuamente durante
l’esecuzione.
Osserviamo questo interessante programma tratto dal libro “Thinking in C++” di Bruce Eckel
(vedi 9.1):
#include <stdlib.h>
#include <iostream.h>
int cane, gatto, uccello, pesce;
void f(int animale) {
cout << "numero id animale: " << animale<< endl;
}
int main() {
int i, j, k;
cout << "f(): " << (long)&f << endl;
cout << "cane: " << (long)&cane<< endl;
cout << "gatto: " << (long)&gatto<< endl;
cout << "uccello: " << (long)&uccello<< endl;
cout << "pesce: " << (long)&pesce<< endl;
cout << "i: " << (long)&i << endl;
cout << "j: " << (long)&j << endl;
cout << "k: " << (long)&k << endl;
system("PAUSE");
return 0;
}
Il programma utilizza l’operatore & per mostrare la posizione in memoria di tutte le variabili
e quella della funzione f(). Il cast (long) serve per visualizzare l’indirizzo delle variabili in decimale invece che in esadecimale. Mandandolo in esecuzione il risultato sarà diverso a seconda dello stato del computer, del sistemo operativo e del compilatore utilizzato, nel mio caso
ho ottenuto:
f(): 4198948
cane: 4272128
gatto: 4272132
uccello: 4272136
pesce: 4272140
i: 37879644
j: 37879640
k: 37879636
Come si vede la posizione della funzione f() è diversa da quella delle variabili cane, gatto,
uccello e pesce che occupano posizioni consecutive a partire da 4272128 (ogni variabile int
65
Corso sul Linguaggio C++ Edizione 2010
occupa 4 bytes) e quella delle variabili locali i, j, k, anch’esse consecutive con ordine invertito, a sua volta è diversa sia da quella della funzione che da quella delle variabili globali.
Quando un programma richiama una funzione prima di tutto inserisce nello stack il valore dei
parametri attuali o il loro indirizzo, in base a quanto stabilito dal programmatore, poi trasferisce nello stack il program counter ovvero l’indirizzo della prossima istruzione del programma, quella che verrà subito dopo la chiamata del sottoprogramma e, infine, trasferisce il controllo alla prima istruzione del sottoprogramma.
Quando il sottoprogramma finisce ovvero quando viene eseguita l’istruzione return, vengono
eliminate tutte le variabili locali dallo stack (il codice macchina relativo a questa operazione è
compreso nel codice del sottoprogramma), viene eseguita l’istruzione macchina RET che preleva dallo stack il program counter e lo ripristina, vengono eliminati i parametri dallo stack e
si prosegue con il codice macchina che segue la chiamata del sottoprogramma. L’eventuale
risultato della chiamata di una funzione viene normalmente inserito in un registro interno della CPU (l’accumulatore).
Memoria centrale
Segmento codice
Posizione:4198948
Funzione f()
Funzione main()
Segmento dati
Variabli globali: cane
gatto
uccello
pesce
Segmento stack
Variabli locali:
k
j
i
Posizione:4272128
Posizione: 37879644
Questo sistema permette la ricorsione ovvero la possibilità per un sottoprogramma di richiamare se stesso. Quando un sottoprogramma richiama se stesso vengono inserite nello stack le
copie dei valori o degli indirizzi dei nuovi parametri attuali e vengono ricreate le variabili locali e, mentre l’esecuzione precedente rimane sospesa, parte una seconda volta l’esecuzione
dello stesso sottoprogramma. Quindi, in questo caso, nello stack saranno inserite due volte,
probabilmente con valori diversi, tutte le variabili locali e i parametri attuali. Se anche durante
la seconda esecuzione il sottoprogramma richiama se stesso, parte una terza esecuzione e così
via, ci possono essere anche centinaia di esecuzioni dello stesso sottoprogramma sospese in
attesa che venga completata l’ultima.
Quando le stesse istruzioni devono essere eseguite più volte in punti diversi del programma,
l’utilizzo delle funzioni permette di scrivere una sola volta le istruzioni da ripetere. Anche in
memoria, nel segmento codice, queste istruzioni saranno scritte una sola volta, ma il meccanismo di passaggio dei parametri attraverso lo stack porta ad un rallentamento dell’esecuzione
66
Corso sul Linguaggio C++ Edizione 2010
dovuto al tempo necessario per l’inserimento dei valori nello stack e, successivamente, per la
loro eliminazione. Anche quando una funzione non ha parametri verrà sempre inserito nello
stack l’indirizzo di ritorno. Se vogliamo velocizzare il programma possiamo eliminare
l’utilizzo dello stack riscrivendo le istruzioni del sottoprogramma ogni volta che servono. Il
linguaggio C++ mette a disposizione del programmatore due sistemi per inserire più facilmente le stesse istruzioni in più parti del programma senza doverle ogni volta riscrivere: le macro
e le funzioni inline.
Con le macro è il preprocessore che si occupa di inserire le istruzioni dove il programmatore
inserisce il nome della macro. Per scrivere una macro si utilizza la direttiva #define, per
maggiori dettagli si consiglia di leggere gli approfondimenti consigliati. Segue un esempio di
macro:
#define prod(a,b)
a*b
……
int a=10,b=5;
cout << prod(a+b,a-b);
//definizione della macro prod(a,b)
Quando il compilatore trova prod(x,y) lo sostituisce con x*y , qualunque siano le espressioni x
e y quindi nell’esempio
prod(a+b,a-b) viene sostituito con a+b*a-b
e questo significa che nell’esempio, verrà visualizzato su video il risultato 55.
Con le funzioni inline è invece il compilatore che fa il lavoro. In pratica, ogni volta che il
compilatore trova la chiamata di una funzione invece di tradurla in linguaggio macchina con
la chiamata del corrispondente sottoprogramma, inserisce in quel punto tutte le istruzioni della
funzione. Per creare una funzione inline basta premettere nella definizione della funzione la
parola chiave inline, per esempio:
inline double area(double r)
{ return r * r * 3.14 ;}
In genere le funzioni inline e le macro sono sottoprogrammi costituiti da poche istruzioni perché hanno lo svantaggio di far aumentare le dimensioni del codice. Naturalmente una funzione inline e una macro non possono essere ricorsive.
4.6
Le variabili static
Sia le variabili che le funzioni possono essere dichiarate utilizzando il modificatore di modalità di memorizzazione static. Abbiamo visto già visto che se si utilizza static per una variabile
locale quest’ultima sarà memorizzata nel segmento dati invece che nello stack e quindi non
verrà cancellata alla fine del blocco in cui è stata definita. Se invece si utilizza static nella definizione di una variabile globale o di una funzione si cambia l’area di validità della variabile
o della funzione, cioè le aree del programma in cui è possibile utilizzare ovvero è visibile la
variabile o la funzione. Normalmente le variabili globali e le funzioni sono visibili in tutti i
moduli del programma mentre quando vengono dichiarate come static diventano visibili solo
nel modulo in cui sono state dichiarate.
Per vedere l’effetto di static sulle variabili locali proviate il seguente programma:
#include <iostream.h>
#include <stdlib.h>
void f() {
67
Corso sul Linguaggio C++ Edizione 2010
int contatore=1;
cout << contatore
<< ";" ; contatore++;
}
int main(){
int i;
for (i=1; i<10; i++) f();
cout << endl; system("PAUSE");
return 0;
}
Il programma richiama la procedura f() nove volte e quest’ultima, ogni volta, scrive su video
il valore della sua variabile locale contatore, mandandolo in esecuzione si otterrà:
1;1;1;1;1;1;1;1;1;
perché ogni volta che la procedura f() viene richiamata, la variabile contatore viene creata con
il valore 1, stampata, incrementata e poi cancellata, quindi quando si visualizzerà la variabile
il valore in essa contenuto sarà sempre 1.
Se, invece, nella dichiarazione della variabile contatore aggiungiamo la parola static
static int contatore=1;
il risultato sarà:
1;2;3;4;5;6,7;8;9;
perché, in questo caso, solo la prima volta che la procedura f() viene richiamata verrà creata la
variabile contatore, tutte le volte successive l’istruzione di dichiarazione non verrà eseguita
perché la variabile risulterà già esistente e quindi, in ognuna delle esecuzioni successive, verrà
utilizzato il valore della variabile che è stato modificato nelle esecuzioni precedenti della stessa funzione.
4.7
Funzioni ricorsive
Abbiamo già parlato della ricorsione e abbiamo detto che una funzione è ricorsiva quando
nel blocco delle sue istruzione ce n’è almeno una che richiama direttamente o indirettamente se stessa.
Quando una funzione richiama se stessa il computer esegue di nuovo la funzione lasciando
sospesa l’esecuzione precedente. In pratica questa tecnica è un modo per far eseguire più volte
una serie di istruzioni e quindi si tratta di un altro modo per implementare algoritmi con cicli.
Infatti una funzione ricorsiva può essere sempre riscritta senza ricorsione, utilizzando i cicli
ed è vero anche il viceversa, tutti i cicli possono essere riscritti utilizzando funzioni ricorsive
ma, gli algoritmi risultanti con o senza la ricorsione non sono in genere equivalenti. Per esempio un ciclo postcondizionale tipo
do {…blocco delle istruz.del ciclo…} while (condizione)
può essere riscritto dichiarando una funzione ricorsiva tipo:
void ciclo(eventuali variabili da utilizzare nel blocco o per la cond.){
…blocco delle istruz.del ciclo; IF (condizione) ciclo(…) else return;
68
Corso sul Linguaggio C++ Edizione 2010
e inserendo al posto dell’istruzione do vista sopra la chiamata alla funzione:
ciclo(…);
Analogamente anche un’istruzione for tipo:
for (i=inizio; i<= fine; i++) { …blocco delle istruzioni del ciclo… }
può essere tradotta dichiarando la seguente funzione ricorsiva ciclofor()
int inizio, fine ;
void ciclofor(int i){
if (i>= inizio) {ciclofor(i-1); {…blocco delle istruz.del ciclo…} }
return ; }
e inserendo al posto della riga con l’istruzione for la seguente chiamata alla funzione ricorsiva:
ciclofor(fine);
Anche se dal punto di vista dell’utente non c’è differenza tra i due modi di implementare i cicli, in realtà, i due programmi non sono equivalenti perché il computer eseguirà operazioni
molto diverse.
Nei due casi visti sopra, il programma scritto utilizzando le istruzioni do e for e sicuramente
più efficiente di quello che utilizza le funzioni ricorsive ma non sempre è così, a volte, soprattutto in presenza di algoritmi complessi, l’utilizzo delle funzioni ricorsive semplifica notevolmente l’algoritmo e rende più chiaro il programma.
Problema: scrivere una funzione per il calcolo del fattoriale di un numero.
Dato un qualsiasi numero intero n, il fattoriale di n, che si indica con n!, è dato dal prodotto
dei numeri da 1 a n:
n! = 1 * 2 * 3 * 4 * …… * (n-1) * n
per esempio 4! = 1 * 2 * 3 * 4 = 24; 5! = 1 * 2 * 3 * 4 * 5 = 120
Per convenzione si pone 0! = 1. La dichiarazione della funzione in C++ potrebbe essere la seguente:
int fatt(int n) {
int ris = 1;
for (; n>1; n--) ris *= n;
return ris;
}
Nella funzione fatt(n) si utilizza un ciclo for per moltiplicare i numeri da n fino a 2 e quindi si
calcola:
n * (n-1) * (n-2) * …….. * 3 * 2
che è la stessa cosa di
2 * 3 * …. * (n-2) * (n-1) * n
perché la moltiplicazione è commutativa. Viene evitata la moltiplicazione per 1 che non cambia il risultato.
69
Corso sul Linguaggio C++ Edizione 2010
Ora si può osservare che se abbiamo calcolato 4! moltiplicando 2 * 3 * 4 = 24, per calcolare
5! non è necessario fare tutte le moltiplicazioni 2* 3* 4* 5 basta fare 24 * 5 e in generale se
abbiamo calcolato (n-1)! Si ha che:
n! = (n-1)! * n
sfruttando questa proprietà si può riscrivere la funzione fatt() in modo ricorsivo così:
int fatt(int n) {
if (n > 1) return fatt(n-1)* n; else return 1;
}
In molti problemi si può individuare un procedimento di calcolo ricorsivo a volte pià semplice
da implementare rispetto a quello iterativo. I procedimenti ricorsivi, in genere, si basano
sull’individuazione di una condizione per l’uscita non ricorsiva e delle istruzioni ricorsive. In
genere la struttura di una funzione ricorsiva è la seguente:
If (condizione per l’uscita) calcolo base ; else calcolo ricorsivo ;
4.8
Esercizi
72) Dato il seguente frammento di programma:
void somma(int &a , int b) {
a+=b; b = a ;
}
void main() {
int a = 3, b= 4; somma(a,b);
}
cout << "a= " << a << "; b= " << b;
Cosa viene visualizzato?
73) Si consideri la seguente funzione.
int funzione ( ){
int contatore = 0; int sum = 0;
while (contatore <= 4){
contatore = contatore + 1;
sum = sum + contatore;
}
return sum;
}
Quale valore restituisce la funzione?
a)10
b)15
c)16
d) Nessuna delle risposte precedenti
74) Dato il seguente frammento di programma:
void funzione(int x) {
int k;
for(k = 0; k < 10; k++) x += k;
}
void main() {
int a = 3; funzione(a); cout << a;
}
Indicare quale valore viene stampato.
75) Considerando il seguente frammento di programma
Int a, b;
70
Corso sul Linguaggio C++ Edizione 2010
void
void
void
void
f1(int
f2(int
f3(int
f4(int
&p,
&p,
&p,
&p,
int
int
int
int
&c)
&c)
&c)
&c)
{p=c;}
{p=b;}
{p=b;}
{c=b;}
dite quali delle seguenti chiamate non ha lo stesso effetto delle altre
a) f1(a, b)
b) f2(a, b)
c) f3(b, a)
d) f4(a, a)
76) Provate a scrivere la funzioni per calcolare il logaritmo naturale di x utilizzando
l’algoritmo dell’esercizio 51) con due parametri
double in(double x, double E=0.1e-6);
x =numero di cui si vuole calcolare il logaritmo e E = errore massimo ammesso
77) Utilizzando gli algoritmi descritti negli esercizi 57) e 59) scrivere le funzioni per calcolare
sen(x) e ex.
Ricorsione
78) Sia definita la seguente funzione:
int cond(){ return cond(); }
Cosa succede all'istruzione if ((a>0) && cond()) cout << "ciao"; al variare di a?
Cosa succede all'istruzione if (cond() && (a>0)) cout << "ciao"; al variare di a?
79) Euclide, nel 300 a.C., dimostrò le seguenti proprietà del massimo comun divisore:
supponiamo che A e B siano due numeri interi qualsiasi tali che A>B, indicando con MCD(A,B) il massimo comun divisore di A e B, si ha che
se A è divisibile per B allora
MCD(A,B) = B
altrimenti, posto R= A % B (resto della divisione tra A e B) si ha che MCD(A,B) = MCD(B,R)
Utilizzando queste proprietà si può costruire una funzione ricorsiva che calcola il massimo comun divisore
tra due numeri interi qualsiasi A e B.
80) Dato che xn = xn-1 * x se n>0, mentre è uguale a 1 se n è 0, scrivere una funzione ricorsiva
per il calcolo di xn.
81) Scrivere una funzione ricorsiva per visualizzare l'ennesimo termine della serie di Fibonacci (i primi due sono uguali a 1, ognuno dei successivi è uguale alla somma dei due precedenti).
82) Considerate la seguente funzione:
int f(int x)
{
if (!x) return 0;
return (x%2)+f(x/2);
}
Tale funzione viene eseguita su tutti i valori di x compresi fra 100 e 500. Chiamate a il minimo valore ottenuto in queste esecuzioni, e b il massimo valore; quanto valgono a e b?
83) Cosa fa la seguente funzione ricorsiva?
void removeHat(char cat) {
for(char c = 'A'; c < cat; c++)
cout << " ";
if(cat <= 'Z') {
cout << "cat " << cat << endl;
removeHat(cat + 1); // chiamata ricorsiva
71
Corso sul Linguaggio C++ Edizione 2010
} else
cout << "VOOM!!!" << endl;
}
Risposte esercizi
Esercizio 72) Risposta a= 7; b=4
Esercizio 73) Risposta = b)
Esercizio 74) Risposta = 3
Esercizio 75) Risposta = c)
Esercizio 78) Risposta: la funzione cond() è una funzione ricorsiva che non finisce mai perché
richiama indefinitivamente se stessa quando viene mandata in esecuzione quindi
ad un certo punto esaurisce lo stack e produce un errore.
Nel caso a) la condizione prevede prima la valutazione di a>0, se questa condizione è falsa allora la seconda non verrà controllata e quindi il programma prosegue senza errori mentre se a è maggiore di 0 allora viene valutata anche
cond() e il programma si blocca con l’errore di esaurimento dello stack. Nel caso b) la condizione contiene per prima la valutazione di cond() e quindi, qualunque sia il valore di a, la funzione viene mandata sempre in esecuzione e si
verificherà sempre l’errore.)
Esercizio 82) Risposta a=1 b=8 la funzione f restituisce il numero di 1 presenti nella rappresentazione binaria del numero x se x != 0
Esercizio 83) Risposta: visualizza su video tutti i caratteri compresi tra il carattere fornito come parametro e il carattere ‘Z’, uno per ogni riga, visualizzando sulla riga, prima del carattere, un numero di spazi corrispondente alla posizione del carattere
nell’alfabeto.
72
5
Capitolo
Corso sul Linguaggio C++ Edizione 2010
5. Vettori, stringhe e puntatori
5.1
Cosa sono i vettori
Abbiamo visto nel paragrafo 2.4che il C++ permette di utilizzare vari tipi di dati elementari:
bool, char, int, float, double, ecc. Questi sono i tipi che possiamo utilizzare per creare le variabili nei nostri programmi. Una variabile è un contenitore e si chiama così perché il suo contenuto, detto valore della variabile, può cambiare. Ogni variabile può contenere solo un determinato tipo di dati. Possiamo immaginare una variabile come una scatola e, così come non
è possibile inserire una videocassetta nel contenitore di una musicassetta o di un CD, allo
stesso modo non si può inserire un numero con la virgola in una variabile di tipo int o char.
Se la variabile SOMMA contiene il valore 25, potremmo rappresentarla graficamente nel seguente modo:
Somma
25
Tutte le variabili che abbiamo visto finora hanno però una caratteristica in comune, sono variabili elementari ovvero possono contenere un solo dato! Tutte le variabili di tipo numerico
possono contenere un solo numero, quelle di tipo carattere (char) possono contenere un solo
carattere, ecc.. Quando in una variabile di qualsiasi tipo inseriamo un nuovo valore verrà automaticamente cancellato il valore precedente.
Quasi tutti i linguaggi di programmazione permettono di mettere insieme più spazi di memoria del tipo di quelli utilizzati per le variabili per costruire quelle che vengono chiamate strutture di dati, che sono gruppi di variabili e possono, quindi, contenere più dati contemporaneamente.
I vettori o array sono strutture di dati consistenti in un insieme di variabili tutte dello stesso
tipo disposte in memoria consecutivamente.
Se le variabili che costituiscono il vettore sono elementari, possiamo rappresentare graficamente un vettore disegnando una serie di caselle consecutive, per esempio un vettore di nome
A, di 6 elementi, contenente i numeri 10, 15, 20, 35, 12, 48, potrebbe essere così disegnato:
10
15
20
35
12
48
A[0]
A[1]
A[2]
A[3]
A[4]
A[5]
Si definisce dimensione di un vettore il numero di elementi che lo costituiscono, per esempio
il vettore A[] definito sopra ha dimensione 6. Un vettore è in pratica costituito da tanti spazi di
memoria consecutivi ognuno dei quali può contenere un dato. Ognuna di queste caselle viene
detta elemento del vettore. Gli elementi del vettore sono numerati in base alla loro posizione
partendo da 0 (0 per la prima posizione, quindi 1 per la seconda, 2 per la terza ecc.), il numero
73
Corso sul Linguaggio C++ Edizione 2010
associato ad ogni elemento si dice indice, l’ultimo elemento di un vettore ha sempre indice =
dimensione -1.
Gli elementi dei vettori sono a tutti gli effetti delle variabili. In tutte le istruzioni in cui si possono utilizzare le variabili di un determinato tipo si possono utilizzare anche, al loro posto,
elementi di un vettore delle stesso tipo.
Nelle istruzioni del linguaggio C++, per indicare un elemento di un vettore, si scrive il nome
del vettore seguito da un espressione numerica racchiusa tra parentesi quadre che rappresenta
l’indice ovvero la posizione dell'elemento all'interno del vettore. Per esempio, se la variabile I
contiene 5 e la variabile J contiene 3, le scritte A[I], A[5], A[10/2], A[J+2] indicano tutte lo
stesso elemento del vettore A, quello con indice 5 che è il sesto elemento del vettore. Seguono
alcuni esempi di istruzioni in cui vengono utilizzati dei vettori:
cin >>
importo[2];
conto[5]++;
cout << somma[8];
// legge un numero e lo conserva nella terza casella
del vettore A
// incrementa di 1 il sesto elemento del vettore conto
// visualizza il nono elemento del vettore somma;
Se si modifica il valore di un elemento di un vettore gli altri elementi non vengono toccati così come quando si modifica il contenuto di una variabile le altre non subiscono variazioni.
In pratica un vettore non è altro che un gruppo di variabili, tutte con lo stesso nome e dello
stesso tipo, a cui si accede attraverso un indice.
Così come si fa con le variabili anche prima di utilizzare un vettore bisogna dichiararlo.
L’istruzione per dichiarare un vettore è simile a quella utilizzata per le variabili con l’aggiunta
del fatto che bisogna specificare il numero di elementi del vettore tra parentesi quadre:
dichiarazione vettore
Tipo dato
Valori iniziali
{
Nome vettore
dimensione
[
]
=
Valori iniziali
Valori iniziali
Costanti
}
,
Il nome di un vettore è un qualsiasi identificatore scelto dal programmatore, la dimensione
deve essere una costante intera. Per esempio:
int v[100]; //dichiara un vettore di 100 elementi di tipo int
char frase[50]; // dichara un vettore di 50 caratteri
double importo[3] = {500.0, 1464.4, 3220.2} ;
L’ultima dichiarazione comprende anche l’inizializzazione del vettore e si poteva anche scrivere:
double importo[] = {500.0, 1464.4, 3220.2} ;
senza indicare la dimensione perché in questo caso il compilatore la ricava direttamente dal
numero di valori racchiusi tra le parentesi graffe.
74
Corso sul Linguaggio C++ Edizione 2010
Il numero di valori iniziali può anche essere minore della dimensione, in questo caso tutti gli
elementi per cui non è stato indicato nessun valore vengono inizializzati con un valore standard che per i tipi numerici è 0 (per i caratteri è il carattere ‘\0’). Per esempio
int a[4] = {1, 2}; // inserisce nel vettore {1, 2, 0, 0}
La dimensione del vettore deve essere una costante non può essere una variabile, per esempio
int posti[n]; // errore! La dimensione non può essere una variabile
da luogo ad un errore durante la compilazione se l’identificatore n è il nome di una variabile.
Nel linguaggio C++ il nome del vettore, non seguito da parentesi quadre, rappresenta la posizione di memoria in cui è memorizzato il primo elemento del vettore, quindi se si cerca di visualizzare il contenuto del vettore a dichiarato prima, con una istruzione del tipo:
cout << a ;
non verrà visualizzato il contenuto del vettore a ma un numero esadecimale che indica la posizione in memoria del primo elemento del vettore. Come vedremo il nome di un vettore è a
tutti gli effetti un puntatore costante (vedi paragrafo 5.9).
Le versioni del linguaggio C che seguono le specifiche ISO segnalano come errore
l’assegnazione:
int a[100], b[100]
……………
a=b; // errore di assegnazione
anche se in alcune versione del linguaggio questa assegnazione è permessa (solo se i due vettori hanno la stessa dimensione e sono dello stesso tipo) e viene eseguita copiando tutti gli elementi del vettore b nel vettore a.
5.2
A cosa servono i vettori
Un personal computer attuale (anno 2006), tipicamente è equipaggiato con una memoria centrale di 512MB. In una tale memoria si possono memorizzare oltre 128 milioni di numeri. Anche considerando che nella memoria centrale, oltre ai dati, sono memorizzate le istruzioni dei
programmi, resta il fatto che un programma può gestire anche milioni di variabili. Se tutte le
variabili fossero elementari un programma che gestisce molte variabili sarebbe inevitabilmente molto lungo perché ogni variabile dovrebbe essere dichiarata e avere un nome distinto dalle
altre, inoltre per ogni variabile ci dovrebbe essere almeno un’istruzione che la utilizza e quindi la dimensione del programma crescerebbe inevitabilmente. Per contro la possibilità di gestire molti dati e la velocità di elaborazione sono i due principali punti di forza dei computer.
Per esempio se vogliamo far incrementare di uno il valore delle quattro variabili semplici a, b,
c, d, dobbiamo scrivere 4 istruzioni diverse:
a++ ; b++ ; c++ ; d++ ;
Se le variabili, invece di 4, fossero 1000 dovremmo scrivere 1000 istruzioni diverse e inventarci 1000 nomi diversi per le variabili. Se, invece, per memorizzare i 1000 valori utilizzassimo un vettore di 1000 elementi creato, per esempio, con l'istruzione int v[1000], per incrementare di uno tutti gli elementi del vettore basterebbe il seguente ciclo for:
For (int i=0, i<1000, i++) v[i]++ ;
Ogni volta che si deve eseguire la stessa operazione su molte variabili semplici dello stesso
tipo bisogna scrivere più volte l'istruzione o le istruzioni che fanno eseguire l'operazione, quasi sempre in questo caso conviene utilizzare uno o più vettori per conservare i valori delle variabili e inserire l'istruzione o le istruzioni in un ciclo nel quale viene modificato ogni volta
anche l'indice del vettore in modo che ad ogni esecuzione si operi su un elemento diverso.
75
Corso sul Linguaggio C++ Edizione 2010
Per esempio se vogliamo leggere da tastiera e conservare in memoria n numeri, con n <=100
possiamo scrivere:
float num[100] ;
int n ; cout << "Quanti numeri ? " ; cin >> n ;
for (int i=0 ;i<n ; i++) {
cout << "Inserisci l\’elemento " << i << " : " ;
cin >> num[i] ;
}
Per visualizzare questi n numeri su video possiamo scrivere:
for (int i=0; i<n; i++) cout << num[i] << "; " ;
L'uso dei vettori costituisce uno strumento molto potente in mano ai programmatori e spesso
permette di risolvere facilmente e brevemente problemi che altrimenti sarebbero complessi
e/o lunghi e noiosi. Per esempio consideriamo il seguente problema:
Problema: Far leggere una serie di numeri e visualizzare solo quelli superiori alla media
Prima di tutto non sappiamo quanti numeri verranno inseriti ma, dovendo essere inseriti da tastiera, probabilmente non potranno essere molti e quindi possiamo limitare la soluzione del
problema per esempio a massimo 100 numeri.
Il problema non presenta difficoltà, basta far leggere tutti i numeri memorizzandoli nel computer, calcolare la media e poi, riesaminandoli ad uno ad uno, visualizzare solo quelli maggiori della media. E’ però necessario memorizzare i numeri.
Se non utilizziamo i vettori dovremmo creare 100 variabili diverse, dovremmo far leggere i
numeri che potrebbero essere anche di meno di 100, scrivere le istruzioni per sommare tutti i
numeri letti, ecc. Come si può immaginare il programma sarebbe molto lungo.
Con i vettori, invece, tutto il programma si riduce al seguente:
// programma media. Legge una serie di numeri e visualizza quelli > media
int main()
{ double num[100] ; double media=0;
int n ; cout << "Quanti numeri (max 100)? " ; cin >> n ;
for (int i=0 ;i<n ; i++) { // legge e somma i numeri
cout << "Inserisci l\'elemento " << i << " : " ;
cin >> num[i] ; media +=num[i]; //legge e somma
}
media = media /n; //Calcola la media
for (int i=0 ;i<n ; i++) if (num[i] > media) cout << num[i] << "; " ;
system("PAUSE");
return 0;
}
5.3
Caricamento di un vettore con numeri a caso
In alcune applicazioni, per esempio quelle di simulazione oppure nei videogiochi spesso è necessario utilizzare numeri scelti a caso. Il linguaggio C dispone di varie funzione per generare
dei numeri pseudocasuali. Vengono detti pseudocasuali perchè in realtà questi numeri vengono calcolati attraverso funzioni matematiche e quindi non sono casuali come quelli che un
bambino bendato potrebbe estrarre da un’urna del lotto, ma sembrano casuali, perché appaiono susseguirsi senza nessuna logica. Per esempio si può utilizzare la funzione rand() (random
= casuale in inglese) che restituisce un numero intero casuale tra 0 e la costante
RAND_MAX e si richiama senza parametri (attenzione, nel linguaggio C quando si richiama
una funzione senza parametri bisogna sempre farla seguire dalle parentesi tonde aperte e chiuse, per esempio rand(), il nome da solo ha un altro significato: è un puntatore).
76
Corso sul Linguaggio C++ Edizione 2010
Per utilizzarla bisogna includere il file di intestazioni <stdlib.h>, lo stesso che serve per utilizzare la funzione system().
int main(){
// programma rnd. Generazione di numeri casuali
int v[100], n;
cout << "Quanti numeri? "; cin >> n;
cout << " i numeri inseriti nel vettore sono:\n";
for (int i=0; i<n; i++) {
v[i]= rand();
cout << v[i]<< '\t';
}
system("PAUSE");
return 0;
}
mandandolo in esecuzione e chiedendo 10 numeri si ottiene in output qualcosa del tipo
i numeri inseriti nel vettore sono:
41 18467 6334 26500 19169 15724 11478 29358 26962 24464
Purtroppo se si manda in esecuzione di nuovo lo stesso programma chiedendo di nuovo 10
numeri si ottengono gli stessi di prima perché il computer, per generare ogni nuovo numero
casuale, applica una funzione matematica all’ultimo numero generato e la prima volta che si
genera un numero casuale nel programma si parte da un numero fisso, il numero 1. Per evitare
questo si può utilizzare la funzione srand( <numero iniziale>) che cambia il numero che verrà utilizzato per la prossima applicazione della funzione rand, insieme alla funzione time che
restituisce il numero di secondi trascorsi dalla mezzanotte del 1/1/1970 ad oggi e quindi restituisce un valore diverso ad ogni esecuzione del programma. Per usare la funzione time bisogna includere il file di intestazioni <time.h>. In pratica bisogna inserire all’inizio del programma main la seguente istruzione:
srand((int)time(0));
Anche se rand genera solo numeri interi tra 0 e RAND_MAX, questa funzione si può utilizzare per generare numeri con la virgola in un intervallo a piacere. Per esempio per generare
numeri con la virgola tra 10 e 20 il programma precedente si può modificare nel seguente
modo:
int main(){
// programma rnd. Generazione di numeri casuali
double v[100], n;
srand(time(0));
cout << "Quanti numeri? "; cin >> n;
cout << " i numeri inseriti nel vettore sono:\n";
for (int i=0; i<n; i++) {
v[i]= 10 + (double)rand()/RAND_MAX*10;
cout << v[i]<< '\t';
}
system("PAUSE");
return 0;
}
5.4
Ordinamento di un vettore
Gli algoritmi di ordinamento sono algoritmi fondamentali, utilizzati in molti programmi, e
quindi non possono mancare fra le conoscenze di un programmatore.
L’ordinamento è un processo che consiste nel mettere un insieme di elementi in un ordine
specifico.
77
Corso sul Linguaggio C++ Edizione 2010
L’ordinamento serve soprattutto a facilitare le successive ricerche. Per rendercene conto immaginiamo la difficoltà che avremmo nel cercare un nome in un elenco telefonico se questo
non fosse ordinato in ordine alfabetico! Oltre agli elenchi telefonici si riordinano oggetti come
l’archivio dei prodotti di un magazzino, i cataloghi dei libri di una biblioteca, l’archivio delle
fatture, ecc. In tutti i casi l’ordinamento è indispensabile per poter effettuare successivamente
le ricerche in un tempo ragionevole.
Per ordinare un insieme di elementi, per esempio quelli appartenenti ad un vettore in memoria, si possono utilizzare vari procedimenti. Come sempre accade in informatica, la stessa operazione si può fare in tanti modi diversi, ognuna con dei vantaggi e svantaggi.
Ordinare un vettore significa scambiare di posto gli elementi in modo tale che alla fine risultino nell’ordine richiesto.
Per esempio se abbiamo in memoria il seguente vettore:
103
15
202
35
121
148
5
e vogliamo ordinarlo in ordine crescente dobbiamo effettuare una serie di scambi in modo che
alla fine il vettore contenga gli stessi elementi ma disposti nell’ordine voluto ovvero:
5
15
35
103
121
148
202
Cominciamo con i metodi di ordinamento piu semplici: selezione, inserzione e bubble sort.
Ordinamento per selezione
Nel seguito supporremo sempre di voler ordinare il vettore in modo crescente, nel caso lo volessimo ordinare in modo decrescente basterebbero piccole modifiche ai programmi.
Questo metotodo si chiama così perché ad ogni passaggio si esamina la parte di vettore ancora
non ordinata, si cerca l’elemento più piccolo e lo si mette al primo posto (nel caso si volesse
ordinare in modo decrescente il vettore si sceglie l’elemento più grande). In pratica si procede
così: si inizia confrontando il primo elemento con tutti gli altri e ogni volta che se ne trova
uno più piccolo si scambia con il primo; dopo questo passaggio l’elemento più piccolo di tutti
si troverà al primo posto che è il suo posto nel vettore ordinato; poi si ripete la stessa cosa con
gli elementi dal secondo in poi, cioè si selezione il più piccolo e si mette al secondo posto; poi
si fa lo stesso con gli elementi dal terzo posto in poi e così di seguito fino a che non si arriva
ad ordinare tutto il vettore. Supponendo che v[] sia il vettore e che n sia il numero di elementi
dello stesso, il programma è il seguente:
// ordinamento crescente per selezione
for (i= 0; i<n-1; i++)
// nel ciclo seguente cerca l'elemento più piccolo e lo mette al posto i
for(j=i+1; j<n; j++)
if (v[i] > v[j]) {
x = v[i]; v[i]=v[j]; v[j]= x; // scambia di posto gli elementi
}
Se applichiamo questo algoritmo al vettore numerico dell’esempio precedente per il quale n =
7, otteniamo nei vari passaggi i seguenti cambiamenti:
valori iniziali
i=0
i=1
i=2
i=3
i=4
i=5
103
5
5
5
5
5
5
15
103
15
15
15
15
15
202
202
202
35
35
35
35
35
35
103
202
103
103
103
78
121
121
121
121
202
121
121
148
148
148
148
148
202
148
5
15
35
103
121
148
202
Corso sul Linguaggio C++ Edizione 2010
ad ogni passaggio si esaminano solo gli elementi da i in poi (quelli precedenti sono già ordinati).
Le operazioni eseguite da un programma di ordinamento sono essenzialmente di due tipi: confronto tra due elementi del vettore e scambio. L’operazione principale e quella di confronto
per cui un parametro significativo per misurare l’efficienza di un metodo di ordinamento è la
stima del numero di confronti effettuato. Nel caso dell’ordinamento per inserzione al primo
passaggio, quando i = 0, si confronta il primo elemento con tutti gli altri, in totale n-1 confronti, al secondo, quando i = 1 si confronta l’elemento v[1] cont tutti i seguenti, in totale n-2
confronti, e così via fino all’ultimo passaggio in cui si confrontano solo il penultimo e
l’ultimo elemento. Quindi il numero totale dei confronti è:
(n-1) + (n-2) + (n-3) + …… + 2 + 1 = n*(n-1)/2
Se si raddoppia il numero degli elementi del vettore il numero totale di scambi all’incirca si
quadruplica, se il numero degli elementi aumenta di 10 volte il numero degli scambi aumenta
di circa 100 volte perché è quasi proporzionale a n2!
Ordinamento per inserzione
Quando io voglio ordinare un pacco di compiti dei miei alunni in ordine alfabetico, in base al
cognome dell’alunno, procedo nel seguente modo: poggio i compiti sul tavolo poi li prendo ad
uno ad uno e ogni volta che prendo un nuovo compito lo inserisco in mezzo a quelli che ho
già in mano in modo da mantenerli ordinati. Per esempio supponiamo che i compiti si trovino
nel pacco in questo ordine:
Lucisano, Scaringella, Calabrese, Melato, Borrelli, Testa, Presutto, Gravina, Bartoli, Senzani
Prendo il primo, poi prendo il secondo mettendolo dopo il primo e quindi in mano ho:
Lucisano, Scaringella
poi prendo il terzo, Calabrese, e lo metto prima degli altri due perché i compiti che ho in mano devono essere ordinati, quindi in mano avrò:
Calabrese, Lucisano, Scaringella
prendo il quarto, Melato, e lo inserisco in mezzo agli altri sempre facendo in modo che siano
ordinati:
Calabrese, Lucisano, Melato, Scaringella
procedo così fino a prendere l’ultimo compito, ogni volta inserisco il compito in mezzo agli
altri che ho in mano in modo da rispettare l’ordine alfabetico:
Borrelli, Calabrese, Lucisano, Melato, Scaringella
Borrelli, Calabrese, Lucisano, Melato, Scaringella, Testa
Borrelli, Calabrese, Lucisano, Melato, Presutto, Scaringella, Testa
Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Testa
Bartoli, Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Testa
Bartoli, Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Senzani, Testa
Questo modo di procedere si chiama ordinamento per inserzione, lo possiamo applicare anche
ad un vettore:
for (i=1, i<n , i++) {
x= v[i]; //prendiamo l’elemento i
….. // lo inseriamo tra gli elementi che si trovano prima di i in modo che siano ordinati
}
Il sottoprogramma completo potrebbe essere il seguente:
for (i=1; i<n; i++) {
79
Corso sul Linguaggio C++ Edizione 2010
x=v[i];
j = i-1;
while (j>=0 && x < v[j]) {v[j+1]=v[j]; j=j-1;}
v[j+1]=x;
}
per inserire l’elemento x al posto giusto lo confrontiamo con tutti gli elementi precedenti (che
sono già ordinati) a partire da j=i-1, decrementando j fino a j=0. Ci fermiamo quando troviamo un elemento v[j] <= x, il posto di x sarà dopo questo elemento. Nel ciclo spostiamo anche
tutti gli elementi che sono maggiori di x di una posizione più avanti in modo da liberare un
posto per mettere x.
Come si vede ci sono due condizioni che possono fermare il ciclo interno che serve a trovare
il posto giusto per l’elemento x: se viene trovato un elmento v[j]<=x oppure se si supera il
primo elemento del vettore (j<0). Si può evitare di controllare la seconda condizione, risparmiando un confronto per ogni ripetizione del ciclo, se utilizziamo l’elemento del vettore con
indice 0 per mettere un elemento sentinella che blocca il ciclo.
In questo caso il programma diventa:
for (i=1; i<n; i++) {
x=v[i]; v[0]= x
j = i-1;
while (x < v[j]) {v[j+1]=v[j]; j=j-1;}
v[j+1]=x;
}
non è più necessario controllare che j sia >=0 perché appena j diventa = 0 il ciclo si fermerà
sicuramente visto che in v[0] ci abbiamo messo x che non è < x. Naturalmente questo richiede
che i dati debbano essere inseriti nel vettore a partire dall’elemento 1.
Bubble sort
Questo sistema consiste nel confrontare ogni elemento con il successivo e, se non sono in ordine, scambiarli di posto. Dopo questo primo passaggio l’elemento più grande finirà
all’ultimo posto, che è il suo posto nel vettore ordinato, e quindi potrà essere escluso dai successivi passaggi. Si ripete lo stesso procedimento alla parte non ordinata fino a quando tutto il
vettore sarà ordinato.
j=n-1;
while (j>0) {
/* confronta ogni elem. da 0 a j con il successivo e scambia di posto
le coppie che non sono in ordine */
for (i=0; i<j; i++) {
if (v[i] > v[i+1]) {tmp=v[i]; v[i]= v[i+1]; v[i+1]=tmp;}
}
j = j-1;
}
se applichiamo questo procedimento al vettore di 7 elementi numerici rappresentato all’inizio
del paragrafo otterremo le seguenti modifiche ad ogni passaggio:
valori iniziali
j=6
j=5
j=4
j=3
j=2
j=1
103
15
15
15
15
15
5
15
103
35
35
35
5
15
202
35
103
103
5
35
35
35
121
121
5
103
103
103
80
121
148
5
121
121
121
121
148
5
148
148
148
148
148
5
202
202
202
202
202
202
Corso sul Linguaggio C++ Edizione 2010
Come si vede gli elementi più piccoli si spostano, una posizione alla volta, verso la cima del
vettore mentre quelli più grandi si spostano verso il fondo così come in una miscela di acqua
ed altri elementi quelli più leggeri (per esempio le bollicine d’aria) si spostano verso l’alto e
quelli più pesanti si spostano verso il basso. Da questa similitudine viene il nome bubble
sort.
Questo algoritmo può essere migliorato considerando che se da un certo punto in poi, diciamo
da k in poi, non vengono effettuati più scambi significa che da quel punto il vettore è ordinato
e quindi, nel passaggio successivo, ci si può fermare a k.
Con questa modifica l’algoritmo diventa:
// ordinamento bubble sort
int j=n-1, i, k, tmp;
while (j>0) {
// k = posizione dove viene effettato l’ultimo scambio
k=0;
for (i=0; i<j; i++) {
if (v[i] > v[i+1]) {tmp=v[i]; v[i]= v[i+1]; v[i+1]=tmp; k=i;}
}
j = k;
}
Quick sort
I tre semplici metodi che abbiamo visto sopra richiedono tutti un numero di scambi
dell’ordine di n2 e questo significa che se un vettore ha un numero di elementi dieci volte
maggiore di un altro, richiede, per essere ordinato, all’incirca 100 volte il tempo dell’altro.
Quindi i metodi diretti che abbiamo visto vanno bene per vettori con pochi elementi ma diventano lenti quando il numero degli elementi è elevato.
Gli studiosi hanno elaborato molti altri algoritmi di ordinamento che chiamerei avanzati e che
tentano di ridurre il numero di scambi e quindi il tempo necessario ad ordinare un vettore di
grandi dimensioni. Nel seguito vedremo uno dei più veloci tanto che il suo inventore C. A. R.
Hoare lo chiamò Quicksort.
L’idea base di questo metodo è quello di dividere il vettore in due parti partendo da un elemento x e spostando tutti quelli maggiori di x a destra e tutti quelli minori a sinistra. Dopo
questa operazione l’elemento utilizzato per effettuare la partizione si troverà al posto giusto
nel vettore mentre la parte sinistra e la parte destra (che sono vettori più piccoli) possono essere ordinate separatamente e più velocemente rispetto all’intero vettore. Ognuna delle due partizioni viene ordinata ripetendo ricorsivamente il procedimento visto per l’intero vettore fino a
che le partizioni non si riducono ad un solo elemento.
Il sottoprogramma potrebbe essere il seguente (come vedremo al paragrafo 5.10 un vettore
può essere passato come parametro ad un sottoprogramma);
void quick(int *v, int s, int d) {
/* ordinamento QuickSort
v = vettore da ordinare
s = posizione primo elemento della partizione da ordinare
d = posizione ultimo elemento della partizione */
int i=s,j=d, x, tmp;
//prende l'elemento centrale della partizione ma può essere uno qualsiasi
x= v[(s+d)/2];
do {
while (v[i] < x) i++; // trova il 1° elemento a sinistra not < x
while (v[j] > x) j--; // trova il primo elemento a destra not > x
// scambia i due elementi trovati
81
Corso sul Linguaggio C++ Edizione 2010
if (i<=j) {tmp=v[i]; v[i]=v[j]; v[j]= tmp; i++; j--; }
} while (i <= j); // ripete fino a quando i e j si incontrano
/* ripete l'operazione per le due partizioni che si sono create,
che sono quella che va da s a j e quella da i a d,
solo se hanno più di un elemento e partendo dalla più piccola */
if (s < j)
if (j-s < d-i) {quick(v, s, j); quick(v,i,d);}
else {
if (i < d) quick(v,i,d);
quick(v,s,j);
}
else if (i < d) quick(v,i,d);
}
per richiamarlo bisogna inserire nel programma principale la seguente riga (supponendo che il
vettore da ordinare sia il vettore v[] e che n sia il numero di elementi):
quick(v,0,n-1);
5.5
La ricerca in un vettore
La ricerca consiste nell’esaminare il contenuto di un vettore allo scopo di verificare se un determinato elemento è presente o meno al suo interno e, se presente, individuarne la posizione.
Se il vettore non è ordinato l’unico modo per raggiungere questo scopo è quello di esaminare
tutti gli elementi fino a che non si trova quello cercato o si raggiunge la fine del vettore.
Supponiamo che il vettore sia v[], che il numero di elementi sia n e che il valore da cercare sia
memorizzato nella variabile elem, allora il pezzo programma per effettuare la ricerca potrebbe
essere il seguente:
for (i = 0; i<n; i++)
if (v[i] == elem) break;
dopo questo ciclo se i < n vuol dire che l’elemento è stato trovato nella posizione i altrimenti
vuol dire che non è stato trovato.
if (i<n)
cout << "L'elemento e' stato trovato nella posizione " << i << endl;
else cout << "L'elemento non c'e' nel vettore\n";
Questo tipo di ricerca si chiama ricerca sequenziale.
Se il vettore è ordinato allora si può applicare una tipo di ricerca molto più veloce.
L’algoritmo più utilizzato in questo caso è quello della ricerca binaria.
Quando cerchiamo un nome nell’elenco telefonico di una città prendiamo una pagina centrale
a caso e leggiamo il primo nome presente, se il nome che cerchiamo viene dopo quello che
abbiamo letto, sappiamo che possiamo escludere dalla ricerca tutte le pagine precedenti, se,
invece, viene prima, possiamo escludere quelle seguenti e quindi coun un solo confronto riduciamo a metà le pagine in cui cercare. Ripetiamo questa operazione nel gruppo di pagine che
restano fino a quando individuiamo la pagina dove c’è il nome che cerchiamo.
Per esempio supponiamo di dover cercare il nome Pietro in un vettore di 16 elementi contenente i seguenti nomi:
0. Anna
1. Amalia
2. Carlo
3. Dario
4. Elena
5. Federica
82
Corso sul Linguaggio C++ Edizione 2010
6. Giacomo
7. Irene
8. Loredana
9. Lorenzo
10. Maria
11. Mario
12. Michele
13. Pietro
14. Renata
15. Roberta
se utilizziamo la ricerca binaria procederemo in questo modo:
- confrontiamo Pietro con il nome che si trova nella posizione 7 (una delle due centrali) ovvero con Irene. Visto che non è quello che cerchiamo e che Pietro viene dopo Irene sappiamo
che possiamo escludere dalla ricerca tutti gli elementi del vettore che si trovano nelle posizioni da 0 a 7;
- prendiamo l’elemento centrale tra 8 e 15 che è l’ 11 dove c’è Mario che viene prima di Pietro quindi posso ridurre il campo di ricerca alle posizioni dalla 12 alla 15;
- prendo l’elemento centrale di questo campo di ricerca che è il 13 dove troviamo quello che
cercavamo.
Abbiamo raggiunto lo scopo con solo 3 confronti mentre con la ricerca sequenziale sarebbero
stati necessari 14 confronti. Si può dimostrare che mentre con la ricerca sequenziale sono necessari in media n/2 confronti con la ricerca binaria ne servono al massimo log2 n, questo significa che con un vettore di 1000 elementi, possiamo trovare quello che cerchiamo con solo
10 confronti e se il numero di elementi è 1000000 ne bastano 20!
Il programma è il seguente:
// ricerca binaria
trovato = 0; i = 0; f = n-1;
while (!trovato && i <= f) {
p = (i+f)/2;
if (vi[p] == elem) {trovato = 1; break;}
else if (vi[p] > elem) f=p-1; else i =p+1;
}
La variabile trovato ci dice se l’elemento è stato trovato o meno mentre la variabile p indica la
posizione in cui è stato trovato.
5.6
Le stringhe di caratteri
Un nome, una frase e una qualsiasi sequenza di caratteri è quello che si intende per stringa di
caratteri. Si tratta di un tipo di informazione molto comune, molti linguaggi dispongono di un
tipo di dati apposito per le variabili stringhe, il linguaggio C++ purtroppo no, per memorizzare le stringhe bisogna utilizzare i vettori di char. Per esempio, per definire una variabile in cui
memorizzare un nome, supponendo che i nomi non superino mai i 29 caratteri di lunghezza, si
può scrivere:
char nome[30] ;
Una stringa, per il linguaggio C++, è sempre una serie di caratteri seguita dal carattere speciale ‘\0’ (codice ASCII 0), anche le stringhe costanti, che sono quelle racchiuse tra una coppia
di virgolette (“). Quindi le due istruzioni seguenti con cui si dichiara e inizializza una stringa
sono equivalenti:
char descr[20]= "pasta" ;
83
Corso sul Linguaggio C++ Edizione 2010
char descr[20] = {‘p’, ‘a’, ‘s’. ‘t’, ‘a’, ‘\0’} ;
Le istruzioni che il C++ mette a disposizioni sono quelle già viste per i vettori e, quindi, operazioni comuni negli altri linguaggi, come l’assegnazione di un valore costante ad una stringa,
il trasferimento del contenuto di una stringa in un'altra, la concatenazione di due stringhe,
danno luogo ad errori durante la compilazione:
char nome1[30]="Mario", nome2[50]="Annamaria", frase[30] ;
nome2 = "Lucia"; //Errore ! assegnazione incompattibile
nome1 = nome2; //Errore! assegnazione non ammessa
frase = "Ciao " + nome1; //Errore ! assegnazione e operazione incomp.
Nel linguaggio C++ si dovrebbero utilizzare i cicli per realizzare le operazioni precedenti ma,
per fortuna, ci sono molte funzioni di libreria, fornite con il compilatore, che svolgono egregiamente queste e tante altre operazioni comuni.
Inoltre quando si crea un vettore di caratteri la sua dimensione è stabilita e non può cambiare
durante l’esecuzione, per cui se si trasferisce in un stringa definita di 10 caratteri una frase con
più di 9 caratteri c’è il rischio di andare a modificare le altre variabili collocate dopo quel vettore, con risultati imprevedibili. Per allargare o restringere lo spazio allocato per una stringa è
necessaria una gestione dinamica della memoria abbastanza complessa.
Per risolvere tutti questi problemi si potrebbe utilizzare la classe string inclusa nelle nuove
librerie standard del C++, ma lo studio di questa classe esula dagli obiettivi di questo libro,
chi volesse approfondire può leggere il secondo volume del libro “Thinking in C++” di Bruce
Eckel dove è presente un intero capitolo dedicato all’argomento.
In questo libro esamineremo le tradizionali funzioni del linguaggio C per le stringhe. Per utilizzarle nei nostri programmi bisogna includere l’header:
#include <string.h>
Le principali sono :
strcpy(<stringa1>,<stringa2>) - Copia la stringa2 nella stringa1. Corrisponde ad
un’istruzione di assegnazione, assegna il contenuto di stringa2 a stringa1. La seconda stringa
può essere anche costante. Il computer non controllerà la lunghezza delle due stringhe, semplicemente trasferisce nella prima stringa ogni carattere della seconda stringa, fino al carattere
‘\0’ di terminazione incluso. É compito del programmatore assicurarsi che la stringa1 (quella
di destinazione) sia abbastanza capiente. In caso di traboccamento l’errore non viene segnalato e i risultati sono imprevedibili. Il valore restituito dalla funzione strcpy è il puntatore
all’inizio della stringa di destinazione ovvero la posizione di memoria di stringa1. Esempi:
strcpy(nome,"Mario"); //assegna "Mario" al vettore di char nome
strcpy(nome1,nome2); //copia il contenuto di nome2 in nome1
strncpy(<stringa1>,<stringa2>,<num>) - Funziona come strcpy ma trasferisce esattamente
<num> caratteri, se stringa2 è più lunga di <num> caratteri trasferisce solo i primi <num>
senza il carattere di terminazione, se è più corta trasferisce sempre esattamente <num> caratteri aggiungendo più volte il carattere di terminazione
Char s1[30]=”Ciro è Bravo”, s2[30] = “Gino Bellitti”;
strncpy(nome1,nome2,4); // a questo punto in s1 ci sarà “Gino è Bravo”
strlen(<stringa>) - restituisce il numero di caratteri effettivamente contenuti nella stringa ovvero conta i caratteri fino a quello di terminazione (‘\0’). La lunghezza di una stringa è sempre
un numero diverso rispetto alla dimensione del vettore di char che si può ottenere con sizeof,
Una stringa può avere anche lunghezza 0 se il primo carattere in essa contenuto è quello di
terminazione, in questo caso si dice che la stringa è vuota.
84
Corso sul Linguaggio C++ Edizione 2010
int a; char frase[20]= "Ciao"; a=strlen(frase); // assegna ad a=4
strcat(<stringa1>, <stringa2>) - Concatenamento di stringhe. Aggiunge a stringa1 la stringa2. Funziona come strcpy con la differenza che stringa2 viene copiata alla fine di stringa1
invece che all’inizio e, quindi, i caratteri di stringa2 vengono aggiunti a quelli di stringa1 invece di sovrascriverli.
Char nome1[30]=”Anna”, nome2[30] = “Maria”;
strcat(nome1,nome2); // a questo punto in nome1 ci sarà “AnnaMaria”
strncat(<stringa1>,<stringa2>,<num>) - come strcat ma vengono aggiunti non più di
<num> caratteri a stringa1. A differenza di strncpy viene sempre aggiunto alla fine il carattere
di terminazione per cui il numero di caratteri aggiunti è, in genere, <num> +1. Anche qui è
compito del programmatore assicurarsi che la prima stringa sia abbastanza grande da contenere tutti i caratteri aggiunti altrimenti si avranno risultati imprevedibili e nessun messaggio di
errore.
Char s1[30]=”Anna”, s2[30] = “Mariagiovanna”;
strncat(s1,s2,5); // a questo punto in s1 ci sarà “AnnaMaria”
In tutte le funzioni descritte in questo paragrafo il parametro <stringa1> è il nome di un vettore di char e, quindi, rappresenta la posizione in memoria del primo carattere della stringa,
questo significa che è, come vedremo nei paragrafi 5.8 e 5.10, un puntatore. Se frase1 è un
vettore, frase1 +3 rappresenta la posizione in memoria del terzo elemento del vettore, discorso analogo vale per frase1 +4 e in generale per frase1+x. Nel richiamare una delle funzioni di
questo paragrafo posso anche mettere al posto di <stringa1> una espressione che punta ad una
posizione interna alla stringa in quel caso l’operazione verrà eseguita a partire da quella posizione. Per esempio supponiamo di voler sostituire tre caratteri interni ad una stringa con altri
tre, le istruzioni sono:
Char s1[30]=”Gino è tuo amico”, s2[30]=”Bob è il suo cane”;
strncpy(s1+7,”mio”,3); // a questo punto in s1 ci sarà “Gino è mio amico”
strncpy(s1+7,s2+9,3); // a questo punto in s1 ci sarà “Gino è suo amico”
strcmp(<stringa1>, <stringa2>) - Confronto tra due stringhe. Viene eseguito il confronto
alfabetico tra le due stringhe il risultato è un numero intero = 0 se le due stringhe sono uguali,
< di 0 se la prima stringa è minore secondo l’ordine alfabetico (per esempio “Gino” < “Mario”), > di 0 se la prima stringa è maggiore della seconda (per esempio “Carlo” > “Anna”). Di
fatto viene restituita la differenza tra i primi due caratteri diversi.
Il confronto è case sensitive ovvero le lettere maiuscole non sono uguali a quelle minuscole e,
precisamente, ogni lettera maiuscola è minore di qualsiasi lettera minuscola (per esempio
“Gianni” < “anna”). C’è anche la funzione strcasecmp del tutto identica a strcmp salvo per il
fatto che ignora le differenze tra maiuscole e minuscole.
char s1[20], s2[20];
cout << “Inserisci il primo nome: “; cin >> s1;
cout << “Inserisci il secondo nome: “; cin >> s2;
if (!strcmp(s1,s2)) cout << "I nomi sono uguali!" << endl;
else if (strcmp(s1,s2)<0) cout << s1 << " e' < di " << s2 << endl;
else cout << s2 << " e' < di " <<s1 << endl;
strstr(<stringa1>,<stringa2>) - Cerca stringa2 all’interno di stringa1 e, se la trova, restituisce un puntatore che indica la posizione in memoria dove è stata trovata altrimenti restituisce
NULL che è il puntatore vuoto. Anche questa funzione è case sensitive e quindi distingue tra
85
Corso sul Linguaggio C++ Edizione 2010
maiuscole e minuscole, se non si vuole fare questa distinzione si può usare l’equivalente
strcasestr.
char s1[100] = "Questa e' una prova", s2[20];
cout << "Inserisci la parola da cercare: "; cin >> s2;
if (strstr(s1,s2)!= NULL) cout << strstr(s1,s2)<< endl;
Se per esempio viene inserito in input “una” il risultato sarà “una prova”, se viene inserito
“a”, il risultato sarà “a e’ una prova”, quindi, in ogni caso viene visualizzata la stringa s1 a
partire dalla posizione in cui viene trovata s2. Da notare che per cercare caratteri invece di
sottostrighe esiste anche la funzione strchr.
Problema: Dato un numero di massimo tre cifre trasformarlo in lettere.
Per esempio se in input si inserisce 51 in output si deve avere “cinquantuno”.
Si tratta di concatenare le parole con cui vengono letti tutti i numeri in modo opportuno.
Esaminiamo il programma passo passo. Prima di tutto bisogna includere le intestazioni compresa quella per le stringhe
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
Conviene inserire, sia all’inizio che nel corpo del programma, commenti con le informazioni
principali sul programma e con le spiegazioni più importanti (il significato delle variabili,
breve spiegazione dell’algoritmo, ecc.)
/* Autore = Prof. Giovanni Calabrese
data = 7/11/2006
Versione = 1.0
Nome Programma = numinlet
Breve descrizione = Trasforma un numero di tre cifre in lettere
Principali dati utilizzati nel programma:
costanti:
par[] = tutte le parole con cui si leggono i numeri da 0 a 19
uni[] = la posizione della parola per i numeri da 0 a 19 nel
vettore par[]
par1[] = le parole per leggere le decine
deci[] = posizione della parola per le decine nel vettore par2[]
variabili:
num = numero letto in input
u = unità
d = decine
ris[] = stringa risultato
*/
Poi dichiariamo tutte le costanti:
const char par[]="zero\0uno\0due\0tre\0quattro\0cinque\0sei\0sette\0”
“otto\0nove\0dieci\0undici\0dodici\0tredici\0quattordici\0”
“quindici\0sedici\0diciassette\0diciotto\0diciannove\0";
const short int uni[] =
{0,5,9,13,17,25,32,36,42,47,52,58,65,72,80,92,101,108,120, 129};
const char par1[]="venti\0trenta\0quaranta\0cinquanta\0sessanta\0”
“settanta\0ottanta\0novanta\0";
const short int deci[] = {0,0,0,6,13,22, 32, 41,50,58};
Invece di inserire tutte le parole in un unico vettore di caratteri si potrebbe utilizzare un vettore di stringhe, cioè un vettore i cui elementi sono vettori di caratteri, in questo modo non sarebbero più necessari i vettori uni[] e deci[] e il programma si semplificherebbe. Provate voi a
modificarlo dopo aver letto il paragrafo sui vettori di vettori.
86
Corso sul Linguaggio C++ Edizione 2010
A questo punto inizia il programma con la dichiarazione delle variabili:
int main(){
int num=-1, i, u, d; char ris[50]="",tmp[50]="";
Nella lettura del numero in input il programma controlla che sia di tre cifre e accetta solo numeri di massimo tre cifre. Se l’utente sbaglia visualizza un messaggio di errore e richiede il
numero. In pratica ripete l’operazione di chiedere il numero fino a quando non viene inserito
un numero valido.
// legge il numero in input
while (num <0 || num >999) {
cout << "Inserisci il numero: "; cin >> num;
if (num <0 || num >999)
cout << "Inserire un numero tra 0 e 999" << endl;
}
se il numero inserito è 0 basta copiare la prima parola del vettore delle parole par[] (“zero”)
nel risultato:
if (num == 0) strcpy(ris,par);
// se num è zero ha finito
altrimenti trova le cifre del numero: u = unità, d=decine e quello che resta in num sono le centinaia, per farlo basta eseguire più volte l’operazione resto della divisione per 10 (num % 10) e
risultato della divisione per 10 (num /=10):
else {
u = num % 10; num /=10; d = num %10; num /= 10;
Dopo aver trovato le tre cifre comincia a trasformare in parole quella delle centinaia, che è
rimasta in num, copiando nel risultato una delle parole “due”, “tre”, “quattro” ecc. a seconda
del valore di num, e poi aggiungendo la parola “cento”
// trasforma le centinaia
if (num > 0) {
if (num > 1) strcpy(ris,par+uni[num]);
strcat(ris,"cento");
}
a questo punto trasforma la cifra delle decine, se è minore di due abbiamo già la parola completa perché nel vettore costante par[] ci sono tutte le parole da “uno” a “diciannove”, altrimenti deve unirci la parola per le unità. Inoltre se la cifra delle unità è 1 o 8 elimina l’ultimo
carattere prima di attaccare la parola delle unità, infatti numeri come 21 o 38 si leggono “ventuno” e “trentotto” e non “ventiuno” e “trentaotto”. Per eliminare l’ultimo carattere basta inserire il carattere di terminazione una posizione più a sinistra (ris[strlen(ris)-1]= '\0';):
if (d+u > 0) // trasforma le decine
if (d <2 )
strcat(ris,par+uni[d*10+ u]);//trasforma insieme decine e unità
else { strcat(ris,par1+deci[d]); // aggiunge parola per decine
// elide l'ultimo carattere se l'unità è 1 o 8
if (u == 1 || u == 8) ris[strlen(ris)-1]= '\0';
if (u) strcat(ris,par+uni[u]); // aggiunge parola per unità
}
}
Scrive il risultato ed ha finito.
cout << "risultato = " << ris << endl ;
system("PAUSE");
return 0;
}
87
Corso sul Linguaggio C++ Edizione 2010
5.7
Le funzioni printf, scanf, puts e gets
Le istruzioni per l’input/output che abbiamo utilizzato finora (cin e cout) e che si possono utilizzare solo includendo il file di intestazioni <iostream>, sono incluse nelle librerie standard
del C++ ma non in quella del linguaggio C. Per utilizzare le istruzioni di input/output del C
bisogna includere il file di intestazioni <stdio.h> che è l’abbreviazione di standard input
output. In un programma C++ si possono includere entrambi i file di intestazione e quindi utilizzare contemporaneamente sia le istruzioni standard del C che quelle aggiunte nel C++.
Inoltre l’istruzione cin del C++ non funziona bene per l’input delle stringhe. Se scriviamo:
char frase[100];
cout << "Inserisci una frase: "; cin >> frase;
e, durante l’esecuzione, inseriamo in input una frase del tipo “Ciao mondo” ci accorgeremo
che nella stringa frase è andata a finire solo la parola “ciao” perché, per l’oggetto cin, il dato
in input termina appena trova uno spazio. Quindi cin non è molto comoda per far leggere le
stringhe.
Le istruzioni per l’I/O standard del C corrispondenti a cin e cout del C++ sono scanf e printf,
inoltre, proprio per favorire l’I/O di stringhe, sono state inserite nella libreria standard le funzioni gets e puts. In realtà le funzioni disponibili sono molte di più, chi volesse studiarle tutte
può far riferimento alla documentazione inserita sul server del laboratorio a cui si è fatto già
riferimento nel paragrafo 3.7.
printf
La funzione printf si richiama con la seguente sintassi:
Sintassi di Printf
printf
,
(
espressione
schema
)
Printf, come cout serve per inviare dati all’unità di output che può essere il video, la stampante oppure un file su disco.
Lo schema è una stringa che serve per dire al computer come deve visualizzare l’elenco di dati che eventualmente la seguono. Lo schema contiene caratteri vari che vengono visualizzati
così come inseriti e comandi per indicare come formattare ciascuno dei dati elencati di seguito
all’interno delle parentesi. Tutti i comandi cominciano col carattere speciale %. Per esempio
int a=10, b=20; printf(“primo numero = %d secondo numero %d”, a,b);
visualizza
primo numero 10 secondo numero 20
il computer costruisce l’output utilizzando la stringa fornita come schema e sostituendo i comandi (la serie di caratteri che inizia con %) con i dati forniti come parametri formattati nel
modo indicato dal comando. Per esempio il comando “%d” significa che il dato deve essere
scritto come numero decimale. La stringa utilizzata come schema può essere anche una stringa variabile, per esempio le istruzioni viste sopra si potevano anche scrivere:
int a=10, b=20; char s[]=“primo numero = %d secondo numero %d”;
printf(s, a, b);
I comandi che si utilizzano nelle stringhe di formato hanno in genere la seguente sintassi:
% [flags] [dimensione] [ . precisione ] tipo [conversione]
88
Corso sul Linguaggio C++ Edizione 2010
per esempio nel comando %-10.8ld, - è un flag, 10 è la dimensione minima del campo in
output, .8 è il numero di cifre che saranno visualizzate, l indica il tipo del dato nel caso sia diverso dal tipo in ci deve essere convertito, d indica il tipo di conversione che verrà effettuato.
Se non si specifica la dimensione verrà assegnato per il dato esattamente il numero di caratteri
che servono. Come si vede tutte le parti del comando sono opzionali escluso il tipo, quindi il
modo più breve per indicare un comando nella stringa di formato è %tipo, per esempio il comando %d dice che in quella posizione deve essere scritto un numero decimale in free-format
ovvero utilizzando il minimo spazio possibile.
Sotto si riporta una tabella con i principali tipi di conversione che si possono utilizzare nelle
stringhe di schema nella funzione printf.
Tabella 4 Elenco dei codici di conversione utilizzati in printf
Conversione
Descrizione
%d %i
utilizzato per i numeri interi, visualizza il numero con uno spazio davanti se
positivo e con il segno - davanti se negativo
%o
Utilizzato con i numeri interi li visualizza in ottale
%x %X
Visualizza i numeri interi in esadecimale
%u
visualizza i numeri interi senza mettere il segno
%f
visualizza il numero con la virgola nel formato virgola fisso (es. 341.46). se si
specifica la precisione per questo comando si intende il numero di cifre dopo
la virgola
%e %E
visualizza il numero con la virgola nel formato virgola mobile, detto anche
formato esponenziale (es 3.4146e+2). Anche qui la precisione indica il numero di cifre che verranno visualizzate dopo la virgola.
%g %G
scrive il numero con la virgola in formato virgola fissa se il numero di cifre di
precisione lo consente, altrimenti lo scrive in virgola mobile (formato esponenziale)
%c
visualizza un carattere
%s
visualizza una stringa
%p
visualizza un puntatore in esadecimale
%%
scrive il carattere %
I principali simboli che si possono usare come flag sono - e +. - significa che il dato verrà allineato a sinistra nello spazio ad esso destinato invece che a destra. + significa che deve essere
sempre inserito il segno per il numero, anche se è positivo, mentre normalmente prima dei
numeri positivi viene visualizzato uno spazio.
scanf
Questa è la funzione principale per l’input di dati da un file o dalla tastiera del linguaggio C.
Ha la stessa sintassi di printf ovvero scanf(<schema>, [&variabile] …). Non approfondiremo
la sintassi, basta sapere che lo schema utilizza più o meno gli stessi codici di printf visti nella
Tabella 4 che, nel caso di scanf servono per dire al computer come deve interpretare i dati letti
dal file o dalla tastiera. Invece delle espressioni bisogna fornire un elenco di indirizzi di variabili di memoria, dove verranno depositati i dati letti.
Il risultato restituito dalla funzione è il numero delle assegnazioni fatte correttamente, se il file
di input termina prima che vengano riempite tutte le variabili viene restituto EOF.
Per esempio per leggere un dato in una variabile intera si scrive:
printf(“Inserisci la base: ”); scanf(“%d”,&base);
89
Corso sul Linguaggio C++ Edizione 2010
Se vogliamo leggere i tre coefficienti di un’equazione di secondo grado possiamo scrivere:
printf("Inserisci i coefficienti: "); scanf(“%f %f %f”,&a, &b, &c);
L’utente li deve inserire tutti sulla stessa riga, inserendo almeno uno spazio tra uno e l’altro.
Se invece vogliamo che l’utente li immetta inserendo una virgola tra uno e l’altro dobbiamo
scrivere:
scanf(“%f,%f,%f”,&a, &b, &c);
Se scriviamo:
printf("Inserisci i coefficienti: ");
printf("sono stati letti %d coefficienti\n", scanf("%f %f %f",&a, &b, &c));
printf("i coefficienti letti sono: %10.3f %10.3f %10.3f\n", a,b,c);
si avrà un output del tipo:
La visualizzazione dei coefficienti è stata fatta in formato virgola fissa, su dieci posizioni
complessive di cui 3 decimali (%10.3f).
Input e output di stringhe di caratteri
Le funzioni puts e gets sono quelle più semplici e efficienti per inviare e leggere una stringa
al video e dalla tastiera. Nel seguito una piccola panoramica delle principali funzioni utili per
l’input-output di caratteri e stringhe:
puts(<stringa>) invia il contenuto della stringa passata come parametro al video aggiungendo un carattere ‘\n’ (precisamente al file stdout = standard output);
fputs(<stringa>,<filestream>) funzione come puts solo che bisogna fornire come parametro
oltre alla stringa anche il puntatore al flusso di output (per inviare al video bisogna indicare
stdout o stderr, per utilizzare un file su disco vedi 7.2)e non invia il carattere ‘\n’;
gets(<stringa>) legge dalla taastiera (precisamente dal file stdin =standard input) un’intera
riga, quindi tutti i caratteri fino a quello di invio, e li trasferisce nella stringa fornita come parametro senza il carattere di invio. Se trova un errore restituisce il puntatore NULL altrimenti
restituisce il puntatore alla stringa letta;
fgets(<stringa>,<num>, <filestream>) legge max num-1 caratteri da un file (tutti quelli presenti dalla posizione corrente fino a fine riga, compreso il fine riga) e li trasferisce nella stringa fornita come primo parametro (deve poter contenere almento num caratteri), alla fine viene
aggiunto il carattere di terminazione ‘\0’. Inserisce nella stringa anche l’eventuale carattere di
fine riga ‘\n’, vedi 7.2;
getchar() legge il prossimo carattere dal buffer di input di stdin e restituisce un numero intero
pari al codice del carattere; se non ci sono più caratteri o se c’è un errore restituisce la costante predefinita EOF (che normalmente è = -1);
fgetc(<filestream>) come getchar() ma richiede il puntatore al flusso di input che per mette di
leggere anche da un file su disco;
Problema: trasformare tutti i caratteri di una frase in maiuscolo
Il programma legge una frase qualsiasi usando gets, esamina ad uno ad uno tutti i caratteri
della stringa letta e, se sono caratteri minuscoli (compresi tra ‘a’ e ‘z’) li trasforma in maiuscolo sottraendo 32 che è la distanza tra un qualsiasi carattere maiuscolo e il corrispondente
minuscolo. Nel programma si utilizzano solo le librerie standard del C.
90
Corso sul Linguaggio C++ Edizione 2010
#include <stdio.h>
#include <string.h>
int main()
{ //Programma Maius. trasforma tutti i caratteri in maiuscolo
char frase[100];
puts("Inserisci una frase: "); gets(frase);
for (int i=0; i< strlen(frase); i++)
if (frase[i] >= 'a' && frase[i] <= 'z') frase[i] -= 32;
puts("\nLa frase trasformata in maiuscolo e':"); puts(frase);
fputs("Batti invio per continuare ...",stdout); getchar();
return 0;
}
5.8
I puntatori
Abbiamo già visto nel paragrafo 4.4 che tutte le variabili sono informazioni memorizzate nella memoria centrale del computer e si trovano, normalmente, o nel segmento dati o nello
stack. Ogni variabile occupa una quantità di spazio diverso a seconda del tipo di dati in essa
contenuti e i vettori sono gruppi di variabili che occupano spazi consecutivi. Abbiamo anche
visto che se nel programma premettiamo l’operatore & al nome di una variabile diciamo al
compilatore che in quel punto vogliamo l’indirizzo della variabile invece del valore della variabile. Anche l’indirizzo di una variabile è un’informazione ed è distinta dal valore della variabile.
Con il linguaggio C è possibile memorizzare l’indirizzo di una variabile in un'altra variabile.
Le variabili che possono contenere l’indirizzo di memoria di un’altra variabile appartengono
ad una specie particolare: le variabili puntatore.
Quando si definisce una variabile di tipo puntatore bisogna specificare anche il tipo di dati a
cui punta, quindi una variabile puntatore ad un intero è diversa da una variabile puntatore ad
un double. Anche se entrambe contengono un indirizzo di memoria sono variabili di tipo diverso, non esiste un unico tipo puntatore. Infatti, come vedremo è possibile fare varie operazioni con i puntatori e queste operazioni comportano istruzioni diverse in linguaggio macchina a seconda del tipo di dati puntato dal puntatore.
La sintassi per la definizione di una variabile di tipo puntatore è simile a quella per la definizione di una qualsiasi variabile basta mettere un asterisco prima del nome della variabile.
Per esempio:
int *p, a, *q; // p e q sono puntatori a int mentre a è una var. normale
int *g=&a; // la definizione può anche inizializzare il puntatore
Supponiamo che la variabile int a=20 sia collocata in memoria all’indirizzo 37286420 e che
sia stato definito una variabile di tipo puntatore a int di nome punt collocata, invece,
all’indirizzo 37286504, con l’istruzione:
punt = &a;
viene assegnato a punt l’indirizzo di a ovvero il numero 37286420.
91
Corso sul Linguaggio C++ Edizione 2010
Memoria centrale
Variabile a
20
37286420
Variabile punt
37286420
37286504
Con il linguaggio C è possibile usare l’operatore di dereferenziazione che è il carattere asterisco (*) per riferirsi al valore della variabile puntata dal puntatore.
Per esempio, nel caso rappresentato in figura, il valore di punt è 37286420 mentre il valore di
*punt è 20. Quindi quando nel programma si scrive in una espressione il nome di una variabile puntatore preceduta da un asterisco vuol dire che in quel punto il computer non deve utilizzare il valore della variabile puntatore ma il contenuto posto nella memoria all’indirizzo che è
contenuto nella variabile puntatore. Questo modi di riferirsi ai valori contenuti in memoria è
detto indirizzamento indiretto.
Sempre considerando l’esempio rappresentato in figura, le due istruzioni:
cout << a;
cout << *punt;
visualizzano entrambe il numero 20 su video.
Quando il compilatore traduce il programma in linguaggio macchina sostituisce i nomi delle
variabili con il loro indirizzo (nei programmi in linguaggio macchina non esistono più i nomi
delle variabili), quindi se traduciamo nel nostro linguaggio quello che il compilatore avrà
scritto in linguaggio macchina otterremo per la prima istruzione qualcosa del tipo:
scrivi in output il valore intero contenuto all’indirizzo37286420
mentre per la seconda istruzione otterremo:
scrivi in output il valore intero contenuto all’indirizzo che troverai memorizzato nella posizione 37286504;
Nella prima istruzione viene indicato direttamente l’indirizzo della locazione di memoria interessata dall’operazione mentre nella seconda viene indicato indirettamente, attraverso un’altra
locazione di memoria, quella del puntatore.
L’operazione * è detta dereferenziazione o indirizzamento indiretto.
Come abbiamo visto negli esempi precedenti, per assegnare un valore alle variabili puntatore
si può utilizzare l’operatore di assegnamento come si fa con le altre variabili, solo che alle variabili puntatore si possono assegnare solo indirizzi di memoria di altre variabili del tipo specificato al momento della creazione del puntatore. Per esempio se punt è un puntatore ad un
valore intero definito con int *punt e num è una variabile intera definita con int num si può assegnare l’indirizzo di num a punt con l’istruzione:
92
Corso sul Linguaggio C++ Edizione 2010
punt = #
a destra dell’uguale ci può essere una qualsiasi espressione che abbia come risultato un indirizzo di memoria di una variabile intera e sappiamo che l’operatore & produce come risultato
l’indirizzo dove è memorizzato il valore della variabile.
Se poi si scrive
a
= *punt * 10;
l’istruzione sarà equivalente a
a = num * 10;
perché *punt indica il valore puntato da punt che corrisponde al valore della variabile num.
Se punt1 è un altro puntatore ad un valore intero possiamo trasferire il valore di punt in punt1
con una normale assegnazione:
punt1 = punt;
perché in questo caso le due variabili sono dello stesso tipo.
Con il linguaggio C++ è possibile definire anche puntatori a void:
void* data;
Il tipo di puntatore a void è un tipo speciale di puntatore. Nel C++, void rappresenta l’assenza
di tipo, così che i puntatori a void sono puntatori ad un valore senza tipo e quindi ad un tipo
indeterminato.
Questo permette ai puntatori void di puntare a qualsiasi tipo di dati, dai tipi numerici elementari ai vettori. Ma, per contro, essi hanno una grave limitazione: il dato puntato non può essere
direttamente dereferenziato (visto che non si conosce il tipo), per questo motivo sarà sempre
necessario eseguire il cast dei puntatori a void verso un altro tipo di puntatore che punta ad un
dato concreto prima di dereferenziarli.
5.9
Puntatori e vettori. Operazioni sui puntatori
Se nel nostro programma abbiamo definito un vettore, come già detto nel paragrafo 5.1, il
nome del vettore in realtà è un puntatore al primo elemento del vettore, però è un puntatore
costante che non può essere modificato. Per esempio se abbiamo definito il vettore di caratteri
char frase[50], l’identificatore frase rappresenta la posizione di memoria del primo elemento
del vettore e quindi corrisponde a &frase[0]. Questo significa che se abbiamo creato un puntatore ad un valore dello stesso tipo degli elementi del vettore, per assegnare a questo puntatore l’indirizzo del primo elemento del vettore basta scrivere dopo l’uguale, nell’istruzione di
assegnazione, il nome del vettore senza parentesi quadre. Per esempio, con le seguenti istruzioni:
float num[50]; float *p; p=num;
viene definito il vettore di numeri num e il puntatore p ad un valore float, poi si assegna la posizione del primo elemento del vettore al puntatore p. Da questo momento in poi, per riferirmi
93
Corso sul Linguaggio C++ Edizione 2010
ad un elemento del vettore posso utilizzare sia il nome num che il nome p, quindi num[k] e
p[k] sono la stessa cosa.
Un altro fatto importante è che è possibile eseguire varie operazioni aritmetiche sui puntatori:
• Incremento e decremento con gli operatori ++ e --. Quando si applicano questi operatori l’indirizzo di memoria contenuto nella variabile puntatore viene aumentato o
diminuito di tanti byte quanti sono quelli occupati dalla variabile a cui punta, quindi
se il puntatore punta ad un elemento di un vettore, dopo l’incremento o il decremento
punterà rispettivamente all’elemento successivo o precedente nello stesso vettore.
• Somma o sottrazione di una costante intera. Ad un puntatore si può aggiungere o
sottrarre un numero intero utilizzando le operazioni + e -, il risultato sarà un puntatore
dello stesso tipo spostato rispettivamente in avanti o indietro di tante posizioni quanto
specificato nella costante. Per esempio se aggiungiamo 4 ad un puntatore che punta ad
un elemento di un vettore otterremo un puntatore che punta all’elemento dello stesso
vettore che si trova 4 posizioni più avanti. Inoltre se p è un puntatore al primo elemento del vettore, scrivere p[k] o *(p+k) è la stessa cosa, entrambe le notazioni indicano il valore dell’elemento k del vettore. Gli operatori [] in realtà eseguono
l’addizione tra il puntatore e il valore contenuto tra le parentesi e dereferenziano il
puntatore ottenuto come risultato della somma.
• Differenza tra due puntatori. Il risultato indica quante variabili del tipo puntato dai
puntatori sono presenti tra le due posizioni puntate. Per esempio se a punta al primo
elemento del vettore e b punta al quinto, b-a darà come risultato 4.
• Confronto tra puntatori. Si possono utilizzare i normali operatori relazionali <, <=,
==, ecc. Il risultato indicherà se la posizione in memoria puntata dal primo puntatore
viene prima, è uguale o viene dopo di quella puntata dal secondo puntatore.
Ad un puntatore si può anche assegnare la costante NULL per dire che il puntatore è vuoto,
ovvero che non punta a niente. Al posto della costante NULL si può utilizzare il carattere ‘\0’.
Per inciso si osserva che si possono definire anche puntatori a una funzione, in questo caso
dereferenziando il puntatore, si manda in esecuzione la funzione.
Problema: Sostituire in una frase tutte le occorrenze di una parola con un'altra parola data in
input.
Si può usare la funzione strstr per cercare una stringa in un’altra stringa, se la troviamo trasferiamo prima tutti i caratteri fino alla posizione trovata e poi la nuova parola. Questi passi li ripetiamo fino a quando non troviamo più la stringa.
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
int main()
{ // programma Sost. Cerca una sottostringa in una stringa
char s1[100] = "Oggi Gino è andato al mare ma domani Gino andrà in montagna";
char ris[100]="", s2[20], s3[20], *p; int i=0, k, l;
cout << "Inserisci la parola da cercare: "; cin >> s2; l=strlen(s2);
cout << "Inserisci la parola da sostituire: "; cin >> s3;
p=s1; // p è il puntatore che ci dice dove siamo arrivati nella ricerca
while (strstr(p,s2)!=NULL) {
k=strstr(p,s2)-p; // k = numero di caratteri prima della parola s2
strncat(ris,p,k); // trasferisce nel risultato esattamente k caratt.
strcat(ris,s3); // trasferisce la nuova parola
p+=k+l;
94
Corso sul Linguaggio C++ Edizione 2010
}
strcat(ris, p);
cout << "il risultato e': " << endl << ris << endl;
system("PAUSE"); return 0;
}
5.10
Sottoprogrammi, vettori e puntatori
I vettori, così come gli altri tipi di dati, possono essere passati come parametri ad un sottoprogramma ma il passaggio avviene sempre per riferimento. Infatti abbiamo visto che il nome di
un vettore è un puntatore costante al primo elemento del vettore e quindi è un indirizzo di
memoria per cui al sottoprogramma viene sempre passato l’indirizzo del vettore e non il valore dei suoi elementi.
Per esempio se vogliamo scrivere un sottoprogramma per leggere n numeri double in un vettore di massimo 100 elementi possiamo definire come parametri il vettore e il numero intero
n. La definizione del sottoprogramma potrebbe essere la seguente:
#define MAX 100
void leggi(double num[MAX], int n) {
for (int i=0; i< n; i++) {
cout << “inserisci l’elemento “ << i+1 << “: “; cin >> num[i];
}
}
Ma l’intestazione del sottoprogramma poteva anche essere:
void leggi(double *num, int n) {
perché in entrambi i casi diciamo al compilatore che il primo parametro sarà un puntatore a
variabili di tipo double.
Quindi quando si passa un vettore ad un sottoprogramma in realtà gli si sta passando un puntatore e quindi il passaggio avviene sempre per riferimento.
Problema: Assegnato nome e cognome generare i primi 6 caratteri del codice fiscale
Le regole per generare i primi 6 caratteri del codice fiscale sono le seguenti:
3 caratteri alfabetici per il cognome. Si prendono le prime tre consonanti del cognome.
Se il cognome contiene solo due consonanti si prendono queste due e la prima vocale, se
contiene solo una consonante si prende questa e le prime due vocali. Se il cognome
contiene meno di tre lettere si prendono tutte le lettere e si aggiunge una X per ogni lettera
mancante. I cognomi costituiti da più parole si considerano come un’unica ininterrotta
successione di caratteri.
3 caratteri alfabetici per il nome. Si prende la prima, la terza, e la quarta consonante del
nome. Se il nome contiene solo tre consonanti si prendono queste tre. Se ne contiene meno
si procede come per il nome.
Quindi sia per il nome che per il cognome bisogna separare le vocali dalle consonanti, conviene scrivere un sottoprogramma per fare questa separazione. Conviene anche trasformare
tutto in maiuscolo. Il programma è il seguente:
#include <iostream.h>
#include <stdio.h>
95
Corso sul Linguaggio C++ Edizione 2010
#include <stdlib.h>
#include <string.h>
// programma codfis. Genera i primi sei caratteri del codice fiscale
void maiusc(char *frase) {
// trasforma in maiuscolo tutti i caratteri di una stringa
for (int i=0; i< strlen(frase); i++)
if (frase[i] >= 'a' && frase[i] <= 'z') frase[i] -= 32;
}
void separa(char *parola, char *cons, char *voc) {
/* Separa le vocali dalle consonanti, salta i caratteri non alfabetici
Tutti i caratteri devono essere maiuscoli altrimenti vengono ignorati
il risultato in cons[] = consonanti e voc[]=vocali
*/
for (int i=0; i<strlen(parola); i++)
if (parola[i]=='A' || parola[i] == 'E' || parola[i] == 'I'
|| parola[i] == 'O' || parola[i] == 'U') {*voc=parola[i]; voc++;}
else
if (parola[i] > 'A' && parola[i] <= 'Z')
{*cons = parola[i]; cons++;}
*cons='\0'; *voc='\0'; // chiude le due stringhe cons e voc
}
int main(){
char cognome[30], nome[30], c[30], v[30], ris[7]="";
cout << "Inserisci il Cognome: "; gets(cognome); maiusc(cognome);
cout << "\nInserisci il Nome: "; gets(nome); maiusc(nome);
// crea una stringa con tutte le consonanti+ tutte le vocali + "XXX"
// per il cognome
separa(cognome,c,v); strcat(c,v); strcat(c,"XXX");
strncpy(ris,c,3); // mette in ris i tre caratteri del cognome
// tratta il nome
separa(nome,c,v);
if (strlen(c) > 3) {strncat(ris,c,1); strncat(ris,c+2,2);}
else {strcat(c,v); strcat(c,"XXX"); strncat(ris,c,3);}
cout << "\n il risultato è: "<< ris << endl;
system("PAUSE");
return 0;
}
5.11
Tipi particolari di puntatori
E’ possibile definire variabili puntatori che puntano ad una costante, per esempio
const int* ptc;
definisce un puntatore variabile che punta ad una costante intera. Lo specificatore const si riferisce al dato puntato dal puntatore e non al puntatore stesso che resta una variabile e, quindi,
nel programma il suo valore può essere modificato mentre non è ammesso modificare il valore della variabile puntata:
const int cc=10; int vv = 20;
ptc= &cc;
//istruzione ammessa
*ptc=20;
// istruzione non ammessa, la variabile puntata è costante!
ad un puntatore ad un costatnte si può assegnare anche l’indirizzo di una variabile perché è
ammessa la conversione da puntatore a variabile a puntatore a costante
ptc=&vv;
96
Corso sul Linguaggio C++ Edizione 2010
I puntatori a costante si utilizzano soprattutto per passare dei puntatori come parametri ad una
funzione con la sicurezza che le variabili puntate non potranno essere modificate durante
l’esecuzione della stessa.
Si possono definire anche dei puntatori costanti utilizzando *const al posto di *. Per esempio
float ff;
float *const ptc= &ff;
in questo caso ptc è un puntatore costante ad una variabile float. E’ possibile modificare la variabile dereferenziando il puntatore ma non è possibile modificare il puntatore stesso:
*ptc = 2.3415;
// ammesso perché il puntatore punta ad una variabile
float f1=0.1254e+10;
ptc=&f1;
// non ammesso perché ptc è costatnte
Il classico tipo di puntatore costante è il nome di un vettore.
Ripetendo due volte const si possono anche definire dei puntatori costanti a costante:
const char dato=’A’; const char ptr *const= &dato;
in questo caso ptr è un puntatore costante ad una costante e quindi non si può modificare né il
puntatore né il dato puntato.
Si possono definire anche puntatori ad una variabile puntatore ripetendo due volte l’asterisco:
int **ppt; // ppt è un un puntatore a un puntatore ad una var intera
Puntatori e funzioni
Il valore restituito da una funzione può essere di qualsiasi tipo, anche del tipo puntatore:
int* funz(); // dichiara una funzione che restituisce un puntatore a int
Nel linguaggio C esistono anche i puntatori a funzione. Questi servono quando un programma
deve mandare in esecuzione una funzione che viene scelta durante l’esecuzione in base
all’input dell’utente e che quindi non è nota a priori.
I puntatori a funzione devono essere dichiarati senza definire il blocco della funzione. Successivamente verrà assegnato al puntatore l’indirizzo di una funzione esistente. Per esempio:
int * (*pfunz)(dounle,*char);
dichiara un puntatore di nome pfunz ad una funzione che ha come parametri un numero double e un puntatore a char e che restituisce un puntatore ad un intero. Da notare che le parentesi
intorno al nome della funzione sono necessarie, infatti in assenza il compilatore interpreterebbe la dichiarazione come il prototipo di una funzione che restituisce un puntatore ad un puntatore intero.
Successivamente, nel corso del programma, il puntatore deve essere inizializzato con
l’indirizzo di una funzione effettiva e precedentenemente dichiarata nel programma con lo
stesso numero e tipo di parametri e con lo stesso tipo restituito. Per esempio:
int* funz1(double , char* );
int* funz2(double , char* );
…………
if ( ......... ) pfunz = funz1 ; else pfunz = funz2;
Per richiamare la funzione puntata da pfunz si può dereferenziare o meno il puntatore:
*pfunz(12.6,”Gino”)
oppure
pfunz(12.6,”Gino”)
i due modi sono equivalenti.
Si possono anche creare dei vettori di puntatori a funzioni con istruzioni del tipo:
float (*apfunz[10])(int);
97
Corso sul Linguaggio C++ Edizione 2010
definisce un vettore di 10 puntatori a funzioni che hanno un parametro int e che restituiscono
un valore float.
I vettori di puntatori a funzioni possono essere utili, per esempio, se si realizza un programma
a menù e si vuole far eseguire una particolre funzine in base alla scelta dell’utente senza utilizzare istruzioni IF o Switch.
Per esempio:
double
(*apfunz[5])(int)
=
{f1, f2, f3, f4, f5}
dichiara un vettore di puntatori a funzione di 5 elementi e lo inizializza con gli indirizzi delle
funzioni f1, f2, f3, f4, f5 che devono essere funzioni precedentemente dichiarate nel programma con istruzioni del tipo double f1(int x){ …}, ecc.
Per assegnare l’indirizzo di una funzione ad un elemento del vettore può anche utilizzare una
normale istruzione di assegnazione:
apfunz[2]=f3;
Per richiamare una delle funzioni puntate si utilizza:
apfunz[i](n);
// non è necessario dereferenziare il puntatore
dove, per esempio, la variabile i potrebbe contenere il numero della voce del menù scelta
dall’utente.
Come ultima cosa faccio notare che è anche possibile utilizzare come parametro in una funzione un puntatore ad un'altra funzione. Per esempio:
void fsel(int (*pfunz)(float)) //dichiarazione della funzione fsel
{ .... n = pfunz(r); .....}
(dove n é di tipo int e r é di tipo float)
int funz1(float);
int funz2(float);
fsel(funz1); //chiamate di fsel
fsel(funz2);
98
Corso sul Linguaggio C++ Edizione 2010
5.12
Esercizi
84) Supponiamo che sia presente in memoria il seguente vettore creato con intA[5]
5
1
3
4
6
2
Viene anche creato un vettore B con int B[5]. Dopo l’esecuzione delle seguenti istruzioni
cosa conterrà il vettore B[]?
for (i = 1; i<5; i++) B[i]=A[i-1]+A[i];
B() =
85) Dire quale dei seguenti frammenti di programma calcola in j la media del vettore di interi
positivi v contenente n elementi, posto che j sia inizializzato a zero:
a) for(i=0; i<n; i++) j = v[i]; j = j/n;
b) for(i=0; i<n; i++) j += v[i]; j = j/n;
c) for(i=0; i<n; i++) j += v[i];
86) Dire quale dei seguenti frammenti di programma calcola in j l'indice del primo elemento
positivo del vettore di interi v contenente n elementi, posto che j sia inizializzato a zero
(potete assumere che il vettore contenga sempre almeno un elemento positivo):
a) for(i=0; i<n; i++) if (v[i] > 0) j=i;
b) for(i=n-1; i>=0; i--) if (v[i] > 0) v[j]=v[i];
c) for(i=n-1; i>=0; i--) if (v[i] > 0) j=i;
87) Considerate il seguente frammento di programma:
t=-1;
for (i=1; i<=n; i++)
if (f(i)) t=i;
if (t>=0) cout << t;
Quale delle seguenti affermazioni è corretta?
a) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale
intero non esiste, il programma entra in un ciclo infinito.
b) Il programma cerca e stampa il più grande intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito.
c) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale
intero non esiste, il programma non stampa nulla.
d) Il programma cerca e stampa il più grande x fra 1 e n tale che f(x)!=0; se tale intero
non esiste, il programma non stampa nulla.
88) Dato il seguente frammento di programma:
int a[5]={10, 7, 4, 2, 1};
int b[5]={3,2,5,6,4};
int i, j, k;
for(i=0; i<5; i++){
k=1;
for(j=4; j>=0; j--)
if (a[i]<=b[j])
99
Corso sul Linguaggio C++ Edizione 2010
k=0;
if (k)
printf("%d ", a[i]);
}
Indicare i valori stampati in uscita:
a) 10 7
b) 10 7 4
c) 10 7 4 2
d) 10 7 4 2 1
e) 2 1
f) 1
g) nessuna delle risposte precedenti è corretta
89) Qual è il ciclo corretto per trovare l'indice del primo elemento negativo in un vettore dichiarato come float v[MAX] ?
a) while(i<MAX) if(v[i]>=0) i++;
b) while(v[i]>=0 && i<MAX) i++;
c) while(i<MAX || v[i]<0) i++;
d) while(i<MAX && v[i]>=0.0) i++;
90) Dato il seguente pezzo di programma
t=x[1];
for (i = 2 ; i <=n ; i++) t*=x[i]/i ;
Quale dei seguenti calcoli viene effettuato?
x(1) + x(2) + ..... + x(n)
n
x(1) * x(2) * ..... * x(n)
b) t =
n
x(1) * x(2) * ..... * x(n)
c) t =
n!
x(3)
x ( n)
x ( 2)
d) t = x(1) *
+ x(1) *
+ ...... + x(1) *
n
3
2
a) t =
91) Scrivere il pezzo di programma per calcolare la seguente somma supponendo presente in
memoria e già valorizzato il vettore float X[…] con N elementi.
1
1
1
1+
+
+ ...... +
x(1) X (2)
X (N )
92) Sia dichiarato un array int a[10]. Cosa succede dopo l'esecuzione del seguente codice:
for (i=0; i<=10; i++)
a[i]=2;
a)
b)
c)
d)
Tutto l'array viene inizializzato al valore 2;
Si superano i limiti dell'array. L'errore viene segnalato in fase di compilazione;
Si superano i limiti dell'array. L'errore viene segnalato all'esecuzione del codice;
Si superano i limiti dell'array. L'errore non viene segnalato, ma si hanno conseguenze
imprevedibili;
100
Corso sul Linguaggio C++ Edizione 2010
e) Si superano i limiti dell'array. All'accesso all'undicesimo elemento, il sistema operativo assegna altro spazio all'array a e l'esecuzione prosegue.
93) Supponete che i e j siano variabili intere, e che s e t siano vettori di interi. Assumete che,
in un dato istante, i=j=0 e che il contenuto dei due vettori sia il seguente:
s = { 0,
1,
2,
3}
t = { 3,
2,
1,
0}
Se viene eseguita la seguente istruzione:
s[i++]=t[++j]+1;
Quali valori avranno alla fine le variabili i e s?
a) i= 0, s= [3,2,1,0]
b) i = 1, s= [2,1,0,3]
c) i= 1, s=[4,3,2,1]
d) i=1, s=[3,1,2,3]
e) i=0, s=[3,1,2,3]
f) nessuno dei precedenti
94) Quale sarà il contenuto del vettore V al termine del ciclo se è V={1,21,31,4,51,6} e n=6?
t=n/2;
for (i=0; i<t ;i=i+1) {
temp=V[i]; V[i]=V[n-i-1]; V[n-i-1]=temp;
}
Programmi con i vettori
95) Calcolare lo scarto quadratico medio di un vettore numerico. Se x(i), con i da 1 a n, sono
gli elementi del vettore e se M è la media aritmetica degli stessi, per definizione lo scarto
quadratico medio, normalmente indicato con σ, si ottiene con la seguente formula:
n
σ=
∑ ( x(i) − M )
2
i =1
n
96) Moltiplicare 2 vettori (il risultato è la somma dei prodotti di tutti gli elementi con lo stesso
indice nei due vettori).
97) Scrivere un programma per far leggere n numeri (n<=100) e visualizzarli al contrario.
98) Prendendo spunto dal programma descritto nel paragrafo 5.3 realizzare un programma per
trasformare in lettere un qualsiasi numero intero fino a un miliardo.
99) Dato un vettore numerico (max 4 cifre ogni numero) contare quanti elementi sono compresi tra 0 e 99, quanti fra 100 e 199, quanti fra 200 e 299 ecc. fino a contare quanti sono
compresi fra 9900 e 9999.
100) Dato un vettore qualsiasi individuare la moda ovvero l’elemento che si ripete più volte.
101) Far caricare da tastiera in un vettore gli n+1 coefficienti di un polinomio P(x) di grado n:
101
Corso sul Linguaggio C++ Edizione 2010
P(x) = a0 + a1x + a2x2 + a3x3 + ....... + an-1xn-1 + anxn
poi far calcolare e visualizzare il valore del polinomio per K valori di x a partire da un numero
A con un incremento di S (K,A,S in input).
102) Far determinare i numeri primi da 1 a 1000 con il crivello di Eratostene (inserire i numeri da 1 a 1000 in un vettore poi partendo dal primo cancellare tutti i multipli, andare avanti nel vettore considerando gli elementi non cancellati e per ognuno cancellarne i multipli, alla fine visualizzare i numeri non cancellati).
103) Far determinare i primi N numeri primi mettendoli in un vettore. (Mettere 2 nel primo elemento del vettore poi, considerando solo i numeri dispari da 3 in poi controllare che
non siano multipli di nessun elemento presente nel vettore e inferiore alla radice quadrata
del numero, se questa condizione si verifica si può inserire l'elemento nel vettore perché
é primo).
104) Far visualizzare le prime n righe del triangolo di Tartaglia:
1 1
1 2 1
1 3 3 1
1 4 6 4 1
. . . . . . .
(la prima riga è costituita da due uno, il primo e ultimo numero di ogni riga è sempre 1, ogni
altro numero è uguale alla somma di due numeri della riga precedente: quello che si trova
proprio sopra il numero da determinare e quello immediatamente a sinistra).
105) Fare un programma che assegnato una frase isoli e metta in un vettore tutte le parole in
essa contenute.
106) Fare un programma per caricare un testo in un vettore di stringhe.
107) Fare un programma che assegnata in input una parola ne generi tutti gli anagrammi possibili (anche quelli senza significato).
108) Far leggere una serie di nomi con il relativo indirizzo e numero di telefono e far stampare la lista in ordine alfabetico.
109) Dato un vettore ordinare gli elementi di posto dispari in ordine crescente e quelli di posto pari in ordine decrescente.
110) Dati due vettori numerici ordinati, riunirli in un terzo sempre ordinato (fusione).
111) Dato un vettore far visualizzare l'elemento mediano (cioè quello che si trova al centro
del vettore dopo l'ordinamento).
112) Far inserire in input le N squadre del campionato e i punti totalizzati dopo di che far visualizzare la classifica.
102
Corso sul Linguaggio C++ Edizione 2010
113) Assegnare ad un vettore di 40 elementi, ognuno di due caratteri, delle sigle per simulare
un mazzo di carte napoletane, per esempio "1C","2C","3C" ecc. potrebbero indicare le
carte asso di coppe, due di coppe, 3 di coppe ecc., "1S","2S" ...potrebbero indicare asso
di spade, due di spade e analogamente per tutte le altre carte. Fare un programma per mischiare le carte usando la funzione rand.
114) Problema assegnato alla selezione regionale del 2001. È venerdì, e il cassiere Camillo
ha davanti a sé una lunga fila di clienti della sua banca venuti a ritirare contante per il
weekend. Per fare presto, Camillo decide di usare per ogni cliente il numero minimo possibile di banconote. Sapreste scrivere un programma per evitargli il mal di testa, considerato che ha a disposizione banconote da 100.000, 10.000, 5.000, 2.000 e 1.000 in quantità
illimitata e che l'entità di ogni prelievo è un multiplo di 1.000 lire?
Puntatori
115) Quale delle seguenti istruzioni non ha lo stesso effetto sulle due variabili a e b?
a) int a,b; a=b;
b) int a,b, *p,*c; p=&a; c=&b; *p=*c;
c) int a,b, *p,*c; p=&a; c=&b; *p=b;
d) int a,b, *p,*c; p=&b; c=&a; *c=b;
e) int a,b, *p,*c; p=&a; c=&b; p=c;
f) nt a,b, *p,*c; p=&a; c=p; *c=b;
116) Si consideri il seguente frammento di codice:
void foo( int *a, int b) {
int temp = *a; *a = b; b = temp;
};
int main( ) {
int a = 1, b = 5;
foo( &a, b );
}
Quanto valgono le variabili a e b alla fine dell'esecuzione?
Risposte esercizi
Esercizio 84)
B() =
6
4
7
10
8
Esercizio 85) Risposta = b)
Esercizio 86) Risposta = c)
Esercizio 87) Risposta = d)
Esercizio 88) Risposta = a)
Esercizio 89) Risposta = d)
Esercizio 90) Risposta = c);
Esercizio 92) Risposta = d)
Esercizio 93) Risposta =d)
Esercizio 94) Risposta = 6, 51, 4, 31, 21, 1
Esercizio 114) Il cassiere Camillo – problema assegnto alla selezione regionale 2001
Si vuole realizzare un programma che data in input la somma da pagare calcoli quante banconote da 100000, quante da 10000, quante da 5000, quante da 2000 e quante da 1000 sono necessarie. In pratica un numero in input e 5 numeri in output. Per utilizzare il numero minimo
103
Corso sul Linguaggio C++ Edizione 2010
di banconote basta utilizzare se possibile i tagli più grandi ovvero per pagare 100000 bisogna
utilizzare una banconota da 100000 e non 10 da 10000. Si possono inserire i 5 tagli in un vettore numerico e poi in un ciclo clalcolare per ogni taglio quante banconote servono cominciando dal più grande. Per questo calcolo basta dividere la somma che resta da pagare per il
taglio delle banconote il quoziente è il numero di banconote che bisogna utilizzare per quel
taglio e il resto è la somma da pagare con i tagli più piccoli.
Il programma è il seguente:
#include <stdio.h>
#include <stdlib.h>
// programma cam.cpp - Cassiere Camillo- selezioni regionali 2001
int main(){
int tb[]={100000, 10000, 5000, 2000, 1000}; // i 5 tipi di banconote
int somma, i; // somma = l'importo in input
int nb[5]; // nb[] = numero banconote per ogni taglio (dati in output)
printf("Inserisci la somma da pagare: "); scanf("%d", &somma);
for (i= 0; i <5; i++) {
nb[i] = somma / tb[i]; // numero banconote del taglio i
somma %= tb[i];// quello che resta dopo aver dato il taglio i
printf("Servono %d banconote da %d \n", nb[i], tb[i]);
}
system("PAUSE");
return 0;
}
Esercizio 115) Risposta = e)
Esercizio 116) a=5; b=5
104
6
Capitolo
Corso sul Linguaggio C++ Edizione 2010
6. Strutture, union, matrici, tabelle e
l’allocazione dinamica della memoria
6.1
Strutture
Se vogliamo creare uno spazio di memoria che possa contenere più dati di tipo eterogeneo,
non possiamo utilizzare i vettori (che, invece, si utilizzano per un gruppo di dati tutti dello
stesso tipo) ma dobbiamo utilizzare un tipo particolare detto struct.
Il tipo struct serve per definire uno spazio di memoria adatto per contenere più variabili di tipo diverso. In altri linguaggi questo tipo di dato viene detto record perché viene utilizzato
principalmente per contenere una registrazione di un archivio, per esempio le informazioni su
un cliente o su un prodotto.
La sintassi per dichiarare una struct è:
Definizione di una struct
struct
nome struct
{
Tipo
dato
Nome
elemento
;
}
Nome
variabile
,
Per esempio, volendo creare un struttura per contenere le informazioni su un prodotto in magazzino:
struct tipo_prodotto {
char
codice[10];
char
desc[50];
double prezzo;
float
quant;
float
sconto;
double costo;
};
La struttura rappresenta un tipo di dati, per utilizzarla all’interno di un programma bisogna dichiarare delle variabili di questo nuovo tipo:
tipo_prodotto prod1, prod2;
Sia la variabile prod1 che la variabile prod2 possono contenere più informazioni, tutte quelle
definite come parte della struct tipo_prodotto, queste informazioni si chiamano campi.
Nell’esempio prod1 e prod2 possiedono i seguenti campi: codice, desc, prezzo, quant, sconto,
costo. Come avviene per i vettori ogni campo è una variabile indipendente e i vari campi sono
situati in memoria consecutivamente, ognuno in un proprio spazio. Per riferirsi nel program105
Corso sul Linguaggio C++ Edizione 2010
ma ad un campo di una struct si utilizza il nome della variabile seguita da un punto e dal nome del campo. Per esempio per far inserire da tastiera la descrizione del prodotto contenuta in
prod1 possiamo scrivere:
gets(prod1.desc);
utilizziamo gets invece di cin perché la descrizione potrebbe contenere spazi.
Per visualizzare in output il prezzo del prodotto prod1 possiamo scrivere
cout << prod1.prezzo;
Una variabile di tipo struct, come qualsiasi altra variabile, può essere passata come parametro
ad un sottoprogramma sia per valore che per riferimento. Per esempio il seguente programma
legge i dati di un prodotto e li stampa:
#include <iostream.h>
#include <stdlib.h>
#include <stdio.h>
struct tipo_prodotto {
char
codice[10];
char
desc[50];
double prezzo;
float
quant;
float
sconto;
double costo;
};
void leggiprodotto(tipo_prodotto &p){//passaggio per riferimento
cout << "Inserisci il codice:\t";
gets(p.codice);
cout << "Inserisci la descrizione:\t"; gets(p.desc);
cout << "Inserisci il prezzo:\t";
cin >> p.prezzo;
cout << "Inserisci la quantita':\t";
cin >> p.quant;
cout << "Inserisci lo sconto:\t";
cin >> p.sconto;
cout << "Inserisci il costo:\t";
cin >> p.costo;
}
void visualizza(tipo_prodotto p){
cout << "il prodotto letto è: " << endl;
printf("%10s %25s %10s %10s %6s %6s\n",
"Codice","Descrizione","Prezzo","Quantita'","Sconto","Costo");
printf("%10s %25s %10.2f %10.2f %6.2f %6.2f\n",
p.codice, p.desc, p.prezzo, p.quant, p.sconto,p.costo);
}
int main(){
tipo_prodotto articolo;
leggiprodotto(articolo);
visualizza(articolo);
system("PAUSE");
return 0;
}
mandandolo in esecuzione il risultato è
Si possono anche definite dei puntatori ad una variabile di tipo struct, per esempio:
tipo_prodotto *p;
106
Corso sul Linguaggio C++ Edizione 2010
in questo caso, per far riferimento ai campi della variabile puntata da p, si usa una sintassi particolare: <nome puntatore>-><nomecampo>
Invece del punto (.) si usa l’operatore -> Per esempio:
cout << p->codice << ‘\t’ << p->desc << ‘\t’ << p->prezzo;
6.2
Le union
Le union sono particolari strutture e si definiscono con la stessa sintassi delle strutture usando,
però, la parola chiave union invece di struct. Quindi anche le union sono un insieme di campi
ma a differenza delle struct tutti i campi delle union condividono lo stesso spazio di memoria. Questo significa che se si modifica un campo di una union anche gli altri campi saranno
interessati dalla modifica. In una union i campi non sono indipendenti perché occupano lo
stesso spazio di memoria, c’è in pratica un solo spazio di memoria che può essere interpretato
in diversi modi.
Problema: rappresentazione interna di una variabile
Vogliamo visualizzare la rappresentazione interna di una variabile di memoria di tipo qualsiasi.
Sappiamo che qualsiasi dato immesso in memoria viene trasformato in una serie di bit ovvero
in una serie di zeri e uno, vogliamo visualizzare il contenuto binario della memoria.
Si può utilizzare una union per far condividere lo spazio utilizzato dalla variabile con un vettore di char e poi visualizzare la rappresentazione interna di ogni elemento del vettore con un
ciclo in cui si esegue l’and bit a bit dell’elemento del vettore con una maschera contenente
tutti zeri e un solo bit = 1, quello che si vuole visualizzare. Se l’and tra l’elemento del vettore
e la maschera è 0 il bit sarà zero altrimenti è 1.
Il programma è il seguente.
#include <iostream.h>
#include <stdlib.h>
int main()
{ //programma rapint. Rappresentazione interna di una variabile
// vc.x =variabile di cui si vuole conoscere la rappresentazione interna
const unsigned char msk1=0x80; // corrisponde al numero binario 10000000
// viene assegnato lo stesso spazio alla variabile x e al vettore lt
union cond
{long double x;
char lt[12];};
cond vc;
unsigned char mask; int sz,i,j;
cout << "Inserisci il valore di cui vuoi vedere la rappresentazione: ";
cin >> vc.x;
cout << "Rappresentazione interna = \n";
sz = sizeof(vc.x);
for (i=0; i<sz; i++) // controlla un byte alla volta
{mask=msk1;
for (j=0; j<8; j++) // controlla un bit alla volta e lo stampa
{if (vc.lt[i] & mask) cout << "1"; else cout << "0";
mask = mask >> 1; }
cout << ' '; }
system("PAUSE");
return 0; }
107
Corso sul Linguaggio C++ Edizione 2010
Questo programma si può utilizzare per visualizzare la rappresentazione interna di qualsiasi
variabile, se per esempio volessimo visualizzare la rappresentazione interna di una variabile
int basta sostituire nella union la riga long double x; con int x;
6.3
Le matrici e le tabelle
Gli elementi di un vettore possono essere di qualsiasi tipo, non necessariamente un tipo elementare. Questo significa che gli elementi di un vettore possono essere a loro volta vettori o
struct (ovvero record), o union. Anche gli elementi di una struct possono essere di qualsiasi
tipo compreso altre struct, in questo caso di parla di struct annidate.
Possiamo, per esempio, creare vettori di vettori che normalmente vengono chiamati matrici,
oppure vettori di struct (vettori di record) che normalmente vengono chiamati tabelle. È anche possibile creare struct con dei campi che sono vettori. Inserendo vettori, struct, union in
altri vettori, struct e union si possono creare strutture di dati di elevata complessità e tali da
rispondere a tutte le esigenze dei programmatori.
Matrici
Esaminiamo in particolare i vettori i cui elementi sono a loro volta dei vettori, questo tipo di
strutture di dati viene chiamato matrice o vettore a due dimensioni. Per esempio se abbiamo
un vettore di dimensione 3 i cui elementi non sono variabili semplici ma sono a loro volta dei
vettori di dimensione 4, otterremo un vettore che contiene in totale 3 * 4 = 12 elementi. Un
vettore di questo tipo si dice che ha due dimensioni (3 e 4) e può essere rappresentato graficamente nel seguente modo (3 righe e 4 colonne):
per definirlo con il linguaggio C bisogna specificare le due dimensioni tra parentesi quadre.
Per esempio:
int a[3][4]; // 3 vettori ciascuno di 4 elementi interi
char testo[10][80]; // 10 stringhe ciascuno con massimo 79 caratteri
Si può inizializzare una matrice come si fa con un vettore elencando semplicemente tutti i valori da inserire:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
oppure, per comodità di lettura, distinguendo i vari vettori che la compongono:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
Normalmente quando si visualizza una matrice si visualizza un vettore per riga e poi, si incolonnano gli elementi che nei vari vettori si trovano nella stessa posizione. Per esempio, per la
matrice di tre vettori ciascuno di 4 numeri inizializzata nell’esempio precedente prima si ottiene:
1
2
3
4
5
3
7
8
9
10
11
12
quindi la prima dimensione riguarda il numero di righe mentre la seconda indica il numero di
colonne.
Le righe sono numerate da 0 a 2 mentre le colonne da 0 a 3. Il numero degli elementi di una
matrice è pari al prodotto del numero delle righe per il numero delle colonne. Per far riferi108
Corso sul Linguaggio C++ Edizione 2010
mento ad un elemento della matrice bisogna specificare il numero della riga e quello della colonna, per esempio l’elemento con valore 7 appartiene alla riga 1 colonna 2, per far riferimento ad esso si utilizza la scritta:
a[1][2]
il primo indice indica la riga mentre il secondo la colonna.
L’inizializzazione della matrice poteva anche essere fatta con le seguenti istruzioni:
a[0][0]=1; a[0][1]=2; a[0][2]=3; a[0][3]=4;
a[1][0]=5; a[1][1]=6; a[1][2]=7; a[1][3]=8;
a[2][0]=9; a[2][1]=10; a[2][2]=11; a[2][3]=12;
Per visualizzare una matrice su video si utilizza un doppio ciclo for come nell’esempio seguente che segue che visualizza la matrice 3 x 4 dell’esempio precedente:
for (int i=0; i<3; i++)
{for (int j=0; j<4; j++) cout << a[i][j] << '\t';
cout << endl;
}
Se vogliamo far inserire i dati per caricare una matrice da tastiera possiamo, per esempio, utilizzare le seguenti istruzioni (valide per una matrice numerica di dimensione n x m):
cout << "inserisci la matrice una riga per volta separando i”
“numeri con spazi" << endl;
for (int i=0; i<n; i++) {
cout << "Riga " << i<< '\t';
for (int j=0; j<m; j++) cin >> a[i][j] ;
}
Si possono creare anche vettori a 3 o più dimensioni definendoli utilizzando tre o più volte le
parentesi quadre, per esempio supponiamo di voler memorizzare le temperature rilevate in
una certa località ogni ora del giorno, per ogni giorno del mese, in ogni mese dell’anno:
float temp[12][31][24];
definisce un vettore a tre dimensioni costituito da 12 matrici ognuna delle quali ha 31 righe e
24 colonne. Una matrice per ogni mese dell’anno. In memoria verranno create 12 x 31 x 24 =
8928 variabili di tipo float, alla prima ci si riferisce con temp[0][0][0], alla seconda con
temp[0][0][1] e così via fino alla 24° temp[0][0][23], mentre la 25° si ottiene con
temp[0][1][0], l’ultima avrà indici temp[11][30][23]. La temperatura rilevata il 15 aprile alle
ore 13 sarà memorizzata nell’elemento temp[4-1][15-1][13] ovvero temp[3][14][13]
Problema: Scrivere un programma per caricare due matrici n x n, calcolarne il prodotto righe
per colonne e visualizzare la matrice risultato.
Una volta scritti i sottoprogrammi per leggere una matrice (leggi), visualizzare una matrice
(scrivi) e per fare il prodotto (prodotto), il programma principale si riduce a richiamare il sottoprogramma leggi due volte per far leggere le due matrici, richiamare il sottoprogramma
prodotto e quindi richiamare il sottoprogramma scrivi per visualizzare la matrice prodotto.
#include <iostream.h>
#include <stdlib.h>
/* Programma prodotto. Legge due matrici nxn, fa il prodotto e visualizza
il risultato
n = dimensione di tutte le matrici
mat1[][] = prima matrice
mat2[][] = seconda matrice
ris[][] = matrice prodotto
*/
void leggi(float a[][10], int n){
cout << "inserisci la matrice una riga per volta separando “
109
Corso sul Linguaggio C++ Edizione 2010
“i numeri con spazi" << endl;
for (int i=0; i<n; i++) {
cout << "Riga " << i<< '\t';
for (int j=0; j<n; j++) cin >> a[i][j] ;
}
}
void scrivi(float a[][10], int n) {
for (int i=0; i<n; i++)
{for (int j=0; j<n; j++) cout << a[i][j] << '\t';
cout << endl; }
}
void prodotto(float a[][10], float b[][10], float c[][10], int n){
// calcola il prodotto righe per colonne
int i,j,k; float s;
for (i=0; i<n; i++)
for (j=0; j<n; j++) {
s=0;
for (k=0; k<n; k++) s+= a[i][k]* b[k][j];
c[i][j]=s;
}
}
void main(){
int n=20;
float mat1[10][10], mat2[10][10], ris[10][10];
while (n>10) {
cout << "Inserisci la dimensione delle matrici (max 10): "; cin >> n;
}
cout << endl <<"Inserisci la prima matrice." << endl; leggi(mat1,n);
cout << endl <<"Inserisci la seconda matrice." << endl; leggi(mat2,n);
prodotto(mat1, mat2, ris, n);
cout << endl << "La matrice prodotto e':" << endl; scrivi(ris,n);
system("PAUSE");
}
da notare che quando si passa una matrice come parametro ad un sottoprogramma la prima
dimensione può essere omessa ma non la seconda perché una matrice è un vettore di vettori e
quindi gli elementi del primo vettore sono altri vettori di cui il sistema deve conoscere la dimensione per poterne calcolare la grandezza.
6.4
Allocazione dinamica della memoria
Tutte le variabili definite nei programmi precedenti con le istruzioni di dichiarazione che abbiamo visto nel paragrafo 2.2, una volta create, hanno un loro ciclo di vita e non possono essere eliminate dalla memoria in anticipo. Le variabili globali e quelle locali definite con static
restano in memoria fino alla fine del programma (nel segmento dati) mentre quelle locali
normali vengono create nel segmento stack e cancellate quando termina l’esecuzione delle istruzioni del blocco nel quale sono state definite (vedi paragrafo 4.5). Inoltre il programmatore deve deciderne il numero e assegnargli un nome in fase di progettazione del programma.
Potrebbe essere utile permettere ad un programma di creare nuove variabili durante
l’esecuzione, in base alle esigenze determinate dall’input dell’utente, senza averle dichiarate
precedentemente. Inoltre, dato che lo spazio di memoria assegnato dal sistema operativo ad un
programma in esecuzione è limitato, potrebbe essere utile poter eliminare variabili che non
servono più durante l’esecuzione del programma per far spazio ad altre. Questo è un problema
110
Corso sul Linguaggio C++ Edizione 2010
soprattutto per i programmi che devono elaborare grandi quantità di informazioni e che hanno
bisogno di molta memoria.
Per esempio abbiamo visto che per dichiarare i vettori è necessario indicarne la dimensione
utilizzando una costante e questo ci costringe a dichiararli con un numero di elementi in genere superiore a quello che verrà effettivamente utilizzato nella maggior parte dei casi. Per esempio se sto scrivendo un programma per elaborare i voti degli alunni di una scuola qualsiasi
e mi serve un vettore per memorizzare i voti, devo dimensionare il vettore in modo che sia
sufficiente per contenere i voti della scuola che ha il maggior numero possibile di alunni e,
quindi, in genere, quando il programma verrà effettivamente impiegato, buona parte delle caselle del vettore resteranno inutilizzate.
Con il linguaggio C++ è possibile creare una variabile durante l’esecuzione e cancellarla
quando non serve più. Questo modo di gestire la memoria libera si dice gestione dinamica.
Le variabili create con questo sistema vengono inserite in uno spazio di memoria particolare
detto heap distinto sia dal segmento dati, dove vanno le variabili globale e locali statiche, che
dal segmento stack dove vanno le varibili locali normali.
Il linguaggio C++ mette a disposizione per queste operazioni gli operatori new e delete. Per
creare una nuova variabile di qualsiasi tipo (anche vettori, struct, ecc.) si usa la sintassi:
•
•
•
<nome puntatore> = new <tipo dato> [[<numero elementi>]] [(<valore iniziale>)]
<tipo dato> é il tipo dell'oggetto (o degli oggetti) da creare;
<numero elementi> é il numero degli oggetti, che vengono sistemati nella memoria heap
consecutivamente (come gli elementi di un array); se questo operando viene omesso, sarà
costruito un solo oggetto; se é presente, l'indirizzo restituito da new punta al primo oggetto;
<valore iniziale> é il valore con cui l'area allocata viene inizializzata (deve essere dello
stesso tipo di <tipo>); se é omesso l'area non é inizializzata.
Questa istruzione permette di creare sia variabili elementari che vettori e di assegnargli anche
dei valori iniziali (le parti tra parentesi quadre sono come al solito facoltative).
Per esempio se in un programma mi serve un vettore di n elementi dove n è una variabile del
problema, posso crearmelo esattamente di n elementi:
int n, i, *p=NULL;
cout << "Quanti elementi: "; cin >> n;
p= new int[n];
cout << "Inserisci i numeri separati da spazi:\n";
for (i=0; i<n; i++) cin >> p[i];
L’operatore new riserva in memoria uno spazio sufficiente a creare la struttura di dati e assegna l’indirizzo iniziale dell’area alla variabile puntatore.
Potrebbe capitare che non sia possibile assegnare la memoria necessaria per creare la struttura
di dati richiesta, in questo caso alla variabile puntatore dovrebbe essere assegnato il valore
NULL (corrispondente a ‘\0’). Purtroppo il DEV C++ in questo caso interrompe semplicemente l’esecuzione senza segnalare nessun errore.
Successivamente è possibile eliminare le variabili create con new utilizzando l’operatore delete con la sintassi:
delete <nume puntatore>
oppure
delete [] <nome puntatore>
111
Corso sul Linguaggio C++ Edizione 2010
se si tratta di un vettore. Per esempio per eliminare il vettore creato nell’esempio precedente
basta scrivere:
delete [] p;
L’operatore delete rende disponibile lo spazio precedentemente assegnato alla variabile puntata dal puntatore ma non elimina il puntatore dalla memoria. Lo stesso puntatore potrà essere
ancora utilizzato per puntare ad altre variabili eventualmente create con un’altra new.
Le variabili inserite nella memoria heap possono essere utilizzate solo attraverso i puntatori.
L’operatore delete è l’unico modo per deallocare una variabile nella memoria heap. Se si
cancella il valore contenuto nel puntatore, per esempio con un’istruzione del tipo
p=NULL;
oppure gli si assegna un altro valore prima di utilizzare delete per rilasciare lo spazio occupato
dalle variabili puntate, queste ultime non potranno più essere cancellate e continueranno ad
occupare spazio in memoria, pur non essendo più accessibili, fino a quando il programma
termina l’esecuzione.
Problema: Gestione di una pila.
La pila o stak è una struttura dinamica di dati dove è possibile fare solo due operazioni : inserimento di nuovi dati (operazione push) ed estrazione di dati (operazione pop). I dati inseriti vengono immagazzinati con la tecnica LIFO (Last In First Out) ovvero, quando si estrae
un dato viene estratto l’ultimo inserito. In pratica, quando inseriamo i dati è come se li mettessimo uno sopra l’altro formando appunto una pila, quando poi li andiamo a riprendere partiamo da quello in cima alla pila che è l’ultimo inserito. Questa è una struttura dinamica perché
si espande man mano che aggiungiamo dati e si riduce man mano che li estraiamo. Per simulare una pila si deve utilizzare della memoria per inserire i dati contenuti nella pila, possiamo
utilizzare un vettore oppure utilizzare la memoria heap per creare e cancellare dinamicamente
le variabili durante il funzionamento del programma.
Il programma seguente mostra come si può gestire una pila di numeri interi utilizzando la
memoria heap:
// Programma pila. Simula la gestione di una pila di numeri interi
struct elem {// elem=un qualsiasi elemento inserito nella pila
int dato;
elem *next;
};
elem *pila=NULL; // puntatore al primo elemento della pila
int pop(){ // funzione che preleva un elemento dalla pila
if (pila == NULL) return 0; else {
int ris =pila->dato; elem *tmp=pila->next;
delete pila; pila = tmp; return ris;
}
}
void push(int num){ // sottoprogramma per inserire un numero nella pila
elem *tmp= new elem; tmp->next=pila; tmp->dato = num; pila=tmp;
}
void main(){
char comando; int num;
cout << "Gestione Pila. I per inserire, E per estrarre, F per fine:\n" ;
do {
cout << "Comando: "; cin >> comando;
// trasforma in maiuscolo il comando
112
Corso sul Linguaggio C++ Edizione 2010
if (comando >='a' && comando <= 'z') comando -=32;
switch (comando) {
case 'I': cout << "Inserisci il numero: "; cin >>num; push(num); break;
case 'E': if (pila == NULL) cout << "Pila vuota\n" ;
else cout << "Elemento estratto= " << pop() << endl; break;
case 'F': break;
default: cout << "Sono ammessi solo I o E o F.\n";
}
}
while (comando != 'F');
system("PAUSE");
}
6.5
Assegnare pseudonimi ai tipi di dati
Una delle caratteristiche del linguaggio C è la possibilità di creare tipi di dati complessi la cui
definizione può essere anche molto lunga. Per scrivere meno si può utilizzare la parola chiave
typedef che permette di assegnare un nome scelto da noi per qualsiasi tipo di dati. La sintassi
è:
typedef
Dichiarazione di tipo
Nome alternativo
Per esempio spesso nei programmi troviamo dichiarazioni del tipo:
typedef unsigned long int ulong;
viene assegnato lo pseudonimo ulong al tipo unsigned lond int. Questo permette successivamente nel programma di utilizzare il nuovo nome di tipo ulong al posto di unsigned long int in
tutte le dichiarazioni successive, per esempio:
ulong a,b,c;
Un altro utilizzo comune è quello per definire i puntatori. Se per esempio si scrive:
typedef double* pdouble;
nel seguito del programma si potranno definire variabili puntatori a double usando il nome
pdouble :
pdouble punt1,punt2;
invece di scrivere:
double *punt1, *punt2;
6.6
Esercizi
117) La variabile dichiarata come char d[N][M]; è...
a) ...un vettore di N stringhe di lunghezza M-1
b) ...un vettore di M stringhe di lunghezza N
c) ...una matrice NxM di stringhe
d) ...una matrice NxM di stringhe lunghe un carattere ciascuna
Esercizio 117) Risposta = a)
118) Data la seguente funzione che inizializza i valori di un array bidimensionale "matrice":
#define N 5
void inizializza( ){
113
Corso sul Linguaggio C++ Edizione 2010
int matrice[N][N];
int riga, colonna;
for ( riga = 0; riga < N; riga++ ) {
for ( colonna = 0; colonna < N; colonna++ ) {
if ( riga == colonna )
matrice[riga][colonna] = 1;
else if ( riga + colonna == N - 1 )
matrice[riga][colonna] = 1;
else if ( riga < colonna )
matrice[riga][colonna] = 0;
else
matrice[riga][colonna] = matrice[colonna][riga];
}
}
for ( riga = 0; riga < N; riga++ ) {
for ( colonna = 0; colonna < N; colonna++ )
printf( "%d ", matrice[riga][colonna] );
printf( "\n" );
}
}
Indicare quale tra le seguenti configurazioni vengono stampate dalla procedura "inizializza".
Risposte:
a)
1 1
0 1
0 0
0 0
1 1
1
0
1
0
1
1
0
0
1
1
1
0
0
0
1
114
Corso sul Linguaggio C++ Edizione 2010
b)
1 0
0 1
0 0
0 1
1 0
0
0
1
0
0
0
1
0
1
0
1
0
0
0
1
115
Corso sul Linguaggio C++ Edizione 2010
c)
1 0
0 1
0 0
0 1
1 0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
116
Corso sul Linguaggio C++ Edizione 2010
d)
1 0
0 1
0 0
0 0
0 0
0
0
1
0
0
0
0
0
1
0
0
0
0
0
1
Esercizio 118) Risposta = b)
Programmi con le matrici
119) Far assegnare in modo casuale i valori in una matrice di dimensione N x M (N e M dati in input) e visualizzarla.
120) Creare una matrice unitaria di ordine N.
121) Far visualizzare l'elemento più piccolo e quello più grande di una matrice.
122) Scambiare di posto gli elementi di una matrice per ottenere la matrice trasposta.
123) Descrivere un algoritmo per verificare se una matrice quadrata é simmetrica.
124) Descrivere un algoritmo per vedere se in una matrice c'é una riga contenente tutti 1.
125) Un quadrato magico é una matrice quadrata con la seguente proprietà: la somma degli
elementi di ciascuna riga, colonna e diagonale dà sempre lo stesso numero. Per esempio:
4
3
8
9
5
1
2
7
6
Quadrato magico d'ordine3
(la somma é sempre 15)
11
24 7
29 3
4
12 25 8
16
17
5
13 21 9
10
18 1
14 22
23
6
19
2
15
Quadrato magico d'ordine 5
(la somma é sempre 65)
Un algoritmo per ottenere un quadrato magico di ordine dispari é il
seguente:
- si parte mettendo 1 nella casella sotto il centro della matrice;
- i numeri seguenti (2, 3, 4 .... n*n) sono inseriti ogni volta nella casella intersezione della
riga e colonna successiva (riga sotto e colonna a destra, con la convenzione che la successiva dell'ultima riga o colonna é la prima);
- se una casella é stata già riempita si inserisce il numero nella colonna precedente una riga
più sotto (si assume che la colonna precedente la prima sia l'ultima).
Scrivere un programma che assegnato un numero N dispari qualsiasi visualizzi il quadrato magico
di ordine N e il numero magico somma di tutte le righe, colonne e diagonali (N*(N2+1)/2).
117
Corso sul Linguaggio C++ Edizione 2010
126) Creare una matrice di ordine NxM nella memoria heap, caricarla con numeri a caso e
visualizzarla.
127) Creare e visualizzare una matrice triangolare superiore con numeri a caso (una matrice
triangolare superiore è quella dove tutti i numeri sotto la diagonale principale sono =
0)
128) Assegnata una matrice quadrata di ordine n qualsiasi far scambiare le righe in modo
che:
per ogni J>I sia
abs(A(I,I)) > abs(A(J,I))
cioè in modo che gli elementi della diagonale principale siano maggiori, in valore assoluto,
di tutti gli elementi che seguono nella stessa colonna. Per esempio partendo dalla matrice:
7 75
-10 5
3 -2
4 22
-6
20
-1
7
4
-10 5 20 -5
-5
7 75 -6 4
+2 si ottiene
4 22 7 -4
-4
3 -2 -1 +2
129) Far visualizzare il polinomio prodotto di due polinomi di grado m e n. (se si mettono i
coefficienti dei due polinomi in due vettori a[i] e b[i], e consideriamo la matrice
c[i][j]= a[i]*b[j] la somma degli elementi delle diagonali secondarie della matrice ci
darà i coefficienti del polinomio prodotto).
118
7
Capitolo
Corso sul Linguaggio C++ Edizione 2010
7. I file su disco
7.1
Input output in generale
La maggior parte dei programmi legge dati (operazione di input) e produce dei risultati che
devono essere visualizzati o scritti da qualche parte (output). Per fare queste operazioni il programma comunica con le unità di input e di output collegate al sistema di elaborazione.
Possiamo immaginare il trasferimento di dati, da e alle periferiche, come flussi di byte che
vanno e vengono, in inglese “flusso” si traduce con “stream”.
Nel linguaggio C, la gestione dei flussi di dati in input e output è demandata ad un gruppo di
funzioni presenti nella libreria standard e inclusi nel file di intestaizioni <stdio.h> (standard
input output), mentre nel linguaggio C++, oltre a <cstdio>, sono stati introdotti una serie di
oggetti, definiti nel file di intestazioni <iostream> (input output stream). Fra questi oggetti ci
sono anche cout e cin, proprio quelli che abbiamo utilizzato finora nei nostri programmi per
l’input e l’output. Nel linguaggio C le istruzioni corrispondenti a cin e cout (del C++) sono
scanf e printf definite in <stdio.h>.
I dati possono essere anche presi o inviati da o a un file presente su disco, oltre che da o a una
periferica. Nel linguaggio C++ i flussi di dati da e verso le memorie di massa vengono trattati
come quelli da e verso le periferiche.
Per flusso (stream) intendiamo il canale di collegamento tra il programma e l’oggetto che fornisce o riceve i dati. Per ragioni storiche, nel linguaggio C base, questi canali di collegamento
vengono chiamati FILE invece di stream (nel linguaggio C++ si chiamano correttamente
stream), normalmente il file è uno degli oggetti che può ricevere o fornire i bytes del flusso,
non il canale di collegamento.
Prima di effettuare qualsiasi operazione di input-output è necessario creare il flusso, ovvero il
collegamento programma <──> periferica o file, questa operazione viene definita apertura
del flusso. L’apertura del flusso mette in collegamento il programma con il particolare file o
periferica e crea il buffer (area di memoria dove vengono depositati temporaneamente i byte
presi o da inviare al file) e tutte le variabili di lavoro che servono per gestire questo collegamento, per esempio la variabile intera che indica la posizione nel file del prossimo byte che
verrà letto o scritto.
Quando il programma ha finito di utilizzare il flusso lo può chiudere per canellare tutte le variabili e liberare gli spazi di memoria utilizzati per la sua gestione.
Quando il programma parte, se abbiamo incluso stdio (#include <stdio.h>) o iostrem (#include <iostream.h>), vengono automaticamente creati tre flussi: stdin, stdout, e stderr; il primo
è sinonimo di standard input ed è collegato normalmente alla tastiera ma il sistema operativo
lo può collegare anche ad un file o ad un'altra periferica; il secondo sta per standard output ed
è collegato normalmente al video ma può essere collegato anch’esso ad un file o ad una altra
119
Corso sul Linguaggio C++ Edizione 2010
periferica; il terzo sta per standard error ed è collegato in genere al video. Alcune istruzioni
come cin, cout, scanf e printf, utilizzano automaticamente proprio questi flussi per l’input e
l’output. Quindi non è necessario aprire questi tre flussi perché sono già aperti quando il programma parte.
7.2
I file su disco
In questo paragrafo vedremo la gestione standard dei file su disco nel linguaggio C attraverso
le funzioni definite in <stdio.h>, quindi nelle dichiarazioni iniziali bisogna inserire:
#include <stdio.h>
Come abbiamo già detto queste funzioni sono disponibili sia nel linguaggio C base che nel
linguaggio C++ e sono quelle che servono per risolvere i problemi assegnati nelle gare delle
olimpiadi dell’informatica. Non verrà trattata in questo testo la gestione attraverso le classi istream, ostream e iostream presenti nella libreria del C++ (accessibili attraverso <iostream>).
Se vogliamo leggere o scrivere da un file su disco bisogna prima di tutto creare una variabile
puntatore ad una struttura particolare definita in <stdio.h> e chiamata FILE, per esempio:
FILE *fleggi, *fscrivi;
crea due puntatori che possono essere utilizzati per collegare il programma a due file diversi.
Prima di utilizzare il flusso è necessario aprirlo e assegnare il valore al puntatore precedentemente definito, questo si fa con la funzione fopen(). Per esempio se vogliamo creare un flusso
che collega il programma al file “prova1.txt” per l’input dei dati e un altro flusso per collegarlo al file “prova2.txt” per l’output è necessario utilizzare le seguenti istruzioni:
fleggi = fopen(“prova1.txt”,”r”);
fscrivi = fopen(“prova2.txt,”w”);
La funzione fopen crea il flusso e restituisce un puntatore alla struttura di tipo FILE che serve
per gestire il flusso. Richiede due parametri di tipo stringa:
- il primo deve contenere il nome, ed eventualmente il percorso, per individuare il file nel
disco;
- il secondo indica il tipo di flusso che verrà creato, è possibile scegliere fra i seguenti valori:
“r”
il flusso serve solo per leggere i dati dal file, se il file non esiste viene segnalato un
errore; la direzione dei bytes è solo quella dal file al programma;
“w”
il flusso serve solo per scrivere i dati nel file. Se il file esiste viene cancellato, se non
esiste viene creato vuoto; la direzione dei bytes è solo quella dal programma al file;
“a”
il flusso può essere utilizzato solo per aggiungere nuovi dati nel file. Se il file esiste i
dati precedenti in esso contenuti non verranno cambiati, i nuovi dati vengono aggiunti alla fine del file, se non esiste viene creato;
“r+”
si può sia leggere che scrivere nel file, la posizione iniziale da cui si legge o in cui si
scrive è quella del primo byte del file, il contenuto del file non viene modificato;
“w+” si può sia leggere che scrivere nel file, se il file esiste viene completamente cancellato;
“a+”
si può sia leggere che scrivere nel file, se il file esiste non viene cancellato e la posizione iniziale è impostata dopo l’ultimo byte contenuto nel file;
Al secondo parametro (il tipo di apertura) si possono aggiungere altre lettere, per esempio la
lettera ‘b’ per dire che il flusso è binario, per esempio “rb”(solo lettura binario) oppure “wb”.
Normalmente i flussi vengono considerati nel linguaggio C come flussi di testo costituiti da
120
Corso sul Linguaggio C++ Edizione 2010
una serie di righe separate dal carattere ‘\n’, a meno che non si inserisca la lettera ‘b’ nel secondo parametro, nel qual caso diventano flussi binari costituiti da una serie di byte qualsiasi.
Per leggere e scrivere in un file su disco si possono utilizzare le funzioni riportate nella Tabella 3 a pagina 63, precisamente per leggere o scrivere un byte alla volta abbiamo le funzioni:
- int fgetc (FILE *stream) legge il prossimo carattere dal flusso fornito come parametro, il
risultato è un numero intero da 0 a 255, se si verifica un errore o se il file è finito viene restituita la costante EOF che normalmente è = -1;
- int fputc (int c, FILE *stream) converte c in un carattere e lo scrive nel file alla posizione
corrente; il risultato è il carattere c oppure EOF se si verifica un errore;
per leggere o scrivere un stringa di caratteri nel file:
- int fputs (const char *s, FILE *stream) scrive la stringa s nel file senza aggiungere il carattere di terminazione o il carattere di nuova riga, vengono scritti solo i caratteri contenuti
nella stringa, per esempio:
fputs ("Ciao. ", stdout);
fputs ("Sei ", stdout);
fputs ("stanco?\n", stdout);
scrive su video la frase
Ciao. Sei stanco?
e va a capo.
- char * fgets (char *s, int count, FILE *stream) legge dal file esattamente count-1 caratteri oppure fino alla fine della riga incluso il carattere ‘\n’ se la linea contiene meno di count1 caratteri; inserisce i caratterei letti nella stringa s fornita come primo parametro; la stringa
deve poter contenere almeno count caratteri perché alla fine viene inserito il carattere di
terminazione ‘\0’; se è stata raggiunta la fine del file viene restitutito NULL altrimenti viene
restituito un puntatore alla stringa s; se il file contiene un carattere ‘\0’ il risultato è imprevedibile;
lettura e scrittura di numeri (input e output formattato):
- int fprintf (FILE *stream, const char *schema, ...) funziona come printf (vedi pag.88)
ma scrive nel flusso passato come primo parametro;
Per esempio ricordando che %d si usa per scrivere numeri interi, %f per numeri con la virgola
e %s per scrivere stringhe:
fprintf(out,
fprintf(out,
fprintf(out,
dec
fprintf(out,
// copia nel
scluso
fprintf(out,
“%d\n”, num); // scrivere num nel file “out” e va a capo
“%d %d”, a, b); // scrive a e b separati da 1 spazio
“%10.3f”, a); // scrive a utilizzano 10 spazi con 3 cifre
“%c/n”, c); //scrive il carattere c e va a capo
file la stringa frase fino al carattere di terminazione e“%s”, frase);
- int fscanf (FILE *stream, const char *template, ...) funziona coma scanf (vedi pag.89)
ma legge dal flusso passato come primo parametro;
Per esempio:
121
Corso sul Linguaggio C++ Edizione 2010
fscanf(in,"%d\n",&n); // legge un numero seguito da un a capo nella variab. n
// legge due numeri separati da uno spazio nelle variabili a e b
fscanf(in,”%d %d”, &a, &b);
legge una serie di caratteri fino ad uno spazio o fino alla fine della riga e li mette nella stringa
str che deve essere abbastanza capiente per contenerli:
fscanf(in, “%s”, str);
Quando si legge e scrive in un file il sistema memorizza in una variabile interna di tipo long
int la posizione corrente nel file. Ogni volta che si legge o scrive questa variabile viene automaticamente aggiornata. E’ possibile cambiare la posizione di lettura o scrittura nel file (accesso random) utilizzando la funzione fseek():
- int fseek (FILE *stream, int spostamento, int tipo) cambia la posizione corrente nel flusso fornito come primo parametro. La nuova posizione dipende da spostamento che dice di
quanti byte bisogna spostarsi e da tipo. Il valore di quest’ultimo parametro deve essere una
delle costanti SEEK_SET, SEEK_CUR, SEEK_END per indicare se lo spostamento è relativo all’inizio del file, alla posizione corrente nel file o alla fine del file.
- long int ftell(FILE *stream) restituisce l’attuale posizione all’interno del file (se c’è un errore viene restituito -1);
- size_t fread (void *data, size_t size, size_t count, FILE *stream) questa funzione legge
count oggetti tutti di dimensione size dal file e li inserisce nel buffer puntato da data. Serve
per leggere dati di qualsiasi tipo da un file binario. Si può utilizzare per leggere i file di record.
- size_t fwrite (const void *data, size_t size, size_t count, FILE *stream) serve per scrivere
count oggetti di dimensione size nel file prendendoli dal buffer data. Permette l’accesso diretto ai file binari per la scrittura.
Attenzione! Quando si esegue il debug di un programma che utilizza i file su disco, con il
Dev C versione 4, bisogna sapere che la directory corrente durante il debug è la cartella bin
contenuta nella cartella di installazione del Dev C e, quindi, se non si utilizzano i percorsi assoluti, bisogna mettere una copia dei file in input in questa cartella altrimenti verrà generato
un errore dopo l’apertura del file.
Esempio 1 copiare il contenuto di un file in un altro file.
Supponiamo di voler copiare un file qualsiasi in un altro con nome “prova.txt”. Il file iniziale
deve esistere prima di avviare il programma. Possiamo crearlo nella stessa cartella del programma con Blocco note inserendoci frasi a caso.
Il programma completo potrebbe essere il seguente:
#include <stdio.h>
#include <stdlib.h>
/* programma "copiafile"
copia un file con nome qualsiasi dato in input
in un altro di nome "prova.txt" */
int main(){
FILE *fleggi, *fscrivi;
char c, nomef[40];
// chiede il nome del file da copiare
printf("Inserisci il nome del file da copiare:"); gets(nomef);
fleggi = fopen(nomef,"r"); // apre il file
if (fleggi == NULL)
122
Corso sul Linguaggio C++ Edizione 2010
// scrive un messaggio di errore se non riesce ad aprire il file
printf("Errore! impossibile aprire il file!\n");
else {
fscrivi = fopen("prova.txt","w"); // apre la copia
while ((c=fgetc(fleggi)) != EOF) fputc(c,fscrivi);
printf("Il file e' stato copiato!\n");
}
system("PAUSE");
return 0;
}
Dopo l’esecuzione dovrebbe apparire, nella cartella dove si trova il programma, il file “prova.txt” contenente una copia esatta del file il cui nome è stato inserito in input.
I nomi dei file passati come parametri alla funzione fopen possono contenere un percorso assoluto o relativo. Se il nome comincia con la lettera del drive o con “\” allora si tratta di percorso assoluto altrimenti è un percorso relativo alla cartella corrente che è quella che contiene
il programma, per esempio:
“c:\temp\prova.txt”
percorso assoluto – drive c, sottocartella temp, cerca il file prova.txt
“\prog\cpp\prova.txt” percorso assoluto nel drive corrente cerca il file nella cartella
\prog\cpp
“prova.txt”
percorso relativo, in questo caso non si sposta dalla cartella del programma
“tmp\prova.txt “
va nella sottocartella tmp della cartella corrente e lì cerca prova.txt
Attenzione, se si esegue il programma utilizzando il debug la cartella corrente non è più quella che contiene l’eseguibile ma è la cartella bin del percorso di installazione del Dec C++!
Esempio2 – Stringhe strane. Problema della selezione regionale 2002.
Dato un insieme di stringhe di lunghezza 6 formate dai caratteri ‘A’, ‘B’, ‘C’ o ‘D’ bisogna distinguere le
stringhe “buone” da quelle “cattive”. Una stringa è buona se soddisfa tutti i seguenti requisiti:
1. Non contiene una sequenza di 3 o piu’ caratteri che sono uguali (ad esempio le stringhe contenenti AAA o
BBBB sono “cattive”)
2. Non contiene una sequenza di 3 o piu’ caratteri consecutivi che sono in ordine crescente ( ad esempio le
stringhe che contengono ABC o ACD sono “cattive”)
Se una stringa non rispetta anche uno solo dei due requisiti precedenti è “cattiva”.
File di Input
Il file di input di nome input.txt ha il seguente formato:
• la prima riga contiene un numero intero N, che rappresenta il numero di stringhe;
• le successive N righe contengono ciascuna una stringa di lunghezza 6 formata dai caratteri ‘A’, ‘B’, ‘C’ o
‘D’. Il primo carattere della riga contiene il primo carattere della stringa e i caratteri della stringa sono consecutivi (cioè non sono separati da spazi).
File di Output
Il file di output di nome output.txt è costituito da N righe, una riga per ciascuna stringa data in input; l’ultima
riga termina con un a-capo. Ogni riga è costituita esattamente da 1 carattere In particolare, l’i-esimo carattere è
• ‘C’ se la stringa i-esima è “cattiva”
• ‘B’ se la stringa i-esima è “buona”
Assunzioni
• Il file di input non contiene altri caratteri oltre a quelli indicati nel testo.
• Il file di output non deve contenere altri caratteri oltre a quelli contenuti nel testo; in particolare non ci devono essere linee di separazione fra le linee di output.
• Il programma non deve produrre alcun altro input/output oltre a quelli indicati: deve limitarsi a leggere il file
di input e a scrivere i risultati sul file di output.
123
Corso sul Linguaggio C++ Edizione 2010
Esempio 1
File di input File di output
8
AABBCC
ABABAC
AAABBD
AABBBB
ABBABC
DCBDAB
ADDBBA
AABAAB
B
B
C
C
C
B
B
B
Soluzione
Con un ciclo leggiamo tutte le stringhe nel file i input e per ognuna di esse controlliamo tutti i
caratteri a partire dal secondo al sesto nel seguente modo:
1) prima di iniziare il controllo supponiamo che la stringa sia “buona” e mettiamo ‘B’ in ris;
inizializziamo anche il contatore dei caratteri uguali con 1 (cu = 1) e quello dei caratteri
crescenti con1 (scc=1);
2) cominciamo il ciclo per controllare i caratteri dal secondo al sesto
3) se il carattere è uguale al precedente incrementiamo il contatore dei caratteri uguali
(cu++) altrimenti mettiamo 1 in questo contatore.
4) se il carattere è > del precedente incrementiamo il contatore della sequenza caratteri crescenti (scc++) altrimenti mettiamo 1 in questo contatore;
5) se cu>2 o scc >2 allora la stringa è cattiva e possiamo terminare il controllo dei caratteri;
6) passiamo al carattere successivo.
7) dopo aver terminato il controllo scriviamo ris nel file di output
Il programma completo è il seguente:
#include <stdio.h>
// programma stringhe.cpp - Assegnato alla selezione regionale 2002
int main(){
int cu, scc, n, i, j;
char str[10], ris;
FILE *in, *out;
in = fopen("input.txt","r"); out = fopen("output.txt", "w");
fscanf(in,"%d\n",&n);
for (i=0; i<n; i++) {
fscanf(in,"%s",str); // legge una nuova stringa
/* si poteva usare anche fgets() per leggere la stringa
fgets(str, 10, in);
in questo caso vengono letti anche gli spazi fin a massimo
9 caratteri compreso il carattere ‘\n’ (con fscanf()
la lettura si ferma al primo spazio oppure al carattere ‘\n’) */
ris = 'B'; cu = 1; scc=1;
for (j = 1; j < 7; j++) {
if (str[j] == str[j-1]) cu++; else cu=1;
if (str[j] > str[j-1]) scc++; else scc=1;
if (cu>2 || scc >2) {ris = 'C'; break;}
}
fprintf(out,"%c\n",ris);
}
fclose(in); fclose(out);
124
Corso sul Linguaggio C++ Edizione 2010
return 0;
}
125
8
Capitolo
Corso sul Linguaggio C++ Edizione 2010
8. Raccolta test delle olimpiadi informatica (solo test linguaggio C)
Le olimpiadi internazionali di informatica sono una gara patrocinata dall'UNESCO a cui partecipano circa 70 nazioni in tutto il mondo. In Italia, il ministero dell'istruzione, ha affidato
all'AICA il compito di gestire e organizzare la partecipazione del nostro paese. Alla gara possono partecipare tutti i giovani con le seguenti caratteristiche:
• essere iscritto ad uno dei primi quattro anni della scuola superiore;
• non aver ancora compiuto 18 anni oppure averli compiuti dopo il 30 giugno dell’anno
in corso;
• essere disponibile, qualora superi le prime prove di selezione, a frequentare i corsi di
formazione che si terranno prima della competizione internazionale;
• essere disponibile, qualora superi l'ultima selezione, a recarsi nella nazione scelta per
ospitare la competizione finale nel periodo stabilito per detta gara.
Ogni anno, più o meno a metà novembre, le scuole che partecipano operano una prima selezione somministrando un test preparato a livello nazionale dall’AICA, costituito per metà da
domande di logica e per l’altra metà da domande su un linguaggio a scelta tra il Pascal e il
C++. I due alunni che, in ogni scuola, ottengono il punteggio più alto in questa prova (il secondo solo se il suo punteggio è superiore alla media nazionale) partecipano alla selezione regionale che si svolge circa ad aprile dell’anno successivo. La prova della selezione regionale
consiste nel risolvere al computer una serie di problemi di programmazione utilizzando sempre uno dei due linguaggi di programmazione ammessi: il C++ o il Pascal.
I migliori in ogni regione più i migliori a livello nazionale nella prova regionale faranno parte
del gruppo di 60 alunni che partecipano alla selezione nazionale. In base a quest’ultima prova
vengono scelti i probabili olimpici che parteciperanno alla prova internazionale.
Gli alunni che hanno seguito fin quì questo corso dovrebbero aver raggiunto un livello di conoscenza del linguaggio C++ sufficiente per rispondere correttamente ai test della selezione
scolastica delle olimpiadi dell’informatica. Si tratta di un livello elevato e quindi per raggiungerlo non è sufficiente aver ascoltato le lezioni con attenzione, è necessario anche passione e
un lavoro autonomo di sperimentazione a casa.
Ho riportato in questo capitolo una piccola raccolta dei test di programmazione per il linguaggio C++, somministrati nelle selezioni scolastiche delle ultime olimpiadi italiane
dell’informatica e alcuni problemi assegnati nelle selezioni regionali. Mi aspetto che tutti provino a risolvere almeno quelli delle selezioni scolastiche e, nel caso non ci riescano, vadano a
126
Corso sul Linguaggio C++ Edizione 2010
ristudiarsi i capitoli del libro relativi alle nozioni dimenticate. Il test finale del corso verrà
preparato sulla falsariga di questi test. L’elenco completo dei test somministrati alle selezioni
scolastiche e regionali delle olimpiadi dell’informatica italiane si può trovare su internet
all’indirizzo: http://www.olimpiadi-informatica.it/.
8.1
Selezione scolastica
I test completi comprendono alcune domande di logica e altre di programmazione, per aumentare la selezione viene concesso un tempo di 60 minuti che è in genere insufficiente per rispondere a tutte le domande. In questo libro ho inserito solo le domande di programmazione
relative al linguaggio C++ per risolvere le quali il tempo concesso è di circa 30 minuti.
Anno 2008 (test somministrato il 23/11/2007)
Domanda N°1 La risposta esatta vale 3 punti.
Si consideri la seguente funzione
int ES1( int a, int b ) {
int j; int k = 1; int p = 0;
while ( k <= a ) {k++; j = 0; while ( j < b ) {j++; p = p+j;} }
return 2* p / b;
}
Dire cosa calcola la funzione nell'ipotesi che a e b siano sempre positivi e che il programma non generi mai un
“overflow” durante le operazioni aritmetiche.
Risposte:
a) a*b
b)a*(b+1)
c)(a+1)*(b-1)
d) nessuna delle precedenti
Domanda N°2 La risposta esatta vale 3 punti.
Si consideri il seguente programma:
#include <stdio.h>
#define DMAX 5
void ES2(int M[][DMAX], int R, int C){
int I, K; int V[DMAX];
for( I = 0; I < R; I++) V[I] = M[I][0];
for( I = 0; I < C-1; I++)
for( K = 0; K < R; K++) M[K][I] = M[K][I+1];
for( I = 0; I < R; I++) M[I][C-1] = V[I];
}
int main (){
int M[DMAX][DMAX]; int I;
M[0][0] = 1; M[0][1] = 2; M[0][2] = 3; M[0][3] = 4; M[0][4] = 5;
M[1][0] = 1; M[1][1] = 2; M[1][2] = 3; M[1][3] = 4; M[1][4] = 5;
M[2][0] = 1; M[2][1] = 2; M[2][2] = 3; M[2][3] = 4; M[2][4] = 5;
M[3][0] = 1; M[3][1] = 2; M[3][2] = 3; M[3][3] = 4; M[3][4] = 5;
M[4][0] = 1; M[4][1] = 2; M[4][2] = 3; M[4][3] = 4; M[4][4] = 5;
for ( I = 0; I < 3; I++) ES2( M, 5, 5 );
return 0;
}
La matrice M inizialmente contiene tutti 1 nella prima colonna, tutti 2 nella seconda colonna e così via. Indicare
il contenuto della matrice Mal termine del programma.
Risposte:
a)
4 5 1 2 3
4 5 1 2 3
4 5 1 2 3
4 5 1 2 3
4 5 1 2 3
127
Corso sul Linguaggio C++ Edizione 2010
b)
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
c)
5 4 3 2 1
5 4 3 2 1
5 4 3 2 1
5 4 3 2 1
5 4 3 2 1
d)
1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4
5 5 5 5 5
Domanda N°3 La risposta esatta vale 1 punto.
Si consideri la seguente funzione
int ES3( int x ){
if ( x <= 1 ) return 0; else return 1 + ES3( x/2 );
}
Dire cosa restituisce la chiamata ES3( 10 ).
Risposte:
a)1
b)2
c) 3
d) nessuna delle precedenti
Domanda N°4 La risposta esatta vale 2 punti.
Si consideri la seguente funzione
int ES4( int x ){
if (x/10 == 0) return x; else return x%10 + ES4( x/10 );
}
Dire cosa restituisce l'invocazione ES4( ES4( 731 ) ).
Risposte:
a) 2
b) 9
c) 36
d) nessuna delle precedenti
Domanda N°5 La risposta esatta vale 2 punti.
Si consideri la procedura
void ES5( int n, int d ){
while ( n != 1 )
if( n % d == 0 ){printf( "%d ", d );n = n/d;} else ++d;
}
Dire cosa stampa su schermo la chiamata ES5( 210, 2 ).
Risposte:
a) 2
b) 2 3 4 5 6 7
c) 2 3 5 7 8
d)nessuna delle precedenti
Domanda N°6 La risposta esatta vale 2 punti.
Si consideri la funzione
int ES6( int x, int y ){
if ( x == 0 && y == 0 ) return 0;
else if ( x%10 < y%10 ) return ES6( x, y/10 );
else if ( x%10 > y%10 ) return ES6( x/10, y );
else return x%10 + 10 * ES6( x/10, y/10 );
}
Dire cosa restituisce la chiamata ES6( 3467, 5678 ).
Risposte:
a) 18
b) 67
c) 2367
d) nessuna delle precedenti
Domanda N°7 La risposta esatta vale 3 punti.
Si considerino le seguenti procedure
void mf( int n, int k, int t[], int e){
128
Corso sul Linguaggio C++ Edizione 2010
int i;
if ( e < n-1 )
for ( i = t[e]+1; i <= k; i++ ){t[e+1] = i; mf( n, k, t, e+1 );}
}
void ES7( int n, int k ){
int t[10]; int i;
for( i = 0 ; i < n; i++ ){t[0] = i+1; mf( n, k, t, 0);}
for ( i = 0; i < n; i++ ) printf( "%d ", t[i] );
printf( "\n" );
}
Si assuma che sempre si verifichi n <= 10. Dire cosa stampa su
schermo la chiamata ES7( 3, 4 ).
Risposte:
a) 1 2 4
b) 2 3 4
c) 3 4 4
d) nessuna delle precedenti
Domanda N°8 La risposta esatta vale 2 punti.
Si consideri la funzione
int ES8( int m){
int a, b, c, g, t;
a = m * m; b = a/2; c = 4 * b * a; t = m; g = m + a + t + t + a;
while ( g < b + a + c + c + a ) {
g = g + a + t + t + a;
c = t + a + t + a;
}
return g;
}
Dire cosa restituisce la chiamata ES8( 2 ).
Risposte:
a) 19
b) 32
c) 38
d) nessuna delle precedenti
Domanda N°9 La risposta esatta vale 2 punti.
Si considerino le funzioni mutuamente ricorsive
int foo( int n );
int ES9( int n ){
if ( n%2 == 1 ) return foo( n - 3 ); else return n;
}
int foo(int n){
if ( n%2 == 0 ) return ES9( 2*n ); else return n;
}
Dire cosa restituisce la chiamata ES9( 51 ).
Risposte:
a) 48
b) 96
c) 102
d) nessuna delle precedenti
Anno 2007 (test somministrato il 16/11/2006)
1) La risposta esatta vale 1 punto.
Quale dei seguenti valori di a e b produce il valore vero per la condizione:
(a > 0) and ((b < 0) or (b > 1))
Risposte:
a) a = 5; b = 0
b) a = 5; b = 2
c) a = -1; b = 5
d) a = 1; b = 1
=======================================================================
2) La risposta esatta vale 2 punti.
Si consideri la seguente funzione:
#define N 5
int vett1[N];
int vett2[N];
void calcola() {
int temp, i;
for (i = 0; i < N; i++) vett1[i] = N - i - 1;
for (i = 0; i < N; i++) {temp = vett1[i]; vett2[temp] = i;}
}
129
Corso sul Linguaggio C++ Edizione 2010
Indicare il contenuto del vettore vett2 al termine dell’esecuzione della
funzione “calcola”.
Risposte:
a)[0,1,2,3,4]
b)[3,1,4,2,0] c)[4,3,2,1,0]
d)nessuna delle precedenti
=======================================================================
3) Ogni risposta esatta vale 2 punti; punteggio totale da -4 a 8 punti.
Considerate le seguenti quattro funzioni, con argomento N (un intero non
negativo). Ciascuna di esse calcola un qualche valore scelto tra:
N, 2N , N2, N3, 2N , N!
Dovete stabilire qual e il valore calcolato da ogni funzione (potrebbero
esserci doppioni: più funzioni potrebbero calcolare lo stesso valore; notate inoltre che N! denota il fattoriale di N ed e definito come N! = 1x2 x 3
…. x N; quindi 2!= 2, 3! = 6 e 5!= 120; inoltre si assume che 0! = 1).
int A(int N) {
if (N > 0) return 2 * A(N - 1) ; else return 1;
}
int B(int N) {
if (N > 0) return B(N - 1) + 2 * N - 1; else return 0;
}
int C(int N) {
int i, R; R = 1;
for (i = 0; i < N; i++) R = R + R;
return R;
}
int D(int N) {
int i, R; R = 1;
for (i = 1; i <= N; i++) R = R * i;
return R;
}
Risposte:
A: ………
B: ………
C: ………
D: ………
=======================================================================
4) La risposta esatta vale 3 punti.
Si consideri la seguente funzione:
int f(int n) {
if (n == 0) return 0;
if ( (n%2) == 0) return 2 * f(n/2); else
return 2 * f(n/2) + 2;
}
Indicare qual e’ il valore restituito dall’ invocazione f(f(2)).
Risposte:
a) 2
b) 4
c) 6
d) nessuna delle precedenti
=======================================================================
5) La risposta esatta vale 3 punti.
Cosa stampa il seguente programma, con input 5?
int main(){
float x,y,z; x= 0; y= 0;
scanf("%f", &z);
do { x = x + 1; y = y + 1/x;} while (!(y > z));
printf ("%f \n",x);
}
Risposte:
a)5
b)12
c)25
d)nessuna delle precedenti
=======================================================================
6) La risposta esatta vale 3 punti.
Cosa stampa il seguente programma?
#include <stdio.h>
#include <stdlib.h>
int calcola(int n) {
130
Corso sul Linguaggio C++ Edizione 2010
if(n == 1) {return 1;} else
if(n == 2) {return 3;}else
if(n==3) {return n + calcola(n-1);} else
{return n + calcola(n-1) + calcola(n-2);}
}
int main(){
printf("%d\n",calcola(6));
return 0;
}
Risposte:
a) 41
b) 45
c) 49
d) nessuna delle precedenti
=======================================================================
7) La risposta esatta vale 5 punti.
Cosa stampa il seguente programma?
#include <stdio.h>
#include <stdlib.h>
int mistero(int m, int n) {
if ( m == 0 ) return n; else
if ( n==0 ) return mistero( m-1, 1 ); else
return mistero( n-1,mistero( m-1, n-1 ) );
}
int main(){
printf( "%d %d %d %d\n", mistero(0,3), mistero(1,3),
mistero(2,3), mistero(3,3));
return 0;
}
Risposte:
a) 3 1 2 0
b) 3 1 1 0
c) 3 1 0 1
d) nessuna delle precedenti
Anno 2006 (test somministrato il 18/11/2005)
1) La risposta esatta vale 1 punto.
Dopo l'esecuzione della seguente porzione di codice:
#include <stdio.h>
void funzione(int *a,int b) {
int temp=*a;
*a=b;
b=temp;
};
main(){
int a=2;
int b=5;
funzione(&a,b);
}
Quanto valgono a e b?
Risposta aperta
2) La risposta esatta vale 1 punto.
Si consideri la seguente funzione.
int funzione ( ){
int contatore = 0;
int sum = 0;
131
Corso sul Linguaggio C++ Edizione 2010
while (contatore <= 4){
contatore = contatore + 1;
sum = sum + contatore;
}
return sum;
}
Quale valore restituisce la funzione?
Risposte:
a) 10
b) 15
c) 16
d) Nessuna delle risposte precedenti
3) La risposta esatta vale 2 punti.
Si consideri la seguente funzione:
void calcola(int* vett, int n) {
int x,y;
int i, j;
for ( i=0; i<n; i++) {
y = vett[0];
for (j=0; j<n-1; j++) {
x = vett[j+1];
vett[j+1] = y;
y = x;
}
vett[0] = y;
}
}
Assumendo che vett contenga il vettore [10,9,8,7,6,5,4,3,2,1],
quali sono gli elementi di vett dopo l'esecuzione di calcola
(usando 10 come secondo parametro)?
Risposte:
a) [1,2,3,4,5,6,7,8,9,10]
b) [10,9,8,7,6,5,4,3,2,1]
c) [1,3,5,7,9,2,4,6,8,10]
d) nessuna delle precedenti
4) La risposta esatta vale 3 punti.
Cosa stampa il seguente programma?
#include <stdio.h>
int funzione(int arr[], int dim) {
int i = 0;
int t=0;
if(dim % 2==1) {
while(i<dim){
t=arr[i];
arr[i]=arr[dim-i-1];
arr[dim-i-1]=t;
i=i+1;
};
} else {
132
Corso sul Linguaggio C++ Edizione 2010
while(i<dim/2){
t=arr[i];
arr[i]=arr[dim-i-1];
arr[dim-i-1]=t;
i=i+1;
};
}
return arr[0];
};
main() {
int arr1[10]={1,2,3,4,5,6,7,8,9,10};
int arr2[11]={1,2,3,4,5,6,7,8,9,10,11};
int a=funzione(arr1,10);
int b=funzione(arr2,11);
printf ("a=%d,b=%d\n",a,b);
}
Risposte:
a) a=10,b=1
b) a=1,b=1
c) a=10,b=11
d) a=1,b=11
=================================================================
5) La risposta esatta vale 3 punti.
Cosa stampa il seguente programma?
#include <stdio.h>
int funzione1(int arr[]) {
int i = 1;
while(arr[i] != -1)
i = i * 2;
return i;
};
int funzione2(int arr[], int f, int k) {
int i = 0;
int m;
while(i <= f) {
m = (i + f) / 2;
if(arr[m] == k)
return m;
if((arr[m] == -1) || (arr[m] > k))
f = m - 1;
else
i = m + 1;
};
return -1;
};
main() {
int arr[10]={1,2,4,8,-1,-1,-1,-1,-1,-1};
int f=funzione1(arr);
int a=funzione2(arr,f,4);
int b=funzione2(arr,f,7);
printf ("a=%d,b=%d\n",a,b);
}
133
Corso sul Linguaggio C++ Edizione 2010
Risposte:
a) a=2,b=4
b) a=2,b=-1
c) a=-1,b=4
d) a=-1,b=-1
6) La risposta esatta vale 3 punti.
Data la seguente funzione che inizializza i valori di un array bi-dimensionale "matrice":
#define N 5
void inizializza( ){
int matrice[N][N];
int riga, colonna;
for ( riga = 0; riga < N; riga++ ) {
for ( colonna = 0; colonna < N; colonna++ ) {
if ( riga == colonna )
matrice[riga][colonna] = 1;
else if ( riga + colonna == N - 1 )
matrice[riga][colonna] = 1;
else if ( riga < colonna )
matrice[riga][colonna] = 0;
else
matrice[riga][colonna] = matrice[colonna][riga];
}
}
for ( riga = 0; riga < N; riga++ ) {
for ( colonna = 0; colonna < N; colonna++ )
printf( "%d ", matrice[riga][colonna] );
printf( "\n" );
}
}
Indicare quale tra le seguenti configurazioni vengono stampate dalla procedura "inizializza".
Risposte:
a)
1 1
0 1
0 0
0 0
1 1
1
0
1
0
1
1
0
0
1
1
1
0
0
0
1
b)
1 0
0 1
0 0
0 1
1 0
0
0
1
0
0
0
1
0
1
0
1
0
0
0
1
c)
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
134
Corso sul Linguaggio C++ Edizione 2010
0 1 0 0 0
1 0 0 0 0
d)
1 0
0 1
0 0
0 0
0 0
0
0
1
0
0
0
0
0
1
0
0
0
0
0
1
7) La risposta esatta vale 3 punti.
Sia data la seguente funzione ricorsiva:
int mistero(int m, int n) {
if ( m == 0 )
return n;
else if ( n==0 )
return mistero( m-1, 1 );
else
return mistero( mistero( m-1, n-1 ), n-1 );
}
Calcolare quale tra le seguenti risposte corrisponde ai valori restituiti invocando:
printf( "%d %d %d %d\n", mistero(0,3), mistero(1,3), mistero(2,3),
mistero(3,3) );
Risposte:
a) 3 1 2 0
b) 3 2 1 0
c) 0 1 2 3
d) Nessuna delle risposte precedenti
Anno 2005
1) La risposta esatta vale 6 punti.
Si consideri il seguente frammento di programma.
int calcola(int vett[], int n){
int i,y,x;
y=0;
x=vett[0];
for (i=0; i < n-1; i++)
if (x < vett[i+1]){
y=vett[i+1]-x+y;
x=vett[i+1];
}
else if (x-y > vett[i+1])
y=x-vett[i+1];
return y;
}
135
Corso sul Linguaggio C++ Edizione 2010
Dire che cosa restituisce la funzione "calcola" assumendo che venga invocata passando un
vettore di lunghezza n con n maggiore di 2.
Risposta aperta
================================================================
2) La risposta esatta vale 4 punti.
Si consideri il seguente frammento di programma.
bool verifica(int vett[], int n){
int i,j=0;
int appoggio[n];
for (i=0; i < n; i++)
if (j==0){
appoggio[j]=vett[i];
j++;
} else
if (appoggio[j-1] ==vett[i])
j--;
else {
appoggio[j]=vett[i];
j++;
}
if (j==0) return true;
else return false;
}
Si considerino i seguenti tre vettori
{5,8,1,1,2,4,4,8,8,2,3,9,7,7,9,3,8,2,2,5}
{6,7,9,9,5,4,3,3,4,5,7,6,2,2,2,2,4,5,4,5}
{3,3,3,2,2,3,1,5,5,1,3,5,7,6,6,7,5,3,1,1}.
Si supponga di invocare tre volte la funzione verifica passando nell'ordine i tre vettori (assumendo che il secondo parametro sia 20 in tutti e tre i casi).
Quali tre valori saranno restituiti nell'ordine dalle tre invocazioni?
Risposte:
a _ true-false-true
b _ true-true-true
c _ true-false-false
d _ nessuna delle precedenti
================================================================
3) La risposta esatta vale 6 punti.
Si consideri il seguente frammento di programma.
int B(int n);
136
Corso sul Linguaggio C++ Edizione 2010
int A(int n){
int m;
if (n==0)
return 0;
else
if (n%2 == 0)
return n+B(n);
else
return B(n);
}
int B(int n){
int m;
if (n==0)
return 0;
else
if (n%2 == 1)
return n+A(n-1);
else
return A(n-1);
}
Dire che cosa calcola la funzione A assumendo che venga invocata passando un intero positivo.
Risposta aperta
4) La risposta esatta vale 4 punti.
Si consideri la seguente funzione:
int calcola(int n) {
if(n == 1) {
return 1;
} else if(n == 2) {
return n * calcola(n-1);
} else {
return n * calcola(n-1) * calcola(n-2);
}
}
Quale valore restituisce se viene richiamata con parametro 5?
Risposte:
a _ 120
b _ 1400
c _ 414720
d _ nessuna delle precedenti
==================================================================
5) La risposta esatta vale 4 punti.
Si consideri la seguente funzione:
137
Corso sul Linguaggio C++ Edizione 2010
bool calcola(int numero, int* vettore, int i, int j) {
int m = (i + j) / 2;
int off = vettore[m] - numero;
if(off == 0) {
return true;
} else if(i == j) {
return false;
} else if(off > 0) {
return calcola(numero, vettore, i, m - 1);
} else {
return calcola(numero, vettore, m + 1, j);
}
}
Determinare quale fra i seguenti problemi è risolto dall'algoritmo implementato:
Risposte:
a _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j
b _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j
quando i valori in vettore sono ordinati dal più piccolo al più grande
c _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j
quando i valori in vettore sono ordinati dal più grande al più piccolo
d _ nessuna delle precedenti
==================================================================
6) La risposta esatta vale 4 punti.
Si consideri la seguente funzione:
int trova(int bersaglio, int *valori) {
int contatore = 0;
while(valori[contatore++] != bersaglio);
return contatore-1;
}
Essa serve a determinare l'indice in cui si trova un certo valore (rappresentato dal parametro
bersaglio) in un vettore (rappresentato dal parametro valori).
La funzione, però, funziona sempre solo se vale un vincolo specifico rispetto ai dati in ingresso, quale?
Risposta aperta
Anno 2004
1) La risposta esatta vale 4 punti.
Si consideri il seguente frammento di programma:
void quack(int *a, int *b, int *c, int l){
int i = 0, j = 0, k = 0;
138
Corso sul Linguaggio C++ Edizione 2010
while(k < 2*l) {
if((a[i] < b[j] && i < l) || j == l) {
c[k] = a[i];
i++;
} else {
c[k] = b[j];
j++;
}
k++;
}
}
Che problema risolve la procedura/funzione quack?
Risposte:
a _ riempie c con gli elementi di a e b ordinati in modo crescente indipendentemente dall'ordine degli elementi in
aeb
b _ riempie c con gli elementi di a e b ordinati in modo crescente se gli elementi in a e b sono ordinati in modo
crescente
c _ riempie c in modo da riportare tutti i valori negativi di a all'inizio di c e quelli positivi di b alla fine di c
d _ nessuna delle precedenti
========================================================
2) La risposta esatta vale 6 punti.
Si consideri il seguente frammento di programma:
void bang(int *a, int l){
int i = 0, t = 0, loop = 1;
while(loop) {
if(a[i] > 0) {
a[t] = a[i]; t++;
}
if(i < l-1) i++; else loop = 0;
}
}
Si supponga che si richiami bang passando come primo paramento il seguente vettore:
33 3 -22 -18 -30 27 1 -42 -14 -19 -1 23 -40 26 15 30 6 40 -12 -34
e come secondo parametro il numero 20.
Scrivere l'insieme dei valori in a al termine dell'esecuzione di bang.
Risposta aperta
=======================================================
3) La risposta esatta vale 4 punti.
Si considerino le due seguenti funzioni:
int A(int n){
if (n > 0) return n+A(n-1);
}
else return 0;
int B(int n){
return (n*(n+1)/2);
}
Quale delle seguenti affermazioni è vera?
139
Corso sul Linguaggio C++ Edizione 2010
Risposte:
a _ la funzione A calcola il fattoriale di un numero mentre la funzione B calcola la sommatoria di tutti i numeri
compresi fra 1 ed n.
b _ sia la funzione A che la funzione B calcolano la sommatoria di tutti i numeri compresi fra 1 ed n, assumendo
n maggiore o uguale a 1.
c _ la funzione A e la funzione B calcolano esattamente la stessa funzione.
d _ nessuna delle precedenti affermazioni è vera.
=======================================================
4) La risposta esatta vale 4 punti.
Si consideri la seguente funzione conta che prende in input un vettore di numeri di lunghezza n
(assumendo che n sia compreso fra 2 e 100):
int conta(int vett[], int n){
int i, k=0; int j = 1;
for (i = 0; i < n-1; i++)
if (vett[i] < vett[i+1]) j++;
else{
if (k < j) k = j;
j = 1;
}
return k;
}
Quale delle seguenti affermazioni è vera?
Risposte:
a _ la funzione restituisce il numero di interi presenti nel vettore
b _ la funzione restituisce il massimo valore presente nel vettore
c _ la funzione restituisce la lunghezza della più lunga sottosequenza ordinata (in modo strettamente crescente)
all'interno del vettore
d _ nessuna delle precedenti affermazioni è vera
=======================================================
5) La risposta esatta vale 6 punti.
Si consideri la seguente funzione:
int A(int n, int m){
if (n == 0) return 1;
else
if (n%2 == 0) return A(n/2,m)*A(n/2,m);
else
return m*A(n-1,m);
}
Dire quale sarà il valore tornato dalle chiamate
A(3,2)
A(4,3)
A(5,4)
140
Corso sul Linguaggio C++ Edizione 2010
Risposta aperta
6) La risposta esatta vale 6 punti.
Si consideri la seguente funzione:
int A(int a, int b){
int p = 0;
while (a > 0) {
if (a%2 == 1) p = b+p;
a = a/2;
b = 2*b;
}
return p;
}
Dire quale sarà il valore tornato dalle chiamate
A(4,3)
A(7,4)
A(35,25)
Risposta aperta
8.2
Selezione regionale
La selezione regionale si volge in modo molto diverso rispetto alla selezione scolastica, tipicamente la procedura è la seguente:
- ad ogni alunno viene assegnato un computer con gli ambienti Dev C e Dev Pascal installati;
- viene data circa mezzora per prendere confidenza ed esaminare la macchina, poi i docenti
accompagnatori vengono fatti uscire, agli alunni viene consegnata la traccia che in genere
consiste in tre problemi di difficoltà diversa (difficoltà indicata nella traccia con un numero
intero da 1 a 4), per risolvere i quali vengono concesse tre ore;
- non è possibile portare con se né libri né appunti né dischetti, pen drive, telefonini o altri apparecchi elettronici;
- gli alunni devono realizzare i programmi al computer memorizzandoli in una cartella, il nome da assegnare al programma è indicato nella traccia (il nome breve del problema) e li consegnano facendo l’upload della cartella o del sorgente e dell’eseguibile su un server della
scuola che ospita la competizione;
- i programmi devono leggere i dati da un file di testo di nome “input.txt” e scrivere i risultati
in un file, sempre di testo, di nome “output.txt”, senza specificare il percorso (quindi per
provare i programmi bisogna creare il file input.txt con un editor di testo come notepad e
metterlo nella stessa cartella del programma). Non devono leggere e scrivere niente su video
pena la loro non valutazione.
La correzione viene fatta automaticamente da un programma apposito che copia l’eseguibile
in una cartella, ci mette un file di input, manda in esecuzione il programma e controlla se il
file di output prodotto è corretto; ripete questa operazione per 10 volte, con file di input diversi, preparati appositamente dai responsabili e non noti a priore (probabilmente i programmi
verranno testati sia con dati in input tipici che con quelli più estremi).
Ad ogni programma sorgente realizzato e non completamente banale, viene assegnato, come
minimo, un punteggio pari al suo coefficiente di difficoltà. Detto D il coefficiente di difficoltà, se oltre al sorgente c’è anche l’eseguibile corrispondente (ovvero se non ci sono errori di
141
Corso sul Linguaggio C++ Edizione 2010
sintassi) il punteggio sarà 2*D. Inoltre per ognuna delle 10 prove effettuate, se l’output risultante è corretto, vengono aggiunti D punti, quindi il punteggio massimo raggiungibile per la
soluzione di un problema di difficoltà D è 12 * D. Per esempio se la difficoltà di un problema
è 2 il punteggio massimo che si può ottenere per la sua soluzione è 24.
Nelle scorse edizioni sono stati pochi quelli che, nelle tre ore concesse, sono riusciti a risolvere più di un problema. In genere se si riescono a risolvere correttamente almeno due dei tre
problemi si ha una certa garanzia di arrivare alla selezione nazionale.
Nel seguito propongo alcune delle tracce degli anni scorsi, le altre si possono trovare al sito
www.olimpiadi-informatica.it, mentre nel sito http://allenamenti.olimpiadi-informatica.it/ è
possibile trovare, oltre alle tracce, anche varie soluzioni sia dei problemi delle regionali che di
quelli delle olimpiadi nazionali e internazionali.
Prove regionali 2001 – il cassiere Camillo (CAM)
Livello di difficoltà D=3
È venerdì, e il cassiere Camillo ha davanti a sé una lunga fila di clienti della sua banca venuti a ritirare contante
per il weekend. Per fare presto, Camillo decide di usare per ogni cliente il numero minimo possibile di banconote. Sapreste scrivere un programma per evitargli il mal di testa, considerato che ha a disposizione banconote da
100.000, 10.000, 5.000, 2.000 e 1.000 in quantità illimitata e che l'entità di ogni prelievo è un multiplo di 1.000
lire?
Dati in input
Il file input.txt contiene l'importo del prelievo. Il file è costituito da un'unica riga di testo, contenente un numero
(senza puntini o virgole che raggruppano le cifre a tre a tre!).
Dati in output
Il programma, dopo aver letto il file di input, deve calcolare il numero di banconote necessario per ognuno dei
tagli disponibili, e scriverlo su un file di nome output.txt. Più precisamente, il file output.txt deve contenere cinque righe, che corrispondono (in ordine, dalla prima all'ultima) alle banconote da 100.000, 10.000, 5.000, 2.000
e 1.000. Ogni riga deve contenere un unico numero intero, che rappresenta il numero di banconote di quel taglio
necessarie.
Assunzioni
1. Il file di input non contiene altri caratteri oltre a quelli precisati.
2. L'entità del prelievo è in ogni caso inferiore a 1 miliardo di lire.
3. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo
leggere il file di input e scrivere il file di output.
4. L'esecuzione del programma deve terminare entro 5 secondi.
Esempio 1
File di input File di output
10000
0
1
0
0
0
Esempio 2
File di input
152000
File di output
1
5
0
1
0
Esempio 3
File di input
2001000
File di output
20
0
0
0
1
La biblioteca degli smemorati (BIB) olimpiade regionale 2001
Livello di difficoltà D=3
La vostra biblioteca rionale ha qualche problema nello stabilire da quanto tempo gli utenti tengono i libri. Dovete
aiutarli scrivendo un programma che, prese in input due date del 2001, stabilisca quanti giorni intercorrono tra le
due date.
Dati in input
Il file input.txt è formato da una riga che contiene la data iniziale e la data finale del prestito. Più precisamente,
la riga contiene quattro interi: la prima coppia specifica la data iniziale, la seconda la data finale. Ogni data è
formata da due numeri, e cioè il giorno del mese e il numero del mese.
142
Corso sul Linguaggio C++ Edizione 2010
Dati in output
Il programma, dopo aver letto il file di input, deve stabilire quanti giorni intercorrono tra le due date, e scrivere il
numero di giorni su un file di nome output.txt. Più precisamente, il file output.txt deve contenere un'unica riga.
Su questa riga dovrà comparire il numero intero corrispondente ai giorni che intercorrono tra le due date in input.
Assunzioni
1. Il file di input non contiene altri caratteri oltre a quelli precisati.
2. Trenta giorni a novembre, con april, giugno e settembre; di ventotto ce n'è uno: tutti gli altri ne han trentuno.
3. La seconda data non precede mai la prima.
4. Il numero di giorni considerato non comprende quello iniziale: quindi, ad esempio, tra il 2 gennaio e il 2 gennaio intercorrono 0 giorni, tra il 30 gennaio e il 2 febbraio intercorrono tre giorni, e così via.
5. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo
leggere il file di input e scrivere il file di output.
6. L'esecuzione del programma deve terminare entro 5 secondi.
Esempio 1
File di input File di output
2 3 2 3
0
Esempio 2
File di input
File di output
30 1 2 2
3
Esempio 3
File di input
1 5 2 7
File di output
62
Lo struzzo Simone (SIM) – olimpiade 2001
Livello di difficoltà D=2
Lo struzzo Simone si sposta solo nelle direzioni dei quattro assi cardinali (Nord, Sud, Est, Ovest). Ogni suo passo misura 1 metro. Dovete scrivere un programma che, data una sequenza di spostamenti di Simone, misuri
quant'è la distanza fra il punto di partenza e il punto di arrivo.
Dati in input
Il file input.txt contiene la sequenza degli spostamenti. Tale file è costituito da un'unica riga di testo, contenente
una sequenza di S, N, E, O (che indicano gli spostamenti nelle direzioni Sud, Nord, Est, Ovest rispettivamente).
La sequenza è terminata da un *. Ad esempio, il file di input NNESO* dice che Simone si sposta di due metri a
Nord, poi di un metro verso Est, poi di un metro verso Sud, e quindi di un metro a Ovest.
Dati in output
Il programma, dopo aver letto il file di input, deve calcolare la distanza in metri fra il punto di partenza e il punto
di arrivo, e scriverla su un file di nome output.txt. Più precisamente, il file output.txt deve contenere un'unica riga. Su questa riga dovrà comparire il numero intero corrispondente al quadrato della distanza.
Assunzioni
1. Il file di input non contiene altri caratteri oltre a quelli precisati.
2. Il numero complessivo di spostamenti contenuti nel file di input è minore o uguale a 100000.
3. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo
leggere il file di input e scrivere il file di output.
4. L'esecuzione del programma deve terminare entro 5 secondi.
Esempio 1
File di input File di output
NNSEEESNOENNS*
13
Esempio 2
File di input
File di output
NNESOS*
0
Esempio 3
File di input
OSOS*
File di output
8
Foto Stellare – olimpiadi 2004
Livello di difficoltà D = 4
Il dottor Hubb è un appassionato di astronomia. Possiede una carta astronomica planare, composta dai due assi
del piano cartesiano aventi l'origine in posizione (0,0). La carta è suddivisa in quattro quadranti delimitati dagli
assi cartesiani, dove a ciascun quadrante viene assegnata una lettera distinta come mostrato in figura:
A
143
B
Corso sul Linguaggio C++ Edizione 2010
D
C
Le stelle nella carta planare sono rappresentate mediante coordinate intere e un valore di intensità. Precisamente,
ciascuna stella è rappresentata da una tripla x, y, k, per individuare le coordinate (x, y) del centro della stella e la
sua intensità espressa mediante un intero k e { 1,2,3 }.
Al dottor Hubb piace molto scattare delle foto digitali al cielo stellato. Ciascuna foto digitale è
composta da zeri e da uni. Lo sfondo del cielo è rappresentato dagli zeri, mentre una stella x, y, k è
rappresentata dagli uni in accordo al valore di k:
k= 1
k=2
k=3
1
1
111
1
111
11111
1
111
1
In tal caso, la coordinata (x, y) si riferisce all'uno in posizione centrale. Le stelle possono parzialmente sovrapporsi nelle foto ma le loro coordinate sono sicuramente distinte come coppie di valori.
Purtroppo il dottor Hubb è un pasticcione. La sua carta astronomica contiene N stelle, e lui ha scattato una foto
digitale di taglia M x M, ma non ricorda in quali quadranti. Per l'elevata tecnologia adottata, non ci sono distorsioni ottiche: basta sovrapporre la foto digitale alla carta planare in una posizione opportuna, per farle combaciare e poter cosi ricostruire il quadrante (o i quadranti) di appartenenza. Le risposte ammissibili sono A, B, C, D,
AD, AB, BC, CD, ABCD, mentre AC, BD, ABC, ecc., non sono considerate valide.
Suggerimento: Quadrettare a griglia il piano cartesiano con le coordinate intere, assegnando le coordinate (x, y)
al quadratino avente (x, y) come coordinata dello spigolo inferiore sinistro.
Dati in input
Il file di input contiene una sequenza di righe. La prima riga contiene il valore di N e M. Le N righe successive
rappresentano le N stelle nella carta astronomica, ciascuna riga contenente una tripla x, y, k, interpretata in accordo a quanto descritto sopra. Le M righe finali rappresentano la foto digitale, ogni riga contenente una sequenza di M valori scelti tra zeri e unì.
Dati in output
Il file di output contiene una sola delle risposte ammissibili, A, B, C, D, AD, AB, BC, CD, ABCD, in base alla
posizione della foto digitale rispetto ai quadranti della carta astronomica.
Assunzioni
I numeri sono rappresentati con il segno. Le coordinate (x, y) sono coppie di numeri, dove - 1000 < x, y < 1000.
Per la conformazione della carta stessa e della foto c'è esattamente una risposta da fornire (non è possibile che la
foto appaia due o più volte in posizioni distinte). La foto non taglia le stelle: una stella è interamente catturata
dalla foto oppure non è catturata affatto. Infine, se una stella ha centro nel quadrante A ma la sua intensità è tale
che induca un uno nel quadrante B, per esempio, allora la stella viene considerata a cavallo dei due quadranti A e
B (non conta solo il centro, ma anche l'intensità).
144
Corso sul Linguaggio C++ Edizione 2010
Esempio 1
File di input
File di output
AB
9 7
-3 7 1
-2 6 1
-1 2 2
-2 1 1
1 7 1
1 3 3
2 3 2
2 -1 1
-3 -1 1
1000100
0100000
0000100
0001110
0011111
0111110
0110100
La dieta di Poldo – olimpiadi 2004
Livello di difficoltà D=3
Il dottore ordina a Poldo di seguire una dieta. Ad ogni pasto non può mai mangiare un panino che abbia un peso
maggiore o uguale a quello appena mangiato. Quando Poldo passeggia per la via del suo paese da ogni ristorante
esce un cameriere proponendo il menù del giorno. Ciascun menù è composto da una serie di panini, che verranno
serviti in un ordine ben definito, e dal peso di ciascun panino. Poldo, per non violare la regola della sua dieta,
una volta scelto un menù, può decidere di mangiare o rifiutare un panino; se lo rifiuta il cameriere gli servirà il
successivo e quello rifiutato non gli sarà più servito.
Si deve scrivere un programma che permetta a Poldo, leggendo un menù, di capire qual è il numero massimo di
panini che può mangiare per quel menù senza violare la regola della sua dieta.
Riassumendo, Poldo può mangiare un panino se e solo se soddisfa una delle due condizioni:
I) il panino è il primo che mangia in un determinato pasto;
2)il panino non ha un peso maggiore o uguale all'ultimo panino che ha mangiato in un determinato pasto.
Dati in input
La prima linea del file input.txt contiene il numero m di panini proposti nel menu. Le successive m linee contengono un numero intero non negativo che rappresenta il peso del panino che verrà servito. I panini verranno serviti nell'ordine in cui compaiono nell'input.
Dati in output
Il file output.txt contiene il massimo numero di panini che Poldo può mangiare rispettando la dieta.
Assunzioni
I pesi di panini sono espressi in grammi, un panino pesa al massimo 10 Kg.
Un menù contiene al massimo 100 panini
145
Corso sul Linguaggio C++ Edizione 2010
Esempio 1
File di input File di output
8
389
207
155
300
299
170
158
65
6
Esempio 2
File di input
3
22
23
27
File di output
1
Esempio 3
File di input
22
15
14
15
389
201
405
204
130
12
50
13
26
190
305
25
409
3011
43
909
987
1002
900
File di output
6
Giardinaggio – olimpiade 2004
Livello di difficoltà D=2
Pippo ha deciso di spendere il proprio tempo libero facendo giardinaggio. Poiché il numero di fiori piantati nelle
sue aiuole gli pare eccessivo, decide di ridurne il numero eliminando quelli tra loro più vicini. A tale scopo, essendo appassionato di informatica, decide di scrivere un programma che gli permetta di risolvere il problema.
Di ogni fiore viene identificata la posizione e questa viene memorizzata in un file indicando le coordinate, in
centimetri, rispetto l'angolo in basso a sinistra dell'aiuola; ogni posizione è contenuta in una riga diversa del file e
codificata con due numeri interi separati da uno spazio. Oltre alle righe che contengono le posizioni dei fiori, il
file contiene anche, nella prima riga, il numero di fiori che deve essere eliminato.
In sostanza il programma deve cercare la coppia di fiori con distanza minima ed eliminare, dei due, il fiore più in
alto (cioè quello la cui ordinata è maggiore); ripetere, quindi, l'operazione tante volte quanti sono i fiori da eliminare. Nel caso in cui l'ordinata dei due fiori fosse uguale, il fiore da eliminare è quello con ascissa maggiore.
Dati in input
I dati di ingresso sono contenuti nel file input.txt che contiene:
- nella prima riga, un numero intero che rappresenta il numero di fiori da eliminare;
- dalla seconda riga al termine, coppie di numeri interi separati da uno o più spazi bianchi, il primo rappresenta
l'ascissa ed il secondo l'ordinata della piantina.
Dati in output
Il risultato deve essere scritto nel file di testo output.txt indicando le coordinate dei punti eliminati nello stesso
formato del file in ingresso (esclusa la prima riga). I punti devono essere ordinati, in modo crescente, rispetto
all'ascissa, e nel caso in cui avessero la stessa ascissa, rispetto l'ordinata.
Assunzioni
1. Non ci sono due o più fiori esattamente nello stesso punto;
2. la distanza è calcolata usando la formula euclidea;
3. il numero di piante da eliminare, indicato nel file di ingresso, è sempre minore o uguale al numero di piante
contenute nello stesso file;
4. il file di ingresso contiene almeno due piante;
5. l'aiuola ha dimensioni 1000 x 1000 cm.
146
Corso sul Linguaggio C++ Edizione 2010
Esempio 1
File di input File di output
2
30
50
10
40
10
20
80
60
60
100
40 60
50 80
Le pesate di bilancino (bilancino) olimpiade – 2006
Livello di difficoltà D = 3
Bilancino è un bambino con una passione maniacale, quella di mettere gli oggetti in ordine crescente di peso.
I suoi genitori posseggono un'antica e rara bilancia con due bracci uguali: posti due oggetti, uno per braccio, la
bilancia permette di stabilire quale dei due oggetti è più pesante, ma non permette di trovarne il peso assoluto.
Oggi Bilancino vuole mettere in ordine crescente di peso N oggetti e, a tale scopo, ha già effettuato una serie di
M pesate, trascrivendone i risultati. Infatti, numerati tali oggetti da 1 a N, egli ha pesato M coppie di oggetti distinti x e y, dove 1 <= x, y <= N, scrivendo i due interi x e y in quest'ordine su una riga per indicare che x è più
leggero di y e, invece, scrivendo y e x in quest'ordine per indicare che y è più leggero di x. Da notare che non
esistono due oggetti con lo stesso peso (siano essi stati pesati o meno da Bilancino) e che la stessa coppia di oggetti non può essere pesata più di una volta.
Esaminate le M pesate finora eseguite da Bilancino e aiutatelo a decidere quale, tra le seguenti alternative, consente di stabilire l'ordine crescente di peso tra gli N oggetti:
• le M pesate sono sufficienti;
• è necessaria un'ulteriore pesata;
• sono necessarie due o più pesate.
Dati di input
Il file “input.txt” è composto da M+1 righe.
La prima riga contiene due interi positivi separati da uno spazio: il primo intero rappresenta il numero N di oggetti da ordinare in base al peso mentre il secondo intero rappresenta il numero M di pesate effettuate da Bilancino.
Le successive M righe contengono coppie di interi positivi: la j-esima di tali righe è composta da due interi distinti a e b separati da uno spazio, a rappresentare la j-esima pesata effettuata da Bilancino, in cui egli scopre che
l'oggetto a è più leggero dell'oggetto b (dove 1 <= j <= M e 1 <= a, b <= N). Da notare che la stessa pesata non
può apparire in più di una riga.
Dati di output
Il file “output.txt” è composto da una riga contenente un solo intero come dalla seguente tabella.
0 : nessuna ulteriore pesata è necessaria per stabilire l'ordine crescente di tutti gli oggetti.
1 : serve e basta un'ulteriore pesata per stabilire l'ordine crescente di tutti gli oggetti.
2 : due o più pesate sono ulteriormente necessarie per stabilire l'ordine crescente di tutti gli oggetti.
Assunzioni
1 < N < 100.
1 <= M <= N(N-1)/2.
I dati in “input.txt” garantiscono sempre che esiste almeno un ordinamento degli oggetti compatibile
con tutte le pesate trascritte da Bilancino.
Esempio 1
File di input File di output
3 2
1 2
3 1
0
Esempio 2
File di input
4
2
1
1
2
4
3
4
3
1
File di output
1
147
Esempio 3
File di input
4
2
1
2
3
3
4
1
File di output
2
Corso sul Linguaggio C++ Edizione 2010
Teste di serie (serie) – olimpiade 2006
Un torneo è composto da K gironi, con N squadre partecipanti in ciascun girone (per un totale di KxN squadre
nel torneo). Dopo le eliminatorie, passa soltanto la prima classificata di ogni girone.
A ogni squadra è associato un "coefficiente di bravura", ovvero un intero positivo che è tanto maggiore quanto
più la squadra è forte. Per rendere più vivace il torneo, gli organizzatori vogliono far gareggiare le squadre più
forti tra loro soltanto dopo le eliminatorie: in altre parole, le K squadre con i coefficienti di bravura più alti devono giocare in gironi distinti.
Aiutate gli organizzatori a verificare che la composizione del torneo rispetti il loro volere: prese le K squadre con
il più alto coefficiente di bravura, ciascun girone deve contenere esattamente una di esse (da notare che due o più
squadre possono avere lo stesso coefficiente).
Dati di input
Il file “input.txt” è composto da K+1 righe.
La prima riga contiene due interi positivi separati da uno spazio: il numero K di gironi e il numero N di squadre
per girone.
Le successive K righe contengono i coefficienti di bravura delle squadre: la j-esima di tale righe contiene N interi positivi separati da uno spazio che sono i coefficienti di bravura delle N squadre nel j-esimo girone, per
1 <= j <= K.
Dati di output
Il file “output.txt” è composto di una riga contenente un solo intero: 1 se il torneo rispetta i vincoli imposti dagli
organizzatori, 0 altrimenti.
Assunzioni
1 < N <= 100.
1 < K <= 100.
Esempio 1
File di input File di output
3
2
2
2
4
2 2 1
1 3 1
4 2 1
1
Esempio 2
File di input
3
3
3
43
4
5 7 9
6 78 90
78 71 32
File di output
0
Ritrovo a Brambillia (brambillia) – olimpiade 2006
Livello di difficoltà D = 2
Nell'isola di Brambillia, vi sono N città numerate da 1 a N e collegate attraverso una ferrovia circolare, le cui
tratte sono anch'esse numerate da 1 a N e possono essere percorse in entrambe le direzioni: la tratta ferroviaria j
collega direttamente la città j alla città j+1 (e, percorsa nella direzione opposta, collega j+1 a j) dove j = 1, 2, ...,
N-1; la tratta N collega la città N alla città 1 (e, percorsa nella direzione opposta, collega 1 a N).
Il biglietto ferroviario per ciascuna tratta ha un costo prestabilito.
Date due qualunque città p e q, è possibile andare da p a q attraverso due percorsi ferroviari alternativi (ipotizzando che 1 <= p < q <= N, un percorso attraversa le tratte p, p+1, ..., q-1 mentre l'altro attraversa, nella direzione opposta, le tratte p-1, p-2, ..., 1, N, N-1, ..., q; per andare da q a p, attraversiamo tali percorsi ma in direzione
opposta). Il biglietto ferroviario per ciascuno dei percorsi ha un costo pari alla somma dei costi delle singole tratte che lo compongono.
Gli abitanti di Brambillia intendono utilizzare la ferrovia circolare per ritrovarsi in occasione della sagra annuale
dell'isola e devono scegliere la città presso cui organizzare tale sagra minimizzando il costo totale dei biglietti.
Per questo motivo hanno contato, per ogni città, quante persone vogliono parteciparvi, visto che è necessario acquistare un biglietto ferroviario per persona al costo descritto sopra (per gli abitanti della città che verrà scelta, il
costo sarà nullo perché non dovranno prendere il treno). In base a tale conteggio, individuate la città in cui organizzare la sagra, tenendo presente che le persone possono giungervi attraverso uno dei due percorsi a loro disposizione nella ferrovia circolare.
Dati di input
Il file “input.txt” è composto da 2N+1 righe.
La prima riga contiene un intero positivo che rappresenta il numero N delle città.
Le successive N righe contengono ciascuna un intero positivo: quello nella j-esima di tali righe rappresenta il
costo del biglietto ferroviario per la tratta j, dove 1 <= j <= N.
148
Corso sul Linguaggio C++ Edizione 2010
Le ulteriori N righe contengono ciascuna un intero positivo o nullo: quello nella j-esima di tali righe è il numero
delle persone della città j che intendono partecipare alla sagra, per 1 <= j <= N.
Dati di output
Il file “output.txt” è composto da una riga contenente un solo intero j che rappresenta la città j presso cui organizzare la sagra. Come osservato in precedenza, tale città rende minimo il costo totale, ottenuto sommando i costi dei biglietti ferroviari di tutti i partecipanti.
Assunzioni
1 < N < 100.
I dati in “input.txt” garantiscono che la soluzione è unica (esiste una sola città in cui organizzare la sagra).
Esempio 1
File di input File di output
4
45
34
18
40
3
0
4
2
4
Mappa antica – olimpiadi 2008
Difficoltà D=2
Topolino è in missione per accompagnare una spedizione archeologica che segue un'antica mappa
acquisita di recente dal museo di Topolinia. Raggiunta la località dove dovrebbe trovarsi un
prezioso e raro reperto archeologico, Topolino si imbatte in un labirinto che ha la forma di una
gigantesca scacchiera quadrata di NxN lastroni di marmo.
Nella mappa, sia le righe che le colonne del labirinto sono numerate da 1 a N. Il lastrone che si
trova nella posizione corrispondente alla riga r e alla colonna c viene identificato mediante la
coppia di interi (r, c). I lastroni segnalati da una crocetta '+' sulla mappa contengono un
trabocchetto mortale e sono quindi da evitare, mentre i rimanenti sono innocui e segnalati da un
asterisco '*'.
Topolino deve partire dal lastrone in posizione (1, 1) e raggiungere il lastrone in posizione (N, N),
entrambi innocui. Può passare da un lastrone a un altro soltanto se questi condividono un lato o
uno spigolo (quindi può procedere in direzione orizzontale, verticale o diagonale ma non saltare) e,
ovviamente, questi lastroni devono essere innocui.
Tuttavia, le insidie non sono finite qui: per poter attraversare incolume il labirinto, Topolino deve
calpestare il minor numero possibile di lastroni innocui (e ovviamente nessun lastrone con
trabocchetto). Aiutate Topolino a calcolare tale numero minimo.
Dati di input
Il file input.txt è composto da N+1 righe.
La prima riga contiene un intero positivo che rappresenta la dimensione N di un lato del labirinto a
scacchiera.
Le successive N righe rappresentano il labirinto a scacchiera: la r-esima di tali righe contiene una
sequenza di N caratteri '+' oppure '*', dove '+' indica un lastrone con trabocchetto mentre '*' indica
un lastrone sicuro. Tale riga rappresenta quindi i lastroni che si trovano sulla r-esima riga della
schacchiera: di conseguenza, il c-esimo carattere corrisponde al lastrone in posizione (r, c).
Dati di output
Il file output.txt è composto da una sola riga contenente un intero che rappresenta il minimo
numero di lastroni innocui (ossia indicati con '*') che Topolino deve attraversare a partire dal
lastrone in posizione (1, 1) per arrivare incolume al lastrone in posizione (N, N). Notare che i
lastroni (1, 1) e (N, N) vanno inclusi nel conteggio dei lastroni attraversati.
Assunzioni
1 ≤ N ≤ 100;
1 ≤ r, c ≤ N.
149
Corso sul Linguaggio C++ Edizione 2010
E' sempre possibile attraversare il labirinto dal lastrone in posizione (1, 1) al lastrone in
posizione (N, N); inoltre tali due lastroni sono innocui
Esempio 1
File di input File di output
4
*+++
+**+
+*+*
+***
8.3
5
Soluzioni delle selezioni scolastiche
Test 2008
Esercizio
1
2
3
4
5
6
7
8
9
Punti
3
3
1
□
x
□
x
□
□
□
□
□
2
2
2
3
2
2
a
a
a
a
a
a
a
a
a
Risposta
b
□
b
□
b
x
b
□
b
□
b
□
b
x
b
x
b
□
c
c
c
c
c
c
c
c
c
□
□
□
□
x
□
□
□
□
Risposta
x b
□
□ b
x
B: N2
C:
□ b
□
□ b
□
□ b
□
□ b
x
c
c
2N
c
c
c
c
□ d
□ d
D: N!
x d
x d
x d
□ d
x
□
□
□
□
x
□
□
x
d
d
d
d
d
d
d
d
d
Test 2007
Esercizio
1
2
3
4
5
6
7
Punti
1
2
2/R
3
3
3
5
□ a
□ a
A: 2N
□ a
□ a
□ a
□ a
Test 2006
Esercizio: Punti:
1
1
2
1
3
2
4
3
5
3
6
3
7
3
8
3
Risposta:
□
□
x
□
□
x
□
a
a
a
a
a
a
a
x
x
□
x
x
□
x
a = 5
b
b
b
b
b
b
b
150
□
□
□
□
□
□
□
b = 5
c
c
c
c
c
c
c
□
□
□
□
□
□
□
d
d
d
d
d
d
d
Corso sul Linguaggio C++ Edizione 2010
Test 2005
Esercizio: Punti:
1
6
2
3
4
5
6
4
6
4
4
6
Risposta:
La differenza fra l’elemento massimo e l’elemento minimo presenti nel
vettore.
X a
□ b
□ c
□ d
La somma di tutti gli interi positivi minori od uguali al parametro.
□ a
□ b
□ c
X d
□ a
X b
□ c
□ d
Che il valore cercato esista effettivamente nel vettore di input.
Test 2004
Esercizio: Punti:
1
4
Risposta:
□a
xb
2
6
3
4
4
4
5
6
6
6
33, 3, 27, 1, 23, 26, 15, 30, 6, 40, -1, 23, -40, 26, 15, 30,
6, 40, -12, -34
□a
xb
□c
□d
□a
□b
xc
□d
ma se il vettore è ordinato K=0, quindi anche ‘d’ è giusta
8, 81, 1024
12, 28, 875
□c
□d
8.4
Soluzione di alcuni problemi delle selezioni
regionali
Cassiere Camillo – 2001
Il problema è stato già proposto e risolto a pag 103. La soluzione deve, però, essere adattata ai vincoli imposti
nella selezione regionale e, quindi, i dati in input devono essere letti dal file input.txt e i dati in output devono
andare nel file output.txt. Questo significa che bisogna aprire i due flussi e poi bisogna utilizzare per l’input
fscanf() invece di scanf e, per l’output, fprintf invece di printf . Il programma modificato è il seguente:
#include <stdio.h>
// programma cam.cpp - Cassiere Camillo- selezioni regionali 2001
int main(){
FILE *in, *out; // in=flusso per l'input e out = flusso per l'output
int tb[]={100000, 10000, 5000, 2000, 1000}; // i 5 tipi di banconote
int somma, i; // somma = l'importo in input
int nb[5]; // nb[] = numero banconote per ogni taglio (dati in output)
in = fopen("input.txt","r"); // apre i due file
out = fopen("output.txt","w");
fscanf(in,"%d", &somma); // legge la somma da pagare
for (i= 0; i <5; i++) {
nb[i] = somma / tb[i]; // numero banconote del taglio i
somma %= tb[i];// quello che resta dopo aver dato il taglio i
151
Corso sul Linguaggio C++ Edizione 2010
fprintf(out,"%d\n", nb[i]);
}
fclose(in); fclose(out);
return 0;
}
Dieta di Poldo – olimpiadi 2004
Si tratta di individuare la più lunga serie decrescente in una serie di numeri. L’algoritmo proposto utilizza due
vettori. il vettore peso[] che contiene i pesi degli m panini caricati in input e il vettore np[] dove, per ogni i
l’elemento np[i] contiene il numero massimo di panini che si possono prendere partendo dalla portata i.
Sappiamo che se si parte dall’ultima portata (la portata m-1) np[m-1]=1. Se conosciamo np[j] per ogni j>i allora
per calcolare np[i] basta trovare il massimo per tutti gli np[j] con j>i e con peso[j]<peso[i] e aggiungerci 1 oppure se tutti i panini seguenti hanno peso >= peso[i] allora np[i]=1.
Il massimo del vettore np[i] sarà il numero massimo di panini che Poldo può prendere.
#include <stdio.h>
// Programma Poldo - Olimpiade 2004
int main(){
FILE *fi, *fo;
int m , peso[100], np[100], i, j, n, max;
fi = fopen("input.txt","r"); // apre i due file
fo = fopen("output.txt","w");
fscanf(fi,"%d ", &m); // legge il numero m dei panini del menù
for (i=0; i<m; i++) fscanf(fi,"%d",&peso[i]); // carica i pesi
max=0; if (m>0) {np[m-1]=1; max=1;}
/* calcola per ogni i il numero massimo di panini np[i] che può prendere
partendo dalla portata i */
for (i=m-2; i>=0; i--) {
n=1; // n=numero max di panini partendo dalla portata i
for (j=i+1; j<m; j++)
if (peso[j] < peso[i] && np[j] >= n) n=np[j]+1;
np[i]=n;
if (max < n) max=n;
}
fprintf(fo,"%d", max);
fclose(fi); fclose(fo);
return 0;
}
Testa di serie – olimpiadi 2006
Riporto qui l’algoritmo proposto da Casciato Amedeo, l’alunno del nostro istituto che ha partecipato alla selezione regionale delle olimpiadi del 2006 e che, risolvendo questo problema, si è classificato ad uno dei primi posti nella classifica regionale.
Vengono definiti due vettori, Best[] e Best2[], il primo di K elementi e il secondo di 2*K elementi (K = numero
di gironi). Carica nel vettore Best[] il coefficiente di bravura della migliore squadra di ogni girone (in totale K
numeri), mentre carica nel vettore Best2[] il coefficiente delle due migliori squadre di ogni girone (in totale 2*k
numeri).
Ordina i due vettori in modo decrescente.
Se i primi K elementi dei due vettori ordinati coincidono vuol dire che le k migliori squadre di ogni girone (tutte
presenti nel vettore Best[]) sono le K migliori squadre in assoluto e quindi il torneo rispetta i vincoli imposti altrimenti non li rispetta.
Questo algoritmo ha il vantaggio di caricare in memoria solo 3*K coefficienti di bravura e non tutti i dati in input che potrebbero anche essere 100*100= 10000 e di essere molto veloce (i vettori da ordinare sono piccoli).
#include <stdio.h>
/*
N
= numero di squadre per ogni girone
K
= numero di gironi
152
Corso sul Linguaggio C++ Edizione 2010
Best[]= vettore contenenente i coefficienti di bravura della
migliore squadra di ogni girone( in totale K coefficienti)
Best2[]= vettore conenente contenente i coefficienti di bravura
delle due migliorisquadre di ogni girone, quindi in
totale 2 * K coefficienti.
*/
void ordina(int vett[],int n) {
int i,j,a;
//ordina in maniera decrescente il vettore vett[] di n elementi
for (i=0; i<n; i++)
for (j=i+1; j <n; j++)
if (vett[i]<vett[j]) {a=vett[i]; vett[i]= vett[j]; vett[j]=a;}
}
int main(){
FILE *fi, *fo;
int j,i,max1, max2,N,K, coeff;
int Best[100], Best2[200];
fi = fopen("input.txt","r"); // apre i due file
fo = fopen("output.txt","w");
fscanf(fi,"%d %d", &K, &N); // legge N e K
// legge i dati in input e carica i vettori Best[] e Best2[]
for (i=0; i<K; i++) { // ciclo sui gironi
fscanf(fi,"%d %d", &max1, &max2); // legge le prime due squadre
if (max1 < max2) {coeff=max1; max1=max2; max2=coeff;}
for (j=2; j<N; j++) { // ciclo sulle restanti squadre del girone
fscanf(fi, "%d ", &coeff);
if (coeff > max1) {
max2 = max1; max1 = coeff;
} else
if (coeff > max2) max2 = coeff;
}
Best2[2*i]=Best[i]=max1; Best2[2*i+1]=max2;
}
ordina(Best, K); // ordina i due vettori
ordina(Best2,2*K);
/* confronta i due vettori, se i primi k elementi non sono uguali
vuol dire che il torneo non rispetta i vincoli */
for (i = 0; i<K; i++) if (Best[i] != Best2[i]) break;
if (i<K) fprintf(fo,"%d", 0); else fprintf(fo,"%d", 1);
fclose(fi); fclose(fo);
return 0;
}
Mappa antica – olimpiadi 2008
La mappa viene rappresentata con una matrice (mappa) di interi di ordine n+2, ossia si utilizza un bordo sentinella intorno alla matrice originale in modo da garantire che ogni cella interna sia attorniata sempre da 8 celle (in
questo modo si evitano di controllare ogni volta se la cella si trova sul bordo).
Il valore di una cella va così interpretato:
- se nullo, si tratta di un "trabocchetto" e quindi non va attraversato;
- se > 0, è il numero ottimo di lastroni attraversati da (1,1) per raggiungerlo.
Durante la lettura da file le celle della mappa vengono poste a zero per le celle "+" (trabocchetto) e ad un valore
"infinito" (basta anche n*n) per le celle "*" (sicure). Il bordo sentinella viene posto a zero come se fossero celle
trabocchetto.
153
Corso sul Linguaggio C++ Edizione 2010
La cella (1,1) viene inizializzata ad 1 che è il numero dei lastroni da attraversare per passare da (1,1) a (1,1). Poi
considerato che, se conosciamo il numero di lastroni ottimo da attraversare per raggiungere la cella (r,c) per tutte
le celle adiacenti (che sono 8) al massimo il numero dei lastroni sarà 1 in più, possiamo propagare le modifiche a
tutte le celle collegate.
Per propagare le modifiche esaminiamo tutta la matrice impostando al massimo 1 in più per le 8 celle adiacenti
ad ogni cella non trabochetto a meno chè in questa cella ci sia già un numero inferiore (quindi le celle con trabocchetto e quelle che si possono raggiungere attraverso un altro percorso più corto venono escluse
dall’aggiornamento). L’aggiornamento si ripete fin quando non ci sono più modifiche. Per ripetere il minor numero di volte l’aggiornamento esaminiamo la matrice alternativamente dall’alto in baso e dal basso in alto.
Il risultato sarà il valore finale memorizzato in mappa[n, n].
#include <stdio.h>
const int NMAX = 102;
const int INFINITO = 10000;
const int TRAB = 0;
int mappa[NMAX][NMAX];
int n;
int main() {
FILE *input, *output;
char s;
input = fopen("input.txt","r"); // apre i due file
output = fopen("output.txt","w");
fscanf(input,"%d", &n); // legge il numero di righe e colonne
int r, c, h;
for (c=0; c<n+2; c++) mappa[0][c]= 0; // prima riga sentinella
// legge la mappa
for (r=1; r<=n; r++) {mappa[r][0]=0;
for (c=1; c<=n; c++) {
do fscanf(input, "%c", &s); while(s !='+' && s != '*');
if (s=='*') mappa[r][c]= INFINITO; else mappa[r][c]=TRAB;
}
mappa[r][n+1]=0;
}
for (c=0; c<n+2; c++) mappa[n+1][c]= 0; // ultima riga sentinella
//imposta il valore dalla prima casella che è conosciuto
mappa[1][1] = 1; int ripeti=1;
/* propaga le modifiche nella mappa fin quando non ci sono più modifiche
da propagare */
while (ripeti) {
/* per evitare di ripetere molte volte il cilco propaga le modifiche
alternativamente dall'alto il basso, da sinistra a destra e dal basso
in alto, da destra a sinistra
*/
if (ripeti > 0) {ripeti=0;
for (r=1; r<=n; r++)
for (c=1; c<=n; c++)
if (mappa[r][c] > TRAB) { // se non è un trabochetto
h = mappa[r][c] + 1;
/* -- stessa riga a sx -- */
if (mappa[r][c-1] > h ) {mappa[r][c-1] = h; ripeti=-1;}
/* -- stessa riga a dx -- */
if (mappa[r][c+1] > h) mappa[r][c+1] = h;
/* -- riga prec. -- */
for (int ic=c-1; ic<=c+1; ic++)
if (mappa[r-1][ic] > h) {mappa[r-1][ic] = h; ripeti=-1;}
/* -- riga succ. -- */
154
Corso sul Linguaggio C++ Edizione 2010
for (int ic=c-1; ic<=c+1; ic++)
if (mappa[r+1][ic] > h) mappa[r+1][ic] = h;
}
} else { // propagazione dal basso in alto e da destra a sinistra
ripeti = 0;
for (r=n; r>0; r--)
for (c=n; c>0; c--)
if (mappa[r][c] > TRAB) { // se non è un trabochetto
h = mappa[r][c] + 1;
/* -- stessa riga a sx -- */
if (mappa[r][c-1] > h ) mappa[r][c-1] = h;
/* -- stessa riga a dx -- */
if (mappa[r][c+1] > h) {mappa[r][c+1] = h; ripeti=1;}
/* -- riga prec. -- */
for (int ic=c-1; ic<=c+1; ic++)
if (mappa[r-1][ic] > h) mappa[r-1][ic] = h;
/* -- riga succ. -- */
for (int ic=c-1; ic<=c+1; ic++)
if (mappa[r+1][ic] > h) {mappa[r+1][ic] = h; ripeti=1;}
}
}
}
// scrive il risultato in output
fprintf(output,"%d", mappa[n][n]);
fclose(input); fclose(output);
return 0;
}
155
9
Capitolo
Corso sul Linguaggio C++ Edizione 2010
9. Appendici e Link
9.1
Conclusioni
Il linguaggio C è un linguaggio di alto livello, per tutti gli usi (general-purpose) progettato per
permettere una efficiente traduzione in linguaggio macchina in modo da creare programmi eseguibili il più possibile simili a quelli scritti direttamente in linguaggio macchina ma, nello
stesso tempo, indipendenti dal particolare processore. Permette l’accesso di basso livello alla
memoria e anche ai registri interni del processore ma contemporaneamente è progettato per
permettere il riutilizzo del codice anche su macchine con sistemi operativi e processori diversi.
Per ottenere programmi eseguibili il più possibile veloci il compilatore riduce al minimo il
supporto run-time, per esempio caratteristiche specifiche di una particolare funzione, che non
sono comuni a tutte le piattaforme, non sono incluse nel linguaggio (ma sono incluse in librerie aggiuntive esterne), non vengono inseriti controlli di run-time per evitare errori che non
dovrebbero essere presenti in un programma corretto (per esempio i controlli sugli indici dei
vettori per evitare che superino i limiti). La traduzione in linguaggio macchina è sempre fatta
nel modo più semplice e diretto.
Fra le sue principali caratteristiche ricordiamo che permette la programmazione strutturata,
permette la ricorsione, permette la definizione di variabili con visibilità limitata ad un particolare pezzo di programma, tutto il codice è contenuto all’interno di funzioni, ha un sistema di
tipi di dati molto flessibile che permette la creazione di praticamente qualsiasi struttura di dati,
supporta in modo completo l’utilizzo dei puntatori (attraverso i quali si può avere anche
l’accesso di basso livello alla memoria), ecc.
Tra le caratteristiche assenti ricordiamo il controllo di run-time per evitare che gli indci dei
vettori superino i limiti, le operazioni su interi vettori (inclusi però nelle classi aggiuntive
String e Vector del C++).
Dopo la sua creazione molte versioni di questo linguaggio sono state sviluppate. Per garantire
uniformità nelle varie implementazioni e, quindi, permettere la migrazione del codice da una
piattaforma all’altra, è internvenutta prima l’ANSI (American National Standards Institute)
per definire un primo standard denominato ANSI C89, poi l’ISO (International Organization
for Standardization) che ha prodotto le specifiche denominate ISO C99. Mentre le specificheper il linguaggio C sono rimaste ferme al 1999 quelle per il linguaggio C++ sono in continua
evoluzione. Anche per questo linguaggio l’ISO ha formato una commissione (ISO/IEC
14882:1998) che ha prodotto vari aggiornamenti nello standard in particolare nel 2003 e nel
2005.
156
Corso sul Linguaggio C++ Edizione 2010
9.2
Come approfondire.

Libri
• Ferdinando Sampietro e Ornella Snpietro, Il linguaggio di programmazione C++, Editore Tramontana
• Piero Gallo e Fabio Salerno, Il linguaggio C++, Editore Minerva Italica
• Bruce Eckel, Thinking in C++ 2° Edition, primo e secondo volume,
http://www.mindview.net/, il primo volume disponibile anche in italiano all’indirizzo
http://www.umbertosorbo.it.

Il DEV-C++
http://www.bloodshed.net/dev/devcpp.html Sito ufficiale del progetto open
source Dev-C++. Il Dev-C++ include il compilatore C g++ realizzato
nell’ambito del progetto gnu (www.gnu.org).
http://sourceforge.net/projects/dev-cpp/ Un sito in inglese dove si possono trovare informazioni sulle ultime versioni del DEV-C++ e della relativa documentazione e da
dove si può scaricare il materiale;
Il Dev C++ è stato fondato originariamente dal programmatore Colin Laplace e la sua compagnia, Bloodshed Software, utilizza il famoso compilatore gcc (Gnu Compiler Collection
http://gcc.gnu.org/) che è stato scritto per Unix. Per farlo funzionare sul sistema operativo
Windows utilizza mingw (http://www.mingw.org/) una altro progetto open source che ha come scopo il passaggio del software scritto per Linux sul sistema Windows.
Per la documentazione relativa ad alcune delle funzioni implementate nel compilatore si veda
http://www.gnu.org/software/libc/manual.
Lo sviluppo del Dev-C++ è stato abbandonato nel 2005 e l’ultima versione è la 4.9.9.2 che è
una beta e riscontra qualche problema di incompatibilità con Windows Vista che però può essere facilmente raggirato settando in modo appropriato il programma. Una caratteristica in più
di Dev-C++ rispetto agli altri sistemi di sviluppo è l’uso dei DevPaks, estensioni sull'ambiente
di programmazione con librerie addizionali, template e strumenti. I DevPaks solitamente contengono strumenti grafici, inclusi kit di sviluppo come GTK+, wxWidgets, e FLTK. Altri DevPaks includono librerie per usi delle funzioni più avanzate.
Il progetto è stato ripreso da un team di sviluppo che lo ha migliorato aggiungendo le funzionalità di wxWidgets per la creazione rapida di applicazioni con interfaccia grafica. La nuova
versione è stata denominata wxDev-C++ e si può scaricare liberamente all’indirizzo:
http://wxdsgn.sourceforge.net/
Olimpiadi dell’informatica
http://www.olimpiadi-informatica.it/ sito ufficiale dell’AICA che gestisce in Italia le olimpiadi internazionali dell’informatica. Si possono trovare tutte le tracce delle prove assegnate nelle
edizioni precedenti.
http://allenamenti.olimpiadi-informatica.it/ un sito contenente la possibilità di svolgere un test
on line con correzione automatica, le soluzioni di molti dei problemi dati alle selezioni regionali, materiale didattico vario.
157
Corso sul Linguaggio C++ Edizione 2010
Siti per programmatori
http://www.cpiupiu.tk/ In italiano, link a risorse disponibile + esercizi vari
Guide on line
http://www.webmasterpoint.org/c/home.asp
http://www.hyperbook.it/c_book/c2.htm
Hyperbook della Mc Grow Hill (bisogna prima
registrarsi)
http://alpha.science.unitn.it/~fiorella/guidac/indexc.html
http://www.tuttogratis.it/costume_e_societa/manuali_c_e_c_gratis.html
http://www.cppreference.com/ Un guida al linguaggio C++ on line gratuita in inglese
http://msdn.microsoft.com/ La MSDN della Microsoft contiene molte informazioni sul lin
guaggio C++, purtroppo in lingua inglese;
http://en.wikipedia.org/wiki/C%2B%2B_Standard_Library anche su wikipedia si può trovare
un manuale di riferimento per le librerie standard del C++, purtroppo solo in inglese;
http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html un elenco
completo delle funzioni delle librerie g++, in inglese;
Link per documentazione
http://www.cplusplus.com/
Un sito contenente un completo manuale di riferimento per il linguaggio C e C++ (tutte le funzioni,
I tipi di dati e gli oggetti definiti delle librerie), con
la possibilità di effettuare ricerche e tutorial (purtroppo in inglese).
http://www.manuali.it/manuali.asp?cr=51 Un elenco di manuali per il C
http://pensareincpp.altervista.org/
Il sito di un progetto per la traduzione di un manuale gratuito inglese (solo 1° Vol. Pensare in C)
www.bruceeckel.com
Sito dove si trova l’originale inglese di 2 vol.
http://ennebi.solira.org/
Sito del Prof. Nunzio Brugaletta, ITC di Ragusa
9.3
Tabelle presenti nel testo
Tabella 1. Sequenze di escape.................................................................................................... 7
Tabella 2. Gli operatori disponibili nel linguaggio C++ raggruppati in ordine di precedenza da
quelli con precedenza maggiore fino a quelli con precedenza minore (le linee più marcate
segnano la divisione tra un gruppo e l’altro).................................................................... 28
Tabella 3 Elenco di alcune funzioni e costanti definite nelle librerie standard del C e utilizzate
in questo libro................................................................................................................... 63
Tabella 4 Elenco dei codici di conversione utilizzati in printf ................................................ 89
158
Scarica