corso 4 C++ - contiriccardo.it

Transcript

corso 4 C++ - contiriccardo.it
Le Basi della Programmazione
Linguaggio C++
Le basi
1
Le Basi della Programmazione
2
1. Linguaggio C++, la storia
Nel 1972 Dennis Ritchie progettava e realizzava, presso i Bell Laboratories, la prima versione del
linguaggio C. Ritchie aveva ripreso e sviluppato molti dei costrutti sintattici utilizzati nella costruzione
del sistema operativo UNIX da Ken Thompson.
Successivamente gli stessi Thompson e Ritchie riscrissero in C il codice di UNIX. Da allora il C ha
subito pochissime trasformazioni, mantenendosi un linguaggio di alto livello che possiede un ristretto
insieme di costrutti e di parole chiave, ma con una straordinaria forza espressiva. Il C consente di
programmare in maniera modulare, utilizzando macro e funzioni, di interagire direttamente con
funzioni tipiche del basso livello come ad esempio l’indirizzamento assoluto di memoria. Per
l’eleganza della sua sintassi e la compattezza dei costrutti, il C è una sfida permanente alle capacità
intellettuali del programmatore.
Quando nella prima metà degli anni Ottanta, nella teoria della programmazione si sviluppano le basi
per la OOP (Object Oriented Programming, programmazione orientata agli oggetti) si capisce presto
che quella sarà la chiave di volta per lo sviluppo di applicazioni general-purpose. Ecco allora che il
danese Bjarne Stroustrup propone nel 1983 un nuovo linguaggio denominato ”C con classi” e
successivamente C++. Mantenendo una compatibilità quasi assoluta con il C, il C++ è un linguaggio
Object Oriented che diventerà lo standard de facto per la programmazione di applicativi nel ventennio a
seguire.
Il C++ rappresenta un linguaggio completamente autonomo rispetto al C, pur utilizzandone
sostanzialmente la sintassi. In particolare, l’introduzione di costrutti quali i template e le classi rende il
C++ un linguaggio multi paradigma, principalmente quello a oggetti. Ciò che ha sempre differenziato il
C e il C++ da altri linguaggi ”artificiali” è il fatto di essere stati creati da dei programmatori che
dovevano fronteggiare particolari esigenze quali la codifica dei programmi e l’organizzazione del
codice e non da un gruppo di ricerca appositamente creato allo scopo.
Questo ha portato un sacco di programmatori a inviare commenti, suggerimenti e migliorie a Ritchie e
Stroustrup nei primi anni dello sviluppo dei due linguaggi ottenendo un prodotto sicuramente più
”malleabile” e adatto alle necessità.
Le Basi della Programmazione
3
I paradigmi di programmazione
Quando si utilizzavano sistemi operativi a linea di comando, come UNIX e il DOS, l’unica
programmazione possibile era basata sul paradigma imperativo. Il paradigma imperativo (da imperio,
comando) è fondato sull’esplicita richiesta di eseguire un comando.
In tale contesto il programmatore dovrà privilegiare l’esame del processo di calcolo in quanto precisa
sequenza di azioni da svolgere. Le strutture di controllo assumono la forma di istruzioni di flusso
(GOTO, FOR, IF/THEN/ELSE, ecc.) e il calcolo procede per iterazione piuttosto che per ricorsione. I
valori delle variabili sono spesso assegnati a partire da costanti o da altre variabili (assegnamento) e
raramente per passaggio di parametri (istanziazione).
La programmazione strutturata è una tecnica il cui scopo è di limitare la complessità della struttura del
controllo dei programmi. Il programmatore è vincolato ad usare solo le strutture di controllo canoniche
definite dal Teorema di Bohm-Jacopini, ovvero la sequenza, la selezione e il ciclo, evitando le
istruzioni di salto incondizionato.
La programmazione orientata agli oggetti (OOP) si è dimostrata la più adeguata nello sviluppo di
sistemi software complessi. In questo paradigma il concetto principale è quello di oggetto in quanto
metafora di un concetto del mondo reale. In tale contesto il programmatore interpreta lo svolgersi delle
azioni come una sequenza di interazioni fra oggetti, quindi come un flusso (più) semplice da
interpretare e sviluppare.
La programmazione guidata dagli eventi (EDP, event driven programming) perde la sequenza logica
precisa descritta dai precedenti paradigmi, ma il flusso delle azioni viene guidato dagli eventi che il
programma intercetta. Evento è l’interazione del mondo esterno con il programma, quindi la pressione
di un tasto sull’interfaccia grafica da parte dell’utente, come la stampante che segnala di aver finito la
carta o l’inchiostro.
Linguaggi compilati e linguaggi interpretati
In informatica, un linguaggio di programmazione è un linguaggio formale, dotato di una sintassi e di
una semantica ben definite, utilizzabile per il controllo del comportamento di una macchina formale.
L’utilità dei linguaggi di programmazione sta nel fatto che le macchine formali sono descrizioni
teoriche di strumenti implementati tipicamente come microcontrollori o microprocessori.
Sostanzialmente quindi, un linguaggio di programmazione è lo strumento che ci permette di creare il
software adatto a controllare strumenti governati da processori. Se si considera infine che si stima che
ogni strumento elettronico attualmente in commercio dal valore superiore alle 50 euro contenga un
Le Basi della Programmazione
4
processore, si capisce subito l’importanza dei linguaggi di programmazione nella definizione della
tecnologia moderna.
Programmare in un dato linguaggio di programmazione significa generalmente scrivere uno o più
semplici file di testo chiamato codice sorgente, seguendo sintassi e semantica del linguaggio scelto. I
linguaggi di programmazione si dividono dunque in due grandi classi di linguaggi:
1. linguaggi compilati
2. linguaggi interpretati.
I linguaggi compilati sono quelli che sottopongono il codice sorgente a compilazione. Questa è
l’operazione di traduzione del codice sorgente dal linguaggio comprensibile all’uomo, quello stabilito
dalla sintassi del linguaggio scelto, al linguaggio comprensibile dal processore, definito linguaggio
macchina. Il risultato della compilazione viene solitamente definito codice oggetto. Per fare questo
ogni linguaggio compilato ha bisogno di un suo proprio compilatore: questo è genericamente un
programma che si occupa della traduzione descritta.
La compilazione crea dunque un legame fra il codice oggetto prodotto e il processore che dovrà
eseguirlo. Questo crea importanti vantaggi, come la velocità di esecuzione del codice e l’efficienza
nella gestione della memoria, ma anche svantaggi come una difficile portabilità (su altre architetture)
del codice oggetto e a volte anche del codice sorgente.
Esempi di linguaggi compilati sono il C, C++, Pascal e Fortran.
I linguaggi interpretati si propongono di eliminare il problema della portabilità. Il codice sorgente in
questi casi non viene più compilato per la creazione del codice oggetto, ma coincide esattamente con
esso! Viene infatti interpretato in fase di esecuzione riga per riga e tradotto ogni volta nella
corrispondente operazione da eseguire. In questo caso, i linguaggi interpretati necessitano dunque di un
interprete che interpreti e faccia eseguire il codice.
L’interpretazione in teoria libera dalla dipendenza dalla piattaforma: sarà sufficiente avere un interprete
adatto ad ogni piattaforma per poter eseguire il codice ovunque. D’altro canto però si assiste ad un
grosso decadimento delle prestazioni in fase di esecuzione, per il doppio lavoro di dover interpretare ed
eseguire in real-time.
Alcuni linguaggi (java, C#) cercano di mitigare questo aspetto negativo dell’interpretazione
mantenendo i suoi aspetti positivi, fornendo una sorta di semi-compilazione preventiva che fornisce un
linguaggio intermedio (bytecode) facilmente interpretabile.
Esempi di linguaggi interpretati sono il Basic, il Perl, il Python e in generale tutti i linguaggi di
scripting.
Le Basi della Programmazione
5
Valutare un linguaggio di programmazione
Non ha senso, in generale, parlare di linguaggi migliori o peggiori, o di linguaggi migliori in assoluto:
ogni linguaggio nasce per affrontare una classe di problemi più o meno ampia, in un certo modo e in un
certo ambito. Però, dovendo dire se un dato linguaggio sia adatto o no per un certo uso, è necessario
valutare le caratteristiche dei vari linguaggi:
Caratteristiche intrinseche
Sono le qualità del linguaggio in sé, determinate dalla sua sintassi e dalla sua architettura interna.
Influenzano direttamente il lavoro del programmatore, condizionandolo. Non dipendono né dagli
strumenti usati (compilatore/interprete, IDE, linker) né dal sistema operativo o dal tipo di macchina.
•
Espressività: la facilità e la semplicità con cui si può scrivere un dato algoritmo in un dato
linguaggio.
•
Didattica: la semplicità del linguaggio e la rapidità con cui lo si può imparare.
•
Leggibilità: la facilità con cui, leggendo un codice sorgente, si può capire cosa fa e come
funziona. La leggibilità dipende non solo dal linguaggio ma anche dallo stile di
programmazione di chi ha creato il programma.
•
Robustezza: la capacità del linguaggio di prevenire, nei limiti del possibile, gli errori di
programmazione.
•
Modularità: quando un linguaggio facilita la scrittura di parti di programma indipendenti
(moduli) viene definito modulare. In genere la modularità si ottiene con l’uso di
sottoprogrammi (subroutine, procedure, funzioni) e con la programmazione ad oggetti.
•
Flessibilità: la possibilità di adattare il linguaggio, estendendolo con la definizione di nuovi
comandi e nuovi operatori.
•
Generalità: la facilità con cui il linguaggio si presta a codificare algoritmi e soluzioni di
problemi in campi diversi.
•
Efficienza: la velocità di esecuzione e l’uso oculato delle risorse del sistema su cui il
programma finito gira.
•
Coerenza: l’applicazione dei principi base di un linguaggio in modo uniforme in tutte le sue
parti.
Le Basi della Programmazione
6
Caratteristiche esterne
Oltre alle accennate qualità dei linguaggi, possono essere esaminate quelle degli ambienti in cui
operano. Un programmatore lavora con strumenti software, la cui qualità e produttività dipende da un
insieme di fattori che vanno pesati anch’essi in funzione del tipo di programmi che si intende scrivere.
•
Diffusione: il numero di programmatori nel mondo che usa il tale linguaggio.
•
Standardizzazione: L’esistenza di alcuni dialetti del linguaggio frammenta la comunità di
programmatori, limitandone l’utilità e la diffusione generale.
•
Integrabilità: dovendo scrivere programmi di una certa dimensione, è molto facile trovarsi a
dover integrare parti di codice precedente scritte in altri linguaggi: se un dato linguaggio di
programmazione consente di farlo facilmente, magari attraverso delle procedure standard,
questo è decisamente un punto a suo favore.
•
Portabilità: la possibilità che portando il codice scritto su una certa piattaforma su un’altra,
questo funzioni subito, senza doverlo modificare.
Le Basi della Programmazione
7
2. Tecniche di programmazione
Un programmatore, inteso come “creatore di software”, deve essere in grado di occuparsi di un po' tutte
le fasi del ciclo di vita del software.
Questa ultima espressione si riferisce al modo in cui si scompone l'attività di progettazione, creazione,
test e manutenzione di un software in sotto attività coordinate fra loro.
Questo concetto è fondamentale in programmazione e segna la sua comparsa del mondo informatico
segna la definitiva trasformazione della programmazione da attività “artigianale”, affidata alla
creatività dei singoli individui, ad attività “scientifica”, con un forte accento sul processo di creazione
come strumento di controllo del software.
Esistono numerosi modelli su cui basare il ciclo di vita di un software, che prevedono comunque la
scomposizione del processo di sviluppo in attività analoghe. Le distinzioni fra questi si basano
soprattutto su:
• l'enfasi relativa che si attribuisce a ciascuna attività;
• l'individuazione degli attori specifici incaricati di ciascuna attività;
• l'ordine in cui le attività si svolgono.
Le principali attività costituenti il processo di sviluppo sono:
• Analisi, ovvero l'indagine preliminare sul contesto in cui il prodotto software deve inserirsi,
sulle caratteristiche che deve esibire, ed eventualmente su costi e aspetti logistici della sua
realizzazione.
Al termine della fase verrà creato un documento che descrive le caratteristiche del sistema, tale
documento viene definito "documento di Specifica".
• Progettazione, in cui si definiscono le linee essenziali della struttura del sistema da realizzare,
in funzione dei requisiti evidenziati dall'analisi e dal documento finale da essa creato.
In questa fase sarà sviluppato un documento che permetterà di avere una definizione della
struttura di massima (architettura di alto livello) e una definizione delle caratteristiche dei
singoli componenti (moduli).
• Implementazione, ovvero la sua realizzazione concreta; questa tipicamente consiste nella
realizzazione di uno o più programmi in un determinato linguaggio di programmazione.
Le Basi della Programmazione
8
• Collaudo, volta a misurare in che modo il sistema realizzato soddisfa i requisiti stabiliti nella
fase di analisi, ovvero a valutarne la correttezza rispetto alle specifiche.
Le tipologie specifiche di test (prove) si possono inoltre distinguere in funzione dei particolari
aspetti dei moduli o del sistema che vengono valutati; si parla per esempio di test funzionali,
test di performance, test di accettazione, test d'installazione.
• Manutenzione, che comprende tutte le attività di modifica del software successive al suo
rilascio presso il cliente o la sua immissione sul mercato.
Queste attività possono essere volte a correggere errori del software, adattarlo a nuovi ambienti
operativi, o estenderne le funzionalità. La manutenzione incide sui costi tanto che si stima che il
60% di questo dipenda dalla manutenzione.
Ogni modifica al software comporta necessariamente la necessità di nuovi test, sia relativi alle
nuove funzionalità eventualmente introdotte, sia mirati a verificare che le modifiche apportate
non abbiano compromesso funzionalità preesistenti (test di regressione).
Vediamo con un migliore dettaglio come sviluppare queste fasi in modo da ottenere un'organizzazione
il più possibile precisa del lavoro da svolgere, lasciando comunque ad ognuno la possibilità di
“personalizzare” il lavoro, mettendo l'accento su uno degli aspetti, a seconda delle inclinazioni proprie
e delle difficoltà del progetto.
Le Basi della Programmazione
9
La fase di Analisi
Prevede l'analisi dei requisiti, uno studio del problema il cui obiettivo è stabilire se vale la pena (da un
punto di vista tecnico ed economico) realizzare il sistema del quale si vanno definendo i requisiti.
Ovviamente in un progetto scolastico o in un progetto personale questa fase è autoreferenziale.
I requisiti del problema vanno individuati mettendo in evidenza l'obiettivo finale del processo stesso: in
questa fase vanno individuate le variabili di input e di output, il linguaggio di programmazione più
adatto a risolvere il problema e l'obiettivo finale del progetto software.
La fase di Progettazione
In questa fase si scompone il problema in tutti i suoi sotto-problemi (moduli) secondo uno sviluppo
top-down, si definisce la gerarchia e la interdipendenza fra questi e se ne discute la soluzione,
supportandola con esempi, modelli matematici, diagrammi di flusso.
Obiettivo della fase è definire l'algoritmo risolutivo del problema.
Strategie Risolutive
Elaborare un algoritmo non è una operazione semplice ed è sempre diversa per ognuno dei problemi
che ci si trova ad affrontare. Spesso però le tecniche di risoluzione, come i modi di ragionare delle
persone, tendono a seguire determinati schemi, consolidati con il tempo. Eccone alcuni:
Sfruttare L'esperienza
“Gioco dello Scarabeo numerico”
Lo scarabeo numerico si gioca con 9 carte numerate da 1 a 9, poste sul tavolo scoperte. I due giocatori
devono prendere, alternativamente, una carta dal tavolo. Vince chi per primo riesce a totalizzare 15 con
tre carte.
Cercate di definire una strategia di gioco che vi consenta di vincere, o quanto meno di non perdere.
(saper giocare a tris potrebbe essere utile!)
Le Basi della Programmazione
10
Procedere a ritroso
“Gioco dei 24 dadi”
Si hanno a disposizione 24 dadi di cui uno, ma non si sa quale, è leggermente più pesante di tutti gli
altri.
Utilizzando una bilancia a due piatti identificare il dado più pesante in non più di 3 pesate
“Gioco del 100”
Si gioca in due, uno per volta, partendo da zero e aggiungendo almeno 1 al totale e al massimo 10.
Vince chi dice 100.
Algebra
“Zio e nipote”
Augusto ha il triplo degli anni che aveva sua nipote Clarabella 10 anni fa, e Clarabella, ora, ha la metà
degli anni che suo zio Augusto avrà fra 5 anni.
Che differenza di età c'è tra zio e nipote?
La fase di Implementazione
In questa fase si passa alla realizzazione pratica dell'algoritmo progettato, implementandolo nel
linguaggio di programmazione scelto nella fase di analisi.
Per ogni difficoltà implementativa incontrata in questo livello è necessario interrompere la fase e
tornare alla precedente fase di progettazione, risolvere l'ostacolo, rielaborare l'algoritmo risolutivo e
procedere di nuovo alla sua implementazione.
La fase termina con la realizzazione di un software funzionante che risolve il problema individuato
nella fase di analisi.
Le Basi della Programmazione
11
3. C++, primi elementi
La struttura generale del codice e l’organizzazione dei programmi nel linguaggio C++, è perfettamente
compatibile con quella del C ed è descrivibile tramite un semplice schema:
•
DIRETTIVE AL PREPROCESSORE
•
DICHIARAZIONE VARIABILI GLOBALI
•
PROGRAMMA PRINCIPALE (main)
•
FUNZIONE 1
•
FUNZIONE 2
•
...
•
FUNZIONE n
Di tutte le sezioni componenti la struttura di un programma, è comunque doveroso notare che solo una
è obbligatoria e cioè la dichiarazione di un programma principale. Tutte le altre possono essere omesse.
Le direttive al preprocessore C sono istruzioni di ”pre-elaborazione” del codice sorgente, che
permettono un’organizzazione dinamica del codice. Sono semplicemente distinguibili da ogni altra
istruzione perché ogni direttiva inizia sempre con un ”#”.
Le direttive più comuni sono la ”#include” che serve a dichiarare l’utilizzo di una determinata libreria
e la direttiva ”#define” che serve a dichiarare una sostituzione di testo.
Ogni programma scritto in C++ è strutturato come una serie di chiamate a funzione. Non c’è codice
esecutivo fuori da una delle funzioni, che sono l’ambiente predefinito per le operazioni. La funzione
main è semplicemente la funzione che viene chiamata per prima.
A questa viene dunque affidato il compito di contenere tutte le chiamate alle altre funzioni disponibili.
La sintassi generale di una funzione è della forma:
Le Basi della Programmazione
12
<tipo restituito> <nome funzione> ( <elenco parametri> )
{
<istruzione 1> ;
<istruzione 2> ;
... ;
}
dove:
•
”tipo restituito” indica il tipo del valore di uscita della funzione,
•
”elenco parametri” rappresenta l’insieme dei parametri che la funzione accetta in ingresso.
Il linguaggio C++ prevede un insieme di categorie lessicali chiaramente distinguibili fra loro:
•
commenti
•
identificatori
•
parole chiave
•
costanti letterali
•
punteggiatura e operatori
I commenti sono una parte fondamentali di ogni buon programma in qualsiasi linguaggio di
programmazione!! Infatti i commenti sono parti inserite direttamente nel codice, ma ignorate dal
compilatore. Questo permette dunque di ”commentare” il codice nella nostra lingua, descrivendo le
operazioni effettuate. Sono importanti anche nell’ottica di permettere ad altri di leggere il nostro codice
o di ricordarne a noi stessi la funzionalità.
In C++ sono possibili due tipi di commenti:
•
il commento di riga
// questo commento termina quando si va a capo
•
il commento lungo
/* questo commento inizia qui
e termina quando qui */
Le Basi della Programmazione
13
Identificatore è un nome generico utilizzato per descrivere assieme variabili, costanti, tipi, funzioni e
macro. Ognuna di queste categorie rappresenta una caratteristica del linguaggio.
Ogni identificatore deve iniziare con una lettera o con un underscore (’_’) e non può contenere spazi.
Tutti gli identificatori presenti in un programma devono essere diversi, indipendentemente dalla
categoria cui appartengono.
Tipi di dato fondamentali
Ogni identificatore che un programma intende utilizzare deve essere prima dichiarato. Per le variabili
abbiamo la seguente sintassi:
<qualificatore> <tipo> <identificatore>;
dove:
•
qualificatore è un parametro opzionale che descrive il tipo e può assumere i valori: signed,
unsigned, short, long.
•
tipo rappresenta (appunto) il tipo di dato memorizzato.
In C++ i tipi fondamentali si dividono in due categorie: semplici e strutturati. I tipi semplici, gli unici
che ci interessano per adesso, sono:
Nome
Descrizione
int
Comprende i numeri interi relativi compresi in un certo intervallo, che dipende
dalle caratteristiche fisiche della macchina ospite.
(a 32 bit, circa 8 miliardi fra positivi e negativi)
char
Comprende i numeri interi da 0 a 255. Tipicamente questo numero viene
rappresentato per` come un carattere in base all’utilizzo del codice ASCII.
float
Comprende i numeri reali, rappresentati in virgola mobile con precisione singola
(a 32 bit, 7 cifre).
Le Basi della Programmazione
Nome
14
Descrizione
double
Comprende i numeri reali, rappresentati in virgola mobile con precisione doppia
(a 32 bit, 16 cifre).
bool
Rappresenta i valori logici true (1) e false (0).
enum
Rappresenta una sequenza di interi come una sequenza di identificatori costanti.
I tipi strutturati si dicono così perché non sono tipi definiti autonomamente come i tipi semplici, ma
sono piuttosto strutture di dati creati a partire da altre già esistenti. I tipi strutturati saranno comunque
approfonditi più avanti nel corso degli studi. Le categorie di tipi strutturati comprendono:
Nome
Descrizione
array
Sono variabili in grado di ospitare un numero predefinito di valori omogenei. Ad
esempio la variabile array ”nomeMesi” potrebbe essere un array contenente 12
stringhe ognuna corrispondente al nome di un mese.
struct
Sono variabili in grado di ospitare un numero predefinito di valori NON omogenei
fra loro, ma solitamente unite da caratteristiche comuni. Ad esempio la variabile
struct ”persona” potrebbe contenere nome, cognome, data di nascita, peso, altezza.
union
Sono variabili in grado di memorizzare nella stessa variabile tipi differenti in
istanti differenti. Ad esempio la variabile ”temperatura” potrebbe essere una union
contenente un valore float che indica la temperatura oppure il valore ”NULL” ad
indicare una temperatura non misurata.
Stringhe e Caratteri
La gestione di caratteri e stringhe (sequenze di caratteri, cioè parole) è uno degli argomenti peculiari
del linguaggio C++ e una delle cose in cui si discosta maggiormente dal genitore C.
Le Basi della Programmazione
15
Sostanzialmente però, rimane anche la possibilità di gestire le stringhe come in C, eliminando di fatto
ogni eventuale problema di compatibilità fra i due approcci.
Essendo però le stringhe solo un insieme di caratteri, sarà utile introdurle successivamente a questi. I
caratteri in C++ sono gestiti tramite una variabile di tipo char che abbiamo detto contenere un numero
intero fra 0 e 255. La corrispondenza numero - carattere viene affidata alla cosiddetta tabella dei codici
ASCII (American Standard Code for Information Interchange), che vediamo qui rappresentata:
Come visto all’interno di questa troviamo una serie di caratteri speciali, visualizzabili, ma non
descrivibili direttamente. Per esempio, sappiamo che per andare a capo in un testo digitato, basta
premere il tasto INVIO, ma come è possibile indicare questo carattere? Per tutti i caratteri speciali
come questo esiste un insieme di shorcut (scorciatoie, combinazioni di tasti) speciali, definite sequenze
di escape:
Le Basi della Programmazione
16
Sequenza di escape
Descrizione
\n
New line
\t
tab
\a
beep
\f
Form feed
\r
Carriage return
\v
Vertical tab
\b
backspace
\\
backslash
\'
apex
\''
Double apex
\?
Question mark
\0
End string
Una qualsiasi sequenza di caratteri, lettere, numeri, spazi e sequenze di escape, può essere inclusa fra
doppi apici ( ” ) a formare una stringa costante. Ad esempio, la stringa
"ciao, Mondo!\n"
indica la scritta ciao, Mondo! con alla fine un carattere newline, lo stesso effetto che si otterrebbe
premendo INVIO su un testo digitato.
Le Basi della Programmazione
17
Operatori
Un operatore è un simbolo che opera su una o più espressioni, producendo un valore che può essere
assegnato ad una variabile. In C++ sono disponibili nativamente i seguenti insiemi di operatori:
Operatori Aritmetici
Operatore
Descrizione
+
Addizione
-
Sottrazione
*
Moltiplicazione
/
Divisione
%
Modulo
(resto divisione intera)
++
Incremento
--
Decremento
Esempio
x=2
x+2
x=2
5-x
x=4
x*5
15/5
5/2
5%2
10%8
10%2
x=5
x++
x=5
x--
Risultato
4
3
20
3
2.5
1
2
0
x=6
x=4
Bisogna notare che la divisione funziona diversamente a seconda del tipo. Cioè se a=5 e b=2 sono
variabili intere, il risultato di a/b sarà l’intero 2 (troncamento della parte decimale), mentre se sono
variabili reali (float), il risultato sarà il reale 2.5.
Nel caso di una operazione fra un reale e un intero il risultato è sempre un reale.
Le Basi della Programmazione
18
Operatori di Assegnazione
Operatore
=
+=
-=
*=
/=
.=
%=
Esempio
x=y
x+=y
x-=y
x*=y
x/=y
x.=y
x%=y
È come fare
x=y
x=x+y
x=x-y
x=x*y
x=x/y
x=x.y
x=x%y
Operatori di confronto
Operatore
==
!=
<>
>
<
>=
<=
Descrizione
È uguale a
È diverso da
É diverso da
È maggiore di
È minore di
È maggiore o uguale a
È minore o uguale a
Esempio
5==8 restituisce false
5!=8 restituisce true
5<>8 restituisce true
5>8 restituisce false
5<8 restituisce true
5>=8 restituisce false
5<=8 restituisce true
Operatori Logici
Operatore
&&
Descrizione
and
||
or
^
not
Esempio
x=6
y=3
(x < 10 && y > 1) restituisce true
x=6
y=3
(x==5 || y==5) restituisce false
x=6
y=3
!(x==y) restituisce true
Esistono inoltre alcuni operatori meno comuni che saranno approfonditi in caso di necessità:
Le Basi della Programmazione
19
Operatori sui bit
Operatore
&&
||
^
>>
<<
Descrizione
AND logico bit a bit
XOR bit a bit
Complemento a uno
Shift destro
Shift sinistro
Operatori speciali
Operatore
,
?:
Descrizione
Esempio
Comma è un operatore binario e associativo da sinistra a c = (a = 5, b = 2, a + b)
destra che fornisce come risultato il valore dell’ultima
assegna a c il valore 7.
espressione riportata a destra dell’ultima virgola ottenuto
con i risultati parziali delle espressione di sinistra.
Question & colon è un operatore ternario con sintassi
< EsprLogica >? < Espr1 >:<Espr2 >
che valuta Espr1 se EsprLogica è vera, Espr2 altrimenti.
a = (b > 0?1 : 2)
assegnerà ad a il valore 1 se b
è maggiore di zero, 2
altrimenti.
Le Basi della Programmazione
20
Esercizi
1. Ciao, Mondo!
2. Programma per calcolare l’area e il perimetro di un rettangolo di base = 4 e altezza = 6.
3. Programma che permette l'inserimento, da parte dell'utente, di un numero intero e che lo
visualizza.
4. Programma che permette l'inserimento, da parte dell'utente, di un numero reale e che lo
visualizza.
5. Programma che permette l'inserimento, da parte dell'utente, di un carattere e che lo visualizza.
6. Programma che permette l'inserimento, da parte dell'utente, di una stringa e che la visualizza.
7. Programma che visualizza il numero intero int n = 5 e va a capo, il carattere char c = 'b' e va a
capo, il numero reale float r = 3.14 e va a capo.
8. Programma per calcolare l’area e il perimetro di un rettangolo con i valori di base e altezza
inseriti dall'utente.
9. Lavorare coi parametri della funzione main().
10. Calcolo dello spazio occupato da ognuno dei tipi fondamentali semplici, tramite l’operatore
sizeof.
11. La libreria climits e i limiti superiore e inferiore dei tipi interi.
12. Dati tre numeri interi, considerarli come hh:mm:ss di un orario determinato. Calcolare i secondi
trascorsi dalla mezzanotte del giorno prima.
13. Sapendo che in un parcheggio la prima ora costa 1.5 mentre tutte lo successive costano 1 e,
scrivere un programma che richieda il numero complessivo delle ore e visualizzi il totale da
pagare.
Le Basi della Programmazione
21
4. Le strutture di controllo
Il lavoro di creazione e organizzazione dell’algoritmo risolutivo di un certo problema si può descrivere,
in buona approssimazione, con la stesura di una serie di operazioni da compiere per ottenere il risultato
prefissato. Durante questo lavoro, vanno tenute in considerazioni anche variabili come il linguaggio
implementativo scelto, il sistema che ospiterà il programma finale e ogni variabile contingente. È ovvio
dunque che la stesura dell’algoritmo un’operazione preliminare alla stesura del codice e andrebbero
sempre ben distinte.
La schematizzazione sistematica delle operazioni da svolgere per le risoluzione del problema è il
procedimento che viene definito programmazione strutturata. Obiettivo di questa tecnica di
programmazione è quello di organizzare in strutture più semplici ogni algoritmo risolutivo. In un
algoritmo le istruzioni possono essere organizzate in:
1. sequenza, in cui le istruzioni sono eseguite una dopo l’altra
2. alternanza, in cui alcune istruzioni sono eseguite alternativamente (o un gruppo o un altro)
3. ripetizione, in cui alcune istruzioni sono ripetute un numero finito di volte.
Accanto a queste semplici strutture possono esserne inserite altre più generali o più particolari, per
schematizzare ogni algoritmo secondo i criteri che più si adattano ai propri gusti. Queste tre sono state
scelte perchè vale il seguente teorema (Jacopini-Bohm, 1966)
“Qualsiasi algoritmo può essere riscritto in maniera equivalente
utilizzando solo le strutture di sequenza, alternanza, ripetizione.”
Questo algoritmo cioè sostanzialmente afferma che le tre strutture indicate sono i mattoni fondamentali
per riprodurre qualsiasi algoritmo formulabile dall’uomo. In altre parole, tutti i problemi che sono
risolvibile, hanno anche una soluzione costruita semplicemente con istruzioni in sequenza, alternanza e
ripetizione.
Le Basi della Programmazione
22
Struttura alternativa
La struttura alternativa viene rappresentata in C++ tramite lo schema:
if CONDIZIONE
ISTRUZIONI cond VERA
[else
ISTRUZIONI cond FALSA]
ove ovviamente il primo gruppo di istruzioni viene eseguito solo se la condizione è vera, mentre il
secondo solo se la condizione è falsa. Le istruzioni rappresentano sempre un blocco e vanno quindi
racchiuse fra parentesi graffe { e }. La condizione è una espressione booleana di cui viene valutata la
veridicità.
Le parentesi quadre nello schema stanno invece
ad indicare che la seconda parte dell’istruzione `
opzionale, cioè si potrebbe creare semplicemente
una struttura del tipo
if CONDIZIONE
ISTRUZIONI
in modo da eseguire un blocco di istruzioni solo
nel caso che una condizione si verifichi.
Le Basi della Programmazione
23
Proviamo con un esempio a prendere pratica con le strutture illustrate. Immaginiamo di dover risolvere
il seguente problema: dati due numeri dall’utente, disporli in ordine crescente; Il seguente codice lo
risolve.
int primo, secondo, min, max;
cout << "inserire primo numero: ";
cin >> primo;
cout << "inserire secondo numero: ";
cin >> secondo;
if (primo < secondo)
{
min = primo;
max = secondo;
}
else
{
min = secondo;
max = primo;
}
cout << "minore: " << min << endl;
cout << "maggiore: " << max << endl;
Le Basi della Programmazione
Va notato anche che l’istruzione IF.. ELSE può
essere ripetuta dentro ad un’altra, producendo una
nidificazione e in questo caso si potrebbe perdere il
legame fra IF e l’ELSE correlato. In questo caso
vale la regola che ogni ELSE si riferisce sempre al
più vicino IF sovrastante.
Nell’esempio seguente l’indentazione chiarifica i
legami fra i vari IF.. ELSE.
if CONDIZIONE
if CONDIZIONE
ISTRUZIONI
else
ISTRUZIONI
else
ISTRUZIONI
24
Le Basi della Programmazione
25
Struttura alternativa multipla
Capita a volte di dover tener testa non a due semplici possibilità alternative, ma ad una serie di
comportamenti in risposta agli eventi. E’ questo il caso in cui è possibile utilizzare una struttura
alternativa multipla, la cui sintassi viene qui mostrata:
switch (VARIABILE)
{
case VALORE-1:
ISTRUZIONI-1;
break;
case VALORE-2:
ISTRUZIONI-2;
break;
...
default:
ISTRUZIONI;
}
Importante notare che la struttura switch funziona solo con variabili intere ed è assolutamente analoga
ad una serie di IF nidificati che hanno nella condizione il controllo sulla stessa variabile.
Anche qui vediamo di illustrare un esempio: dato un numero intero da 1 a 12, visualizzare il mese
corrispondente. Vediamo il codice:
int mese;
cout << "inserisci numero (da 1 a 12): ";
cin >> mese;
switch(mese)
{
case 1:
cout << “Gennaio”;
break;
case 2:
cout << “Febbraio”;
Le Basi della Programmazione
26
break;
...
case 12:
cout << “Dicembre”;
break;
default:
cout << “E questo? Che mese è?”;
}
L’istruzione break interrompe lo switch e in generale provoca l’uscita da un blocco, portando la linea
da eseguire ad essere la prossima dopo una parentesi graffa chiusa. In caso di assenza lo switch esegue
dal punto di ingresso tutte le istruzioni rimanenti nel blocco.
Esercizi sulla struttura alternativa
1. Dato un numero, inserito dall’utente, verificare se è pari o dispari
2. Scambiare due valori fra loro (ordinare due valori)
3. Scrivere un programma che richieda in ingresso tre valori interi distinti e ne determini il
maggiore.
4. Scrivere un programma che richieda in ingresso QUATTRO valori interi distinti e ne determini
maggiore e minore
5. Ordinare tre valori
6. Soluzione equazione di secondo grado
7. Programma che visualizza un menù di scelta
8. Controllo se una data è valida (giorno rispetto al mese, se si vuole strafare, anno bisestile). PS:
un anno è bisestile se è divisibile per 4, ma non per 100. Oppure se è divisibile per 100 e per
400.
9. Programma che individua la più recente fra due date e calcola la differenza di tempo in secondi
fra le due.
Le Basi della Programmazione
27
Struttura iterativa
In questa parte ci occuperemo di tutte le istruzioni del linguaggio C++ che permettono di ripetere un
numero finito di volte un blocco di istruzioni. Il numero delle ripetizioni è solitamente controllato
tramite una condizione booleana.
Una caratteristica comune delle istruzioni seguenti è che tutte ripetono le istruzioni finché la
condizione risulta vera, mentre escono dal ciclo esecutivo quando questa diventa falsa.
Iterazioni PREcondizionali
Le iterazioni PREcondizionali sono semplicemente
quelle che prima di iniziare a eseguire le istruzioni
da ripetere controllano la verità della condizione
imposta. In questo caso se la condizione è falsa, il
ciclo non viene eseguito neanche una volta
while CONDIZIONE
ISTRUZIONI
Vediamo con un esempio di chiarire il concetto:
calcolare il prodotto di due numeri interi A, B
sommando A per B volte.
int A = 5, B = 7, prodotto = 0;
while ( B!=0)
{
prodotto += A;
B--;
};
// prodotto = prodotto + A;
// B = B - 1
Le Basi della Programmazione
Iterazioni POSTcondizionali
Le iterazioni POSTcondizionali sono quelle
iterazioni che prima eseguono una volta il blocco di
istruzioni da ripetere e poi controllano una
condizione per capire se bisogna iterare il
procedimento.
do
ISTRUZIONI
while CONDIZIONE
Questo tipo di iterazione non è molto comune e si
utilizza solo in determinati casi, come ad esempio
per controllare che un valore di input corrisponda ad
un certo range:
int A;
do
{
cout << "inserire un valore intero maggiore di ZERO: ";
cin >> A;
}
while( A < 0);
28
Le Basi della Programmazione
29
Iterazioni enumerative
Le iterazioni enumerative, o con contatore, sono una struttura derivata dalle iterazioni
PREcondizionali. Sono molto comuni perchè permettono di mettere in evidenza un contatore intero
nella condizione che indicherà il numero di ripetizioni da eseguire. E’ questa quindi la struttura da
utilizzare nel caso in cui sia noto a priori il numero di iterazioni.
for ( INIZIALIZZAZIONE, CONDIZIONE, INCREMENTO )
ISTRUZIONI
Vediamo un esempio: scrivere i primi 10 numeri interi
for( int i = 1; i <= 10; ++i)
{
cout << i << endl;
}
E' importante notare due cose di questa nuova struttura introdotta:
1. il ciclo for, pur utilizzando una sintassi propria, si comporta in maniera assolutamente identica
alla ripetizione pre-condizionale (il ciclo while). Questi due costrutti sono dunque
assolutamente interscambiabili
2. a differenza dei precedenti costrutti iterativi, il ciclo for utilizza molto comodamente come
contatore una variabile intera (nell'esempio precedente, la variabile intera i) definibile
direttamente al suo interno. La variabile così definita è una pura variabile di lavoro: esiste solo
durante il ciclo for di cui fa da contatore e “scompare” alla fine del ciclo
Le Basi della Programmazione
30
Esercizi sulla struttura iterativa
1. Visualizzare i primi 100 numeri.
2. Fare la somma dei primi 100 numeri e poi dei primi n numeri, con n inserito dall’utente.
3. Fare la somma di 10 numeri inseriti a scelta dall’utente
4. Calcolo dello “zero di macchina”
5. Programma per il calcolo del fattoriale di un numero dato e poi di tutti i fattoriali dei numeri
minori di quello.
6. Visualizzazione Tavola Pitagorica
7. Visualizzazione di un rettangolo con cornice “+” e interno “=” di misure A e B, decise
dall’utente
8. Predisporre un programma, che determini il maggiore, il minore e la media degli n valori
immessi dall’utente.
9. Divisori di un numero intero A, inserito dall’utente
10. Dato B, verificare se è primo
11. Programma che calcola tutti i numeri primi minori di X
12. Dato C, verificare se è perfetto e poi calcolare tutti i numeri perfetti minori di C.
13. Programma che disegna un rettangolo di caratteri di BASE e ALTEZZA scelti dall’utente. Il
rettangolo è formato da un carattere a scelta come cornice e da un carattere a scelta all’interno.
14. Scrivere un programma che, richiesti i coefficienti (a, b, c) di un’equazione di secondo grado,
determini il numero delle soluzioni possedute e i valori, se esistono, delle soluzioni.
15. Scrivere un programma che calcoli il podio di una gara con 10 corridori. I corridori sono
identificati da un numero di gara e hanno un tempo sul giro espresso in primi, secondi,
millesimi decisi da una funzione random.
Le Basi della Programmazione
31
5. Array
Gli array sono una particolare struttura derivata molto utilizzata in programmazione. Rappresentano un
tipo di dato derivato da altri preesistenti e raggruppano un insieme di dati omogenei (= dello stesso
tipo) fra loro. Con una sola variabile di tipo array dunque, possiamo indicare tanti dati (e quindi tanti
valori) dello stesso tipo.
Gli array sono insiemi collettivi omogenei di grandezza predefinita, quindi il numero di elementi
facenti parte della collezione viene decretato in fase di dichiarazione dello stesso.
Ognuno degli elementi dell’array viene numerato per poterlo distinguere dagli altri; la variabile o
costante intera utilizzata per riferirsi a questo numero viene definita indice dell’array.
La dichiarazione di un array è analoga a quella delle altre variabili, ricordandosi però che questo è un
tipo derivato: non esiste un array in sé, ma esistono array di interi, array di reali, di caratteri o di
qualsiasi tipo di dato definito in precedenza. La dichiarazione assume dunque la forma:
tipo nome [ dimensione ] ;
dove:
•
tipo è il tipo degli elementi della collezione;
•
nome è il nome (identificatore) della variabile array;
•
dimensione è un intero o una costante intera che rappresenta il numero di elementi della
collezione.
Ad esempio, per dichiarare un array di 12 interi di nome giorniMese dovremo scrivere:
int giorniMese[12];
L’array giorniMese conterrà dunque 12 variabili numerate da 0 a 11. La numerazione degli elementi di
un array infatti parte sempre da ZERO! Questo significa che per assegnare ad esempio il valore 31 al
primo elemento (il numero di giorni del primo mese) dovremo scrivere:
Le Basi della Programmazione
32
giorniMese[0] = 31;
Per assegnare il valore 30 all’undicesimo mese scriveremo
giorniMese[10] = 30;
La posizione 10 rappresenta infatti l’undicesimo elemento dell’array. Molto utile, quando si tratta di
array, lavorare in collaborazione con il costrutto for. Ad esempio per azzerare tutti i valori dell’array
giorniMese, sarà sufficiente scrivere:
for(int i=0; i< 12; ++i)
{
giorniMese[i] = 0;
}
Allo stesso modo per permettere ad un utente di inserire 10 valori basterà scrivere:
int valoriUtente[10];
for(int i=0; i<10; ++i)
{
cout << "Inserisci valore " << i + 1 << ": ";
cin >> valoriUtente[i];
}
Nei casi analoghi all’esempio giorniMese, in cui dobbiamo inizializzare un array a dei valori prefissati
e scrivere 12 o più assegnazioni può risultare noioso, sarà sufficiente scrivere:
int giorniMese[12] = {31,28,31,20,31,30,31,31,30,31,30,31};
inizializzando dunque l’array in fase di dichiarazione. Per prendere confidenza con gli array ecco una
serie di esercizi da eseguire PRIMA di procedere con la lettura.
Buon lavoro!
Le Basi della Programmazione
33
Esercizi svolti
•
Dichiarare un array di 100 interi e riempirlo con i primi 100 numeri dispari.
int a[100];
for(int i = 0; i<100; i++)
{
a [ i ] = 2*i + 1;
}
•
Dichiarare un array di 15 interi e inserirvi numeri casuali compresi tra 1 a 90.
Per eseguire questo programma dobbiamo imparare due nuove funzioni: la funzione rand() restituisce
un valore intero positivo.
int cart[ 15 ];
srand( time(0) );
for(int i = 0; i<15; i++)
{
cart [ 15 ] = rand() % 90 + 1;
}
•
Dichiarare un array di stringhe e inizializzarlo con i giorni della settimana.
string giorni[7] = {“lun”, “mar”, “mer”, “gio”, “ven”, “sab”, “dom”};
Le Basi della Programmazione
34
Esercizi semplici sugli array
1. Dichiarare e inizializzare l’array di stringhe mesiAnno e visualizzarne il contenuto.
2. Permettere all’utente di inserire 5 valori interi memorizzati tramite array, poi visualizzarli e
farne la somma.
3. Eseguire la media aritmetica di 8 valori numerici inseriti dall’utente e memorizzati in un array
di double.
4. Inserimento random di valori float in un array di 5 posti e visualizzazione.
5. Visualizzare uno qualsiasi degli array precedenti in ordine inverso (dall’ultimo elemento al
primo).
6. Dato un array di 100 interi, inizializzato con valori random, trovare: il massimo e la sua
posizione, il minimo e la sua posizione.
Le Basi della Programmazione
35
Ricerca di un elemento
La ricerca di un elemento in un array è una operazione tra le più comuni in assoluto in informatica:
immaginate ogni volta che inserite un nome utente, un codice, una password oppure semplicemente
cercate un numero di telefono nella rubrica del telefonino!
Tutte queste sono operazioni in cui informaticamente avviene una ricerca su un array di elementi
omogenei fra loro.
Vi sono vari tipi di ricerche effettuabili su un array; per ora vedremo la cosiddetta ricerca sequenziale.
Questa ricerca esamina le componenti dell’array fino a trovare l’elemento desiderato oppure
arrestandosi alla fine dell’array se l’elemento non è presente. Per semplicità descriveremo la procedura
per un array di interi.
// array inizializzato
int ar[10] = {1,12,23,34,45,56,67,78,89,90};
int pos = -1;
// posizione dell’elemento da cercare;
// -1 significa NON trovato
int daCercare;
cout << "Inserisci valore: ";
cin >> daCercare;
int i = 0;
while( pos == -1 && i < 10 )
{
if( ar[i] == daCercare )
{
pos = i;
}
i++;
}
// se ar[i] non è mai stato uguale a daCercare significa che
// l’elemento non è presente nell'array
// (e che pos è rimasto -1!!)
if (pos == -1)
Le Basi della Programmazione
36
{
cout << "L'elemento NON e’ presente nell'array." << endl;
}
else
{
cout << "L’elemento e’ presente alla posizione " << pos << endl;
}
La ricerca si arresta quando viene trovato un elemento uguale a quello da cercare o quando l’indice è
arrivato all’ultimo elemento (pos == −1 && i < 10 ). Ulteriori esercizi per la comprensione e
l’approfondimento sono elencati di seguito.
Esercizi sulla ricerca sequenziale
1. testare il codice proposto per la ricerca modificandolo in modo da permettere all’utente di
inserire anche i valori dell’array.
2. Modificare il codice proposto per la ricerca di un numero reale su un array di reali.
3. Modificare il codice proposto per la ricerca di un carattere su un array di caratteri.
4. Modificare la ricerca su array trovando tutte le occorrenze nell’array di un valore da cercare.
(suggerimento: i risultati saranno stanziati NON su un intero ma su un array di interi)
Le Basi della Programmazione
37
Ordinamento degli elementi
Ordinare gli elementi di un array (array sorting) è un’altra fra le operazioni più comuni esistenti in
informatica: mai riusciremmo a leggere un elenco del telefono o un vocabolario disordinato!!
I metodi di ordinamento sono numerosi e spesso molto ingegnosi: qui ne vengono presentati due fra i
più semplici e comuni.
Il primo è il classico Bubble Sort, un metodo che permette di far ”salire” in alto gli elementi più grandi
di un array, così come le bolle di sapone più grandi salgono più velocemente. Il risultato finale sarà
ovviamente un array ordinato in senso crescente.
Il secondo è il cosiddetto Insert Sort, ovvero Inserimento Ordinato. In questo modo ogni array è
sempre ordinato perfettamente perché ogni nuovo elemento viene inserito ”al punto giusto”, spostando
i successivi di una posizione in avanti.
Una semplice ricerca in Rete permetterà di scovare almeno un’altra decina di algoritmi di ordinamento
ognuno con la sua peculiarità e i suoi punti di forza. Come detto, i due che verranno citati sono stati
scelti in base alla semplicità di comprensione (del primo) e utilità nella programmazione (di entrambi,
soprattutto il secondo).
// BUBBLE SORT
int ar[ 10 ];
srand( time(0) );
for(int i = 0; i < 10; i++)
{
ar[i] = rand();
}
// ordinamento
for(int j = 1; j < 10; j++)
{
for( int i = 0 ; i < 10 -j; i++)
{
// se il valore del precedente è maggiore del successivo, scambiali
if(ar[i] > ar[i+1])
{
int temp;
temp = ar[i];
Le Basi della Programmazione
38
ar[i] = ar[i+1];
ar[i+1] = temp;
}
}
}
Il secondo algoritmo, piuttosto che un ordinamento di un insieme di valori disordinati, è una procedura
per inserire ”sempre” nel punto giusto i valori in un array. E’ molto utile in quanto molti elenchi hanno
un naturale bisogno di essere ordinati e comunque aggiornabili e una procedura completa di
ordinamento richiederebbe un lavoro inutile e da ripetere ogni volta per ordinare un solo elemento.
// INSERT SORT
int ar[ 10 ];
int n = 0;
...
// numero elementi inseriti nel array
// va eseguito ogni volta che si vuole inserire un numero nell’array
if(n<10)
{
int temp;
cout << "Inserisci un valore: ";
cin >> temp;
if(n==0)
{
array[n] = temp;
n++;
}
else
{
int i = n - 1;
while(temp < array[i] && i >= 0)
{
array[i+1] = array[i];
i--;
Le Basi della Programmazione
39
}
array[i+1] = temp;
n++;
}
}
Ognuno di questi algoritmi merita sicuramente di essere testato personalmente. Fatelo. E poi anche gli
esercizi di comprensione e approfondimento che seguono.
Esercizi su ordinamenti degli array
1. Ordinamento di un array di caratteri.
2. Ordinamento di un array di caratteri case-insensitive (dove ’a’ conta come ’A’).
3. Ordinamento decrescente di un array di interi random.
4. Inserimento ordinato decrescente di un array di float.
5. Menù di scelta che permette inserimento ordinato e visualizzazione dei cognomi dei
componenti della classe.
Le Basi della Programmazione
40
Ricerca Binaria (ricerca su array ordinato)
La ricerca binaria (o dicotomica) è un altro degli algoritmi ”fondamentali” che studieremo nel corso. È
un algoritmo di ricerca molto veloce che si può utilizzare solo nei vettori ordinati. Per dare un’idea di
come funziona immaginate di cercare una parola nel vocabolario: apriamo a caso il tomo e guardiamo a
che punto siamo; se la parola si trova prima apriamo un’altra pagina a caso tra quelle precedenti,
viceversa in quelle successive. Ripetiamo restringendo il campo di ricerca finché non troviamo la
pagina in cui deve trovarsi la nostra parola e solo a quel punto consultiamo la pagina!
Il seguente algoritmo funziona esattamente così: cerca all’esatta metà del array il valore da cercare. Se
lo trova bene, altrimenti se il valore da trovare è minore di quello trovato si cerca nella prima metà (a
metà della prima metà), se il valore da trovare è maggiore di quello trovato si cerca nella seconda metà
(la metà della seconda metà).
Il procedimento si itera finché non si trova il valore o finché ci sono caselle in cui cercare. Nell'esempio
seguente il campo di ricerca è delimitato ad ogni iterazione dai valori low e high, che ne rappresentano
estremo inferiore e superiore.
// RICERCA DICOTOMICA
int daCercare;
cout << "Inserisci elemento da cercare: ";
cin >> daCercare;
int high = 10 – 1; // 10 è la dimensione dell'array
int low = 0;
int pos = -1;
do
{
int i = (high + low)/2;
if ( ar[i] == daCercare )
{
pos = i;
}
else
{
if( ar[i] < daCercare )
{
low = i + 1;
Le Basi della Programmazione
41
}
else
{
high = i - 1;
}
}
}
while(high > low && pos == -1);
if (pos == -1)
{
cout << "Elemento NON presente!" << endl;
}
else
{
cout << "L’elemento si trova alla posizione " << pos << endl;
}
Esercizi sulla ricerca dicotomica
1. Menù che permetta inserimento ordinato e ricerca binaria su un insieme di interi.
2. Ricerca binaria su un elenco decrescente di float.
3. Test di velocità. Preparare un array di interi di 1000 caselle con valori random. Eseguire una
ricerca su un elemento. Poi ordinarlo ed eseguire una ricerca binaria. Valutazione delle
differenti velocità di ricerca.
Le Basi della Programmazione
42
6. Stringhe in C++
Nel linguaggio C++, la classe std::string è la rappresentazione standard per una stringa di testo. Questa
classe rimuove molti dei problemi introdotti nel linguaggio C++ nella gestione delle stringhe e
permette pure una conversione implicita dal tipico array di caratteri del linguaggio C alla nuova classe
C++.
Inoltre introduce nuovi importanti benefici nella trattazione delle stringhe, come la possibilità di
confronto tramite gli appositi operatori ==, !=, <, >, <=, >= invece dell’utilizzo delle funzioni della
libreria C string.h che diviene sostanzialmente deprecata (in C++).
Vediamo un semplice esempio di utilizzo per capire quanto sia facile gestire stringhe in C++:
string uno = "ciao";
string due = "hello";
if( uno == due )
cout << uno << " è uguale a " << due << endl;
else
cout << uno << " è diverso da " << due << endl;
return 0;
Semplificando, si può dire che il tipo “string” così introdotto, funziona esattamente come tutti gli altri
tipi primitivi introdotti.
Le Basi della Programmazione
43
Stringhe e Puntatori
Poiché il tipo char* può essere implicitamente trasformato in string &, questo modo di dichiarare i
parametri permette un uso più semplice e universale della funzione.
const char* a = "ciccio";
string b = "pippo";
// tutti e tre i metodi funzionano!!
scrivi( a );
scrivi( "poldo" );
scrivi( b );
Operatori su stringhe
Le stringhe in C++ possono utilizzare una serie interessante di operatori ”di convenienza”, in modo da
rendere semplici e intuitive certe operazioni altrimenti piuttosto laboriose nel linguaggio C.
Il più semplice e ovvio è l’operatore di assegnazione:
string str;
str = "andrea"; // possibile assegnare parole
str = ’a’; // ERRORE: una stringa non è un carattere
str = "a"; // così funziona
char pippo[30] = "diamantini";
str = pippo;
// CORRETTO
char *pluto = str; // ERRORE
Inoltre, le stringhe usano l’operatore aritmetico + per le concatenazioni
string nome = "andrea";
string cognome = "diamantini";
string spazio = " ";
Le Basi della Programmazione
44
string nomeCompleto = nome + spazio + cognome;
cout << nomeCompleto << endl;
// scrive "andrea diamantini"!!
e gli operatori di confronto (come già visto nel primo esempio)
string uno = "prova";
string due = "tentativo";
if( uno < due ) // VERO. Alfabeticamente parlando
{
// nell’alfabeto e soprattutto nel codice ASCII!
cout << "OK!" << endl;
}
Chiaramente funzionano anche gli operatori ! =, <=, >=.
Ultima cosa, la possibilità di accedere casualmente ad ogni elemento della stringa, esattamente come in
un array di caratteri
string uno = "prova";
uno[0] = ’P’;
// mette la maiuscola
char c = uno[3];
// c vale ’v’
Le Basi della Programmazione
45
7. Tipi Derivati
I tipi derivati (o composti) sono così detti perché nelle basi del linguaggio di programmazione non
esistono, ma devono essere dichiarati dal programmatore a partire dai tipi fondamentali (o semplici).
I linguaggi di programmazione fanno un grande uso dei tipi derivati, perché tendono ad avvicinarsi
molto di più alle strutture dati necessarie per risolvere un problema, favorendo la leggibilità e la
chiarezza del programma.
In C++ abbiamo gli strumenti per definire infiniti tipi derivati, classificabili però in poche categorie:
•
array,
•
funzioni,
•
puntatori,
•
strutture,
•
unioni,
•
campi,
•
file.
Quindi qualsiasi tipo derivato sarà comunque in una di queste categorie e quindi soggetto alle regole
proprie della stessa.
Alcuni di questi tipi sono già stati affrontati nel corso della nostra trattazione, vediamo gli altri ancora
sconosciuti.
Le Basi della Programmazione
46
Strutture
la struttura serve a definire in un unico tipo un insieme di dati che non sono slegati fra loro, ma che
insieme rappresentano un unico concetto. Quando, ad esempio, si parla di un compleanno, si ha in
mente un unico dato, quello relativo alla propria data di nascita.
Secondo le nostre attuali conoscenze, avremmo però dovuto dichiarare almeno tre variabili intere
diverse per memorizzare ciò che nella nostra testa è un unico concetto.
La struttura si pone di superare questo limite, insito nella semplicità dei tipi fondamentali e permette di
dichiarare un nuovo tipo, data, che contenga informazioni sul giorno, mese, anno di ogni data.
struct data
{
int gg;
int mm;
int aa;
};
Come si vede, tramite la parola chiave struct, abbiamo definito il nuovo tipo data, che contiene al suo
interno tre interi, uno per il giorno, uno per il mese, uno per l’anno. I dati contenuti all’interno di una
struct sono solitamente definiti campi.
Attenzione però!
La struct così definita NON è una variabile, ma un tipo, che dovrà essere istanziato per poter essere
utilizzato.
data compleannoAndrea;
compleannoAndrea.gg = 3;
compleannoAndrea.mm = 7;
compleannoAndrea.aa = 1976;
Come visto nell’esempio, i campi sono accessibili dall’istanza tramite l’operatore punto (.). A quel
punto, è possibile lavorare coi dati come si è sempre fatto.
I campi di una struct possono essere di qualsiasi tipo (semplice o derivato), ma che deve già essere
stato definito.
Così se la struct data è già definita, possiamo ora creare:
Le Basi della Programmazione
47
struct indirizzo
{
string via;
int numero;
string citta;
char provincia[2];
int cap[5];
};
struct datiPersonali
{
string nome;
string cognome;
data dataNascita;
indirizzo indResidenza;
};
Da notare che la dichiarazione di una struct richiede il punto e virgola ; finale dopo la chiusura delle
parentesi graffe. In C++ esistono solo due casi in cui è assolutamente necessario (e questo è il primo
che incontriamo)!
Esercizi sulle struct
1. Programma che utilizza la struttura data sopra descritta per memorizzare la propria data di
nascita e visualizzarla nella forma gg/mm/aaaa
2. Programma che utilizza la struttura datiPersonali sopra descritta per memorizzare i propri dati
personali e che al termine dell’inserimento li visualizza.
3. Programma che gestisce la classifica finale di una gara di nuoto con nome dell’atleta, vasca in
cui ha gareggiato, posizione finale raggiunta e tempo di gara.
4. Programma che simula la rubrica del proprio cellulare.
5. Programma che gestisce i dati per la compravendita di automobili.
Le Basi della Programmazione
48
Unioni
Le unioni sono un costrutto analogo alle strutture, ma con tutt’altra funzionalità: se infatti le strutture
introducono una organizzazione logica dei dati, le union raggruppano nello stesso spazio di memoria
tutti i campi presenti. Questo permette di memorizzare un solo campo per volta tra tutti quelli presenti
nella union.
Vediamo la sintassi generale:
union Identificativo
{
tipo_campo1 nome_campo1;
tipo_campo2 nome_campo2;
. . .
};
Ovviamente, visto come sono definite, sarà facile intuire che le union occupano in memoria lo stesso
spazio del loro campo più grande.
E’ possibile utilizzare le union per memorizzare nella stessa variabile (alternativamente) dati di tipo
diverso. Immaginiamo di avere un elenco di oggetti di cui per l’identificazione si hanno una sigla di
TOT lettere, tipo codice a barre oppure un numero di serie.
In quel caso sarà possibile definire una union del tipo:
union oggetto
{
char codiceBarre[20];
int numeroSerie;
};
Il tipo union oggetto potrà dunque contenere o una sequenza di 20 caratteri oppure un intero.
le union, vista la loro estrema particolarità e la difficoltà nel trattamento dei dati memorizzati, sono
poco usate se non in casi particolari, che qui non tratteremo.
Le Basi della Programmazione
49
Campi
I campi (fields) sono un costrutto tipico del C, che permette di accedere direttamente ai singoli bit che
compongono una variabile. La sintassi generale è analoga alle precedenti incontrate:
struct Identificativo
{
unsigned nome_campo1 : numero_bit1;
unsigned nome_campo2 : numero_bit2;
. . .
};
Vediamo con un esempio pratico di illustrare al meglio il concetto:
struct Date
{
unsigned nWeekDay : 3;
// 0..7
(3 bits)
unsigned nMonthDay : 6; // 0..31 (6 bits)
unsigned nMonth : 5;
// 0..12 (5 bits)
unsigned nYear : 8;
// 0...100 (8bits)
};
Date today;
La variabile today di tipo Date occupa uno spazio di memoria pari alla somma dei bit utilizzati in ogni
campo, arrotondato (sempre) per eccesso ad un multiplo della word di macchina. Nel nostro caso,
abbiamo 3 + 6 + 5 + 8 = 22 bit utilizzati, mentre le macchine moderne hanno attualmente word a 32 o
64 bit. Questo significa, nel caso di word = 32, che la variabile today occupa 4 byte, pari a 32 bit di
memoria (22 utilizzati, 10 sprecati).
I bit della variabile si accedono con la solita sintassi (dot notation), utilizzata anche per le strutture e le
unioni e i bit possono essere trattati come semplici numeri interi non negativi.
Ad esempio, sarà possibile scrivere, nella variabile today, qualcosa come:
today.nMonth = 3;
Come le union, anche i campi sono costrutti poco usati, se non nell’interazione diretta bit a bit con
determinati dati. Il loro utilizzo tipico non sarà oggetto del corso.
Le Basi della Programmazione
50
Puntatori
Un Puntatore in C++ è una variabile che contiene l’indirizzo di memoria in cui si trova un oggetto,
invece che memorizzare direttamente quello.
Il tipo di dato ”puntatore” è dunque tipicamente un tipo di dato derivato dal tipo dell’oggetto di cui
contiene l’indirizzo di memoria. Vediamo un esempio di codice che possa chiarirci le idee:
int main()
{
int a, b;
int *p;
// p è un puntatore a intero
a = 2;
p = &a;
// p contiene l’indirizzo di a
*p = 3;
// all’indirizzo individuato da p
// viene assegnato il valore 3
b = a;
// all’intero b viene assegnato il valore di a.
// Quanto vale b??
}
Un puntatore nullo è un puntatore a cui non è stato assegnato nessun indirizzo (p = 0, oppure p =
NULL). Quando si tenta di utilizzare un puntatore nullo, si ottiene un crash dell’applicazione con un
poco rassicurante ”Segmentation Fault”.
I problemi tipici con i puntatori sono suddivisibili in due categorie, entrambe da evitare accuratamente:
Problema 1: Dangling Reference
La problematica definita “dangling reference” (riferimento sospeso) si verifica quando un valore viene
memorizzato tramite puntatore e il puntatore perde il riferimento all'oggetto. Questo rimane perso in
memoria in un punto indefinito, occupando il blocco fino allo spegnimento della macchina!
int n = 4;
Le Basi della Programmazione
double *p = & sqrt(n);
51
// Assegnato a p l’indirizzo del calcolo
// della radice quadrata di n.
p = 0;
// il numero precedentemente calcolato
// NON è stato cancellato,
// ma non è più raggiungibile
Problema 2: Dangling Pointer
La problematica definita “dangling pointer” (puntatore sospeso) si verifica quando un puntatore
contiene un indirizzo inutile, non contenente nessun dato del problema.
Quando si cerca di accedere al dato contenuto nel puntatore si viola un blocco di memoria non
accessibile e l'applicazione va in crash!
int *p;
int i = 1;
while(i<3)
{
int n = 2*i;
p = &n;
// p punta all’indirizzo di n
i++;
}
cout << *p;
// CRASH! p contiene un indirizzo non valido
// infatti n non esiste più!!
Se non intendiamo modificare in alcun modo l’oggetto attraverso il puntatore, possiamo dichiararlo
costante:
int n = 7;
const int *p = n;
*p = 5;
// ERRORE. CODICE NON COMPILABILE
Le Basi della Programmazione
52
Riferimenti
Oltre ai puntatori, il C++ supporta anche il concetto di ”riferimenti”. Come un puntatore, un
riferimento contiene l’indirizzo di un oggetto. Le differenze sostanziali sono che:
•
i riferimenti sono dichiarati usando & anziché *
•
i riferimenti devono essere inizializzati (cioè bisogna subito assegnargli un indirizzo) e non
possono essere poi riassegnati.
•
l’oggetto associato ad un riferimento è direttamente accessibile con la solita sintassi (no − >)
•
un riferimento non può essere nullo
I riferimenti sono usati soprattutto per la dichiarazione dei parametri. Per default, il C++ utilizza il
passaggio di parametri per valore:
int moltiplicazione(int a, int b)
{
return a*b;
}
E in questo caso dovremmo invocare la funzione nel seguente modo:
int primo = 2;
int secondo = 3;
int d = moltiplicazione(primo,secondo);
Questo crea nel calcolo della funzione distanza un inutile spreco di memoria dato dalla duplicazione
dei valori passati come parametri. Il programmatore attento scriverà dunque:
int moltiplicazione(int* a, int* b)
{
return (*a) * (*b);
}
E utilizzerà così la funzione:
Le Basi della Programmazione
53
int primo = 2;
int secondo = 3;
int d = moltiplicazione(&primo,&secondo);
Questa accortezza però rende le cose più dificili e introduce ulteriori problemi, ad esempio nel caso del
passaggio di puntatori nulli, oppure rischiando che il codice della funzione moltiplicazione modifichi i
valori dei nostri dati. Utilizzando riferimenti al posto dei puntatori, faremo così:
int moltiplicazione(int& a, int& b)
{
return a*b;
}
E potremo utilizzare il semplice codice:
int primo = 2;
int secondo = 3;
int d = moltiplicazione(primo,secondo);
Se infine NON vogliamo rischiare modifiche indesiderate ai nostri valori, possiamo utilizzare
riferimenti costanti:
int moltiplicazione(const int& a, const int& b)
Puntatori e riferimenti rappresentano le stesse informazioni in memoria. Per convertire uno nell’altro è
sufficiente utilizzare gli operatori unari & e *
int p;
int *ptr = p;
int &ref = *ptr;
int *p2 = &ref;
Le Basi della Programmazione
54
I riferimenti hanno una sintassi molto semplice e conveniente, ma i puntatori possono essere sempre
riassegnati, possono contenere valore nullo e la loro sintassi esplicita li individua senza ombra di
dubbio. Per queste motivazioni in programmazione si usano di solito i puntatori lasciando ai
riferimenti, per lo più costanti, il ruolo di parametri nelle funzioni.
Strutture e Puntatori
Data una struttura, è possibile accedere ai suoi campi anche tramite un opportuno puntatore. I puntatori
però, utilizzano l’operatore arrow (− >) per accedere ai campi della struttura, al posto dell’operatore dot
(.).
Data la struttura data, definita precedentemente, in una variabile normale è possibile riferirsi ai suoi
campi tramite il solito operatore dot (.).
struct data n;
n.gg = 30;
n.mm = 3;
n.aa = 2009;
Se utilizziamo un puntatore invece, dobbiamo riferirci ai campi tramite l’operatore arrow (− >):
struct data m;
struct data *p;
p = &m;
p->gg = 31;
p->mm = 3;
p->aa = 2009;
Inoltre quando utilizziamo gli operatori unari & e *, dobbiamo ricordarci che l’operatore . e − > hanno
sempre precedenza su di essi.
Le Basi della Programmazione
struct data m;
struct data *p;
p = &m;
p->gg = 6;
(*p).mm = 4;
cout << Anno: << (&m).aa << endl;
55