- 1.1 Introduzione
- 1.2 Un primo programma
- 1.3 Le variabili
- 1.4 Gli operatori
- 1.5 Le strutture di controllo
- 1.6 Le funzioni
- 1.6.1 un primo esempio
- 1.6.2 funzioni senza tipo di ritorno
- 1.6.3 funzioni ed omonimia
- 1.6.4 il prototipo di una funzione e la sua implementazione
- 1.6.5 valori di default degli argomenti di una funzione
- 1.6.6 l'esportazione delle funzioni in librerie
- 1.6.7 il file
libreria.h
- 1.6.8 il file
libreria.cc
- 1.6.9 il file
main.cpp
- 1.6.10 librerie in
C++
- 1.6.11 le funzioni
inline
- 1.6.12 funzioni matematiche
- 1.6.13 un esempio: radice quadrata ed elevamento a potenza
- 1.6.14 accesso all'orologio del computer
- 1.6.15 un test di performance
- 1.7 Direttive al preprocessore
- 1.8 La scrittura del proprio programma
- 1.9 Le opzioni di compilazione
- 1.10 Gli errori di compilazione
- 1.11 ESERCIZI
- le istruzioni che un calcolatore segue per eserguire i compiti assegnati sono scritte in linguaggio macchina, che non è umanamente intelleggibile
- i linguaggi di programmazione sono gli strumenti di scrittura umana delle istruzioni per il calcolatore
- la traduzione delle istruzioni scritte in linguaggi di programmazione in istruzioni per il calcolatore è effettuata da appositi programmi
- l'esecuzione di un programma corrisponde al momento in cui il calcolatore segue le istruzioni impartite
- esistono due grandi categorie di linguaggi di programmazione:
linguaggi interpretati |
---|
- la traduzione del programma avviene automaticamente durante la sua esecuzione
- le istruzioni sono lette e tradotte riga per riga, quindi spesso il programmatore può scrivere la riga successiva dopo aver osservato il risultato dell'istruzione precedente
- il programma è in generale lento, perché la traduzione contestuale non è ottimizzata
python
è un linguaggio di programmazione interpretato
linguaggi compilati |
---|
- la traduzione del programma avviene prima della sua esecuzione, tramite l'invocazione di istruzioni specifiche di compilazione
- il programma va concepito nella sua interezza prima dell'esecuzione
- le istruzioni vengono eseguite velocemente
C
eC++
sono esempi di linguaggi di programmazione compilati
C
è un linguaggio di programmazione:- imperativo, cioè che impartisce sequenze di istruzioni al calolatore
- procedurale, cioè che permette di raggruppare istruzioni in procedure
C++
è un linguaggio di programmazione:- che estende il
C
:- un programma
C
compila anche inC++
- la sintassi del
C
è valida anche inC++
- esistono concetti nuovi nel
C++
- esiste più libertà nel
C++
- un programma
- object oriented, cioè che permette di definire nuovi tipi di variabili
all'interno dei programmi
- vedremo che si tratta di un cambio di paradigma fondamentale
- permette la programmazione template,
che è una forma di generalizzazione delle istruzioni impartite al calcolatore
- vedremo che porta alla creazione di molte librerie di utilità generale
- che estende il
- esistono diverse versioni del linguaggio
C++
: C++98 (che useremo noi), C++03, C++11 (che accenneremo), C++14, C++17
- la sequenza di istruzioni scritte nel linguaggio di programmazione sono salvate in un file di testo, che è solitamente chiamato codice sorgente
- gli editor di testo dedicati alla programmazione contengono strumenti specifici che permettono di evidenziare la sintassi del codice e talvolta controllarne la grammatica ed ortografia
- oltre alle istruzioni, in un codice sorgente si possono inserire commenti,
che sono frasi ignorate durante la compilazione
- i commenti in
C
iniziano con/*
e terminano con*/
:
/* questo è un commento */
- i commenti in
- ATTENZIONE: i simboli di apertura e chiusura di commenti non funzionano come parentesi
- scrivere
/* /* */ */
non è come scrivere( ( ) )
- scrivere
- in
C++
i commenti possono anche iniziare con//
e terminano automaticamente a fine riga:// questo è un commento
- il programma più semplice e autoconsistente è costituito dalla sequenza di istruzioni relative alla funzione
main
- ogni programma deve contenere una ed una sola funzione chiamata
main
, che viene eseguita dal calcolatore quando il programma viene lanciato - esistono due versioni della funzione
main
- una senza argomenti che corrisponde al caso in cui il programma sia eseguito da SHELL senza argomenti:
int main () { return 0 ; }
- una con argomenti in ingresso che corrisponde al caso in cui il programma sia eseguito da SHELL con l'aggiuntda di argomenti mediante una frase scritta a linea di comando:
int main (int arcg, char ** argv) { return 0 ; }
- entrambe le versioni illustrate della funzione
main
implementano un programma funzionante che, quando viene eseguito, restituisce alla SHELL un numero intero, chiamato exit status- per convenzione si sceglie di restituire il numero
0
se tutto è andato bene, mentre un numero non nullo è usato per segnalare che ci sono stati problemi durante l'esecuzione (esistono codici di errori codificati, tuttavia il programmatore ha la libertà di aggiungerne o cambiarli)
- per convenzione si sceglie di restituire il numero
suggerimenti |
---|
- si consiglia di svolgere tutti gli esercizi presentati in ogni lezione in una cartella dedicata,
quindi, dopo aver aperto una SHELL:
> mkdir Lab2_Modulo1 > cd Lab2_Modulo1 > mkdir Lezione_01 > cd Lezione_01 > touch main_00.cpp
- il comando
touch
crea un file vuoto, in questo caso con nomemain.cpp
. Aprite quindi il file con il vostro editor preferito, scrivete il codice e salvate - il nome del file che contiene il codice sorgente può essere scelto arbitrariamente.
Noi useremo sempre il suffisso
.cpp
per i codici sorgenti che contengono la funzionemain
(vedremo che il codice sorgente di un programma può essere spezzato in più file)
- il comando
- nello scrivere un programma, ogni volta che si apre una parentesi graffa la si chiuda immediatamente, per non dimenticarlo
- non dimenticate le variabili di ritorno delle funzioni,
main
incluso
- create un codice sorgente
main.cpp
con lo scheletro vuoto descritto al paragrafo precedente - il sorgente va compilato perché possa essere eseguito dal calcolatore
- per compilare il programma si utilizza il comando
c++
, chiamato compilatore:> c++ -o main_00 main_00.cpp
- l'argomento
-o main_00
dice al compilatore di chiamare l'eseguibile con il nomemain_00
- l'argomento
- per eseguire il programma (nel caso di
main
senza argomenti):> ./main_00 >
- non succede nulla, infatti non ci sono istruzioni all'interno della funzione
main
- l'istruzione
return 0
non dice di scrivere a schermo0
, ma di restituire alla SHELL il valore0
(questo valore può essere intercettato ed utilizzato mediante i comandi di SHELL)
- non succede nulla, infatti non ci sono istruzioni all'interno della funzione
suggerimenti |
---|
- includete sempre all'inizio del codice sorgente un commento che contenga il comando di compilazione del programma
- oltre ai comandi fondamentali disponibili di default, il
C++
offre insiemi di istruzioni dedicate allo svolgimento di specifici compiti, questi sono incapsulate in librerie (identificate da un nome comeiostream
,cmath
...) - bisogna sempre dichiarare al programma che si vuole utilizzare una o più librerie
(usando il comando
#include <nome-della-libreria>
) - per scrivere a schermo,
si utilizza la libreria
iostream
che gestisce il flusso (stream) di informazione in input (i) ed output (o) durante l'esecuzione del programma:#include <iostream> int main (int arcg, char ** argv) { std::cout << "42" << std::endl ; return 0 ; }
- la linea
#include <iostream>
dice al compilatore di utilizzare la libreriaiostream
- il compilatore sa dove trovare le librerie standard tramite variabili di ambiente della SHELL
- la variabile
cout
(che abbrevia character output) rappresenta lo strumento di output; in questo caso, essendo quello standard (std::
) si tratta dello schermo - la variabile
endl
è la fine della linea, essendo quella standard è un accapo - il simbolo
<<
rappresenta l'operatore di redirezione, che sposta quello che sta alla propria destra verso sinistra. Quindi in questo caso primaendl
viene incollato a42
, quindi l'insieme dei due viene inviato allo schermo.
- la linea
- l'esecuzione del programma visualizzerà a schermo
42
:> c++ -o main_01 main_01.cpp > ./main_01 42
- la compilazione di un programma di divide in tre fasi
preprocessing |
---|
- creazione del programma da compilare:
- vengono eseguite le direttive date al preprocessore, sono le istruzioni che iniziano con il simbolo
#
- ad esempio, l'istruzione
#include <iostream>
chiede al preprocessore di copiare al posto della linea stessa tutto il codice sorgente contenuto nella libreriaiostream
- vengono eseguite le direttive date al preprocessore, sono le istruzioni che iniziano con il simbolo
compilazione |
---|
- il compilatore vero e proprio entra in azione in questo stadio ed effettua:
- controllo sintattico del programma
- ad esempio,
itn
invece diint
dà errore
- ad esempio,
- controllo grammaticale del programma
- traduzione del codice sorgente in linguaggio macchina
- controllo sintattico del programma
- ogni funzione, creata in linguaggio macchina, diventa un oggetto del compilatore
linking |
---|
- in questo ultimo passaggio, vengono connessi i vari oggetti del compilatore
- nel nostro esempio, la parte pre-compilata delle librerie viene debitamente connessa
alle chiamate presenti nella funzione
main
- nel nostro esempio, la parte pre-compilata delle librerie viene debitamente connessa
alle chiamate presenti nella funzione
- NOTA BENE: gli oggetti del compilatore non hanno a che fare con la programmazione ad oggetti, si tratta di uno sfortunato caso di omonimia
- si possono passare informazioni al programma aggiungendo parametri a linea di comando
- la SHELL passa alla funzione
main
la frase scritta dall'utente, sotto forma diarray
di stringhe di tipoC
argc
è il numero di elementi dell'array
argv
è l'array stesso
#include <iostream> int main (int arcg, char ** argv) { std::cout << "42" << std::endl ; std::cout << "ecco il nome dell'eseguibile: " << argv[0] << "\n" ; return 0 ; }
- la liberia
<iostream>
può essere anche utilizzata per leggere informazioni dalla tastiera#include <iostream> int main (int arcg, char ** argv) { int numero = 0 ; std::cout << "inserisci un numero\n" ; std::cin >> numero ; std::cout << "hai inserito: " << numero << "\n" ; return 0 ; }
- la tastiera è identificata da
std::cin
- l'operatore
>>
trasferisce l'informazione dall'esterno verso il programma:
> ./main_03 inserisci un numero 4 hai inserito: 4
- la tastiera è identificata da
- le informazioni sono manipolate dal programma sotto forma di variabili, che indicano zone di memoria del calcolatore riservate dal programma per la memorizzazione dell'informazione
- diversi tipi di oggetti hanno bisogno di dimensioni differenti di memoria e di un formato diverso di scrittura
- per ogni differente possibilità esiste un tipo associato in
C++
, che contiene le informazioni di lunghezza e formattazione - i principali tipi sono i seguenti:
tipo | Keyword | contenuto |
---|---|---|
Boolean | bool | vero/falso |
Character | char | singoli caratteri |
Integer | int | numeri interi fra -32768 fino a 32767 |
Floating point (virgola mobile) | float | numeri razionali |
Double floating point | double | numeri razionali |
Valueless | void | nessun tipo |
- le variabili si definiscono ed inizializzano utilizzando le keyword indicate in tabella precedente:
// definizione di due numeri interi int num1 = 0 ; int num2 = 3 ; // somma di due numeri interi int somma = num1 + num2 ; std::cout << "Somma: " << somma << std::endl ; // definizione di due numeri razionali float razionale1 = 3.1416 ; double razionale2 = 1.4142 ; // definizione di un carattere char lettera = 'à ; // definizione di un valore booleano bool condizione = true ;
- NOTA BENE: le variabili di tipo
char
sono definite con valori indicati fra apici, non fra virgolette
- NOTA BENE: le variabili di tipo
suggerimenti |
---|
- non appena una variabile viene definita, assegnarle sempre un valore,
tipicamente optando per una delle seguenti scelte:
- si assegna un valore di default, che abbia senso nei calcoli a seguire;
- si assegna un valore palesemte insensato, in modo che se ci si scorda di assegnare il valore corretto alla variabile, il programma non funzioni o dia risultati palesemente insensati;
- definire una variabile per riga, per chiarezza di lettura;
- dare nomi esplicativi alle variabili (e quindi anche sufficientemente lunghi).
- l'attributo
const
premesso ad una variabile indica che essa non può cambiare di valore durante l'esecuzione del programma. - se nel codice sorgente si prova a modificare una variabile dichiarata
const
, il compilatore si accorge di questo errore di grammatica di programmazione e non compila, restituendo un errore:> c++ -o main_05 main_05.cpp main_05.cpp:10:12: error: cannot assign to variable 'numerò with const-qualified type 'const int' numero = numero + 1 ; ~~~~~~ ^ main_05.cpp:9:15: note: variable 'numerò declared const here const int numero = 0 ; ~~~~~~~~~~^~~~~~~~~~ 1 error generated.
- ad una variabile è associata una zona di memoria nella RAM, che è dove il calcolatore scrive la variabile durante le operazioni
- in
C++
è possibile definire una zona di memoria estesa, chiamata array, predisposta a contenere un elenco di variabili dello stesso tipo giustapposte in celle di memoria contigue// array di 5 numeri interi int num_array[5] ;
- la dimensione dell'array,
indicata fra parentesi nella definizione della variabile,
non può essere una variabile (nemmeno
const
), deve essere un numero scritto nel codice sorgente
- la dimensione dell'array,
indicata fra parentesi nella definizione della variabile,
non può essere una variabile (nemmeno
- le singole celle di memoria sono accessibili
tramite l'operatore
operator[]
applicato al nome della variabile, che si scrive utilizzando le parentesi quadre come nell'esempio che segue:num_array[0] = 3 ; num_array[1] = 6 ; num_array[2] = 9 ; num_array[3] = 11 ; num_array[4] = 131 ;
- gli indici delle celle di memoria di un array lungo N partono a 0 e finiscono ad N-1
- il compilatore non sempre si accorge che gli indici siano in questo intervallo
- qualunque tentativo di leggere una zona di memoria all'esterno di questo intervallo
può produrre un errore in fase di compilazione,
oppure un comportamento inatteso del programma
- provate, nel caso dell'array precedente, a includere queste istruzioni nel vostro programma:
int index = 4 ; std::cout << num_array[index + 1] << std::endl ;
- NOTA BENE: si tratta di errori difficili da trovare, bisogna prestare molta attenzione agli indici di lettura degli array
- un array può essere anche definito indicandone esplicitamente la lista degli elementi
fra parentesi graffe:
float float_array[] = {2., 3.14} ; std::cout << float_array[0] << std::endl ; std::cout << float_array[1] << std::endl ;
- il casting in
C
è la conversione fra diversi tipi di variabili numeriche - siccome le medesime operazioni fra tipi diversi possono dare risultati differenti
(provate a calcolare il valore della frazione 3/5
come rapporto fra due variabili
int
o come rapporto fra due variabilifloat
), è importante sapere come convertire variabili in maniera esplicita, utilizzando la sintassi(type) numero
per convertire la variabilenumero
nel tipotype
:int numero_intero = 4 ; float numero_razionale = (float) numero_intero ;
- in
C++
l'operazione di casting ha portata più ampia e può essere realizzato con operatori dedicati. Quello con la funzionalità equivalente al type cast delC
è:float secondo_razionale = static_cast<float> (numero_intero) ;
- NOTA BENE uno dei vantaggi di usare l'espressione
C++
del cast è che questo è facilmente rintracciabile nel codice sorgente!
- Gli operatori predefiniti in
C++
permettono di compiere operazioni fra variabili - Per ogni tipo di variabile, esistono operatori corrispondenti alle operazioni che si possono fare fra queste variabili
- gli operatori si comportano alla stregua di funzioni, con variabili in ingresso e variabili di ritorno
- tipicamente un operatore agisce su un singolo di tipo, quindi l'applicazione di operatori a più di un tipo implica un casting implicito fatto dal compilatore
- attribuiscono il valore iniziale ad una variabile:
int numero = 5 ;
- in questo caso, il tipo in ingresso è un
int
(la variabile stessa è una sorta di argomento implicito dell'operatore) - l'effetto dell'operatore è quello di assegnare alla variabile
numero
il valore che sta a destra del simbolo=
- il tipo in uscita è ancora
int
ed è il valore assegnato alla variabile
std::cout << (numero = 7) << std::endl ;
- di conseguenza, le assegnazioni si possono fare in cascata:
int numero_2 = numero = 7 ;
- in questo caso, il tipo in ingresso è un
- anche per l'operatore di assegnazione si realizza il casting implicito:
float razionale = 5 ;
5
è di tipoint
, quindi viene prima convertito infloat
(5.
) e poi passato come argomento all'operatore di assegnazione
- corrispondono alle tipiche operazioni matematiche fra numeri interi o razionali
float R1 = 5. ; float R2 = 5. ; float R3 = R1 + R2 ; std::cout << R3 << std::endl ; R3 = R3 + 4.5 ; std::cout << R3 << std::endl ;
-
gli operatori aritmetici esistono anche composti con l'operatore di assegnazione
R3 += 2.1 ; std::cout << R3 << std::endl ;
-
l'operazione precedente è equivalente a equivalente a
R3 = R3 + 2.1 ;
operatore op. composto operazione +
+=
addizione -
-=
sottrazione *
*=
moltiplicazione /
/=
divisione %
%=
resto della divisione tra interi
-
L'incremento o decremento unitario di una variabile si può ottenere anche con operaori dedicati:
operatore operazione ++
incremento unitario --
decremento unitario -
l'operatore agisce direttamente sulla variabile al quale viene applicato, similmente agli operatori composti:
int num = 5 ; ++num ; std::cout << num << std::endl ; --num ; std::cout << num << std::endl ;
- ciascun operatore esiste in due versioni:
- pre-incremento o pre-decremento: la variabile a cui viene applicato
viene modificata dall'operatore prima dell'esecuzione di eventuali altre operazioni
che accadono in quella linea (
++num
,--num
), quindi l'operatore restituisce la variabile stessa - post-incremento o post-decremento: la variabile a cui viene applicato
viene modificata dall'operatore dopo dell'esecuzione di eventuali altre operazioni
che accadono in quella linea (
num++
,num--
)
int num1 = 5 ; std::cout << ++num1 << std::endl ; int num2 = 5 ; std::cout << num2++ << std::endl ; std::cout << num2 << std::endl ;
- gli operatori di post-incremento e post-decremento creano una copia della variabile alla quale sono applicati, incrementano la variabile e restituiscono la copia (che non è stata incrementata)
- di conseguenza, gli operatori di post-incremento e post-decremento sono più lenti di quelli di pre-incremento e pre-decremento e richiedono che sia possibile creare una copia della variabile alla quale sono applicati
- pre-incremento o pre-decremento: la variabile a cui viene applicato
viene modificata dall'operatore prima dell'esecuzione di eventuali altre operazioni
che accadono in quella linea (
- gli operatori di incremento vengono tipicamente utilizzati per aumentare o diminuire il valore delle variabili indice dei cicli
-
gli operatori relazionali confrontano tra loro i valori di due variabili
-
prendono in ingresso due variabili e restituiscono un valore booleano che indica se la relazione è soddisfatta o meno
operatore operazione ==
uguaglianza !=
non uguaglianza <
minore di <=
minore o uguale >
maggiore di >=
maggiore o uguale -
NOTA BENE: l'operatore di uguaglianza ha due segni
=
nel nome, perché l'operatore con un solo=
assegna il valore di destra alla variabile di sinistra. La confusione tra i due operatori è una frequente sorgente di errori!
- gli operatori logici codificano le relazioni fra variabili booleane:
operatore operazione &&
and ||
or !
not - NOTA BENE: spesso ha luogo casting implicito fra variabili intere e booleane:
in questo caso lo
0
risulta falso, mentre ogni altro valore intero risulta vero
-
se in una singola linea di un codice sorgente vengono effettuate diverse operazioni, il calcolatore le esegue da destra verso sinistra, rispettando l'ordine imposto da eventuali parentesi e una serie di regole di precedenza
-
ecco una tabella ridotta alle operazioni più comuni, gli operatori nelle righe più in alto hanno precedenza rispetto a quelli delle righe sottostanti
categorie di priorità |
---|
a++ , a[] |
++a , ! |
a*b , a/b , a%b |
a+b , a-b |
< , <= , > , >= |
== , != |
&& |
``` |
= , += , -= , *= , /= , %= |
- la lista completa delle precedenze si trova qui
- Le strutture di controllo sono il metodo che si utilizza nei linguaggi di programmazione procedurali
per gestire il flusso di istruzioni che il calcolatore deve eseguire.
Esistono tre tipi di strutture di controllo:
- sequenza: si tratta della configurazione di default: le istruzioni si susseguono una dopo l'altra
- selezione: a seconda che una condizione sia o meno soddisfatta, il calcolatore sceglie di eseguire diverse istruzioni
- ciclo: un insieme di istruzioni viene ripetuto un certo numero di volte, in funzione di un algoritmo che decide quando l'iterazione è terminata
- nel codice sorgente diverse istruzioni vengono raggruppate in insiemi chiamati scope, delimitati da parentesi graffe
- le variabili definite all'interno di uno scope rimangono definite solamente fino alla chiusura dello scope e vengono automaticamente rimosse alla chiusura della parentesi graffa
- singole istruzioni all'interno di una struttura di controllo possono essere sostituite da un intero scope
- la sequenza
if (condizione) {scope} else {scope alternativo}
realizza una selezione binaria, nella quale una istruzione o uno scope di istruzioni vengono eseguiti nel caso in cui venga soddisfatta una condizione booleana - opzionalmente, uno scope alternativo può essere eseguito
nel caso in cui la condizione risulti falsa
int num1 = 5 ; if (num1 % 2 == 0) { std::cout << num1 << " è pari\n" ; } else { std::cout << num1 << " è dispari\n" ; }
- la sequenza
switch ... case ... default
realizza una selezione fra molte opzioni, basata sul valore di una variabile:int num2 = 2 ; switch (num2) { case 1: // blocco di istruzioni std::cout << "uno\n" ; break; case 2: // blocco di istruzioni std::cout << "due\n" ; break; default: std::cout << "altri numeri\n" ; // blocco di istruzioni }
- nella struttura di controllo
switch (espressione)
vengono eseguite le istruzioni che stanno sotto la lineacase
tale per cuiespressione
è uguale al valore riportato dopo la parola chiavecase
- per evitare che vengano eseguite anche le istruzioni riportate dopo i
case
seguenti, solitamente si inserisce in ogni blocco di istruzioni il comandobreak
, che interrompe l'esecuzione dello scope- la situazione in cui le istruzioni eseguite non siano soltanto quelle
del
case
corrispondente al valore diespressione
prende il nome di fallthrough - il comando
break
può essere utilizzato anche per interrompere l'esecuzione di un ciclo - la presenza di un
break
non è obbligatoria
- la situazione in cui le istruzioni eseguite non siano soltanto quelle
del
- oltre ai vari
case
, si può aggiungere un ulteriore caso, che contiene istruzioni da svolgere nell'evenienza in cui nessuno deicase
venga soddisfatto, che viene etichettato con la parola chiavedefault
- il caso di
default
non è obbligatorio
- il caso di
-
la struttura di controllo
for ()
è un modo di implementare la struttura di controllo a ciclo, tipicamente nel caso in cui al ciclo sia associato un conteggio -
nella parentesi che segue il comando
for
sono solitamente riportate tre istruzioni, separati da un punto e virgola:- inizializzazione: dove viene inizializzata (e talvolta definita) una variabile che conta il numero di cicli, detta contatore
- controllo: dove si verifica se il numero di cicli abbia oltrepassato una determinata soglia
- incremento: dove si incrementa il contatore
int N = 10 ; for (int i = 0 ; i < N ; ++i) { std::cout << "il doppio di " << i << " vale: " << 2 * i << std::endl ; }
- le variabili definite fra parentesi rimangono definite soltanto all'interno dello scope del ciclo
- l'operazione di controllo viene compiuta prima di effettuare nell'iterazione corrispondente
- l'operazione di incremento viene compiuta dopo che è stata effettuata l'iterazione corrispondente
-
c'è molta libertà nella scrittura di un ciclo
for
: i tre campi fra parentesi possono anche essere vuoti ed il programma compila- utilizzare una scrittura non ortodossa del ciclo
for
può portare ad errori logici nel programma, che possono condurre a risultati inaffidabili in fase di esecuzione
- utilizzare una scrittura non ortodossa del ciclo
- la struttura di controllo
while ()
consente di implementare un ciclo fintanto che una condizione risulta vera - nella parentesi che segue l'istruzione
while
è codificata un'affermazione da verificare (condizione); se l'affermazione è vera (condizione soddisfatta), lo scope del ciclo viene effettuatoint N = 10 ; int i = 0 ; while (i < N) { std::cout << "il doppio di " << i << " vale: " << 2 * i << std::endl ; ++i ; }
- il controllo sulla condizione viene effettuato prima dell'esecuzione dell'iterazione corrispondente
- talvolta conviene che la condizione di proseguimento del ciclo venga verificata dopo l'esecuzione dello scope (ad esempio quando, prima della prima esecuzione dello scope, non ha senso effettuare il controllo)
- per ottenere questo comportamento,
si utilizza la sintassi
do { ... } while ()
do { std::cout << "il doppio di " << i << " vale: " << 2 * i << std::endl ; ++i ; } while (i < 2 * N) ;
- oltre a terminare quando la condizione di controllo diventa falsa, l'esecuzione di un ciclo può essere interrotta con due comandi:
- l'istruzione
break
che interrompe l'esecuzione dell'iterazione ed esce dal ciclo - l'istruzione
continue
che interrompe l'esecuzione dell'iterazione e passa a quella successiva
- l'esistenza di questi comandi permette di aggiungere controlli aggiuntivi
oltre alla condizione presente nella parentesi delle istruzioni
for
ewhile
, rendendo la programmazione più elastica - questo permette addirittura di lasciare i controlli nelle parentesi vuoti
ed effettuarli direttamente nello scope del ciclo
- aumenta considerevolmente il rischio che il ciclo non termini mai
- insiemi di istruzioni che svolgono un compito preciso e spesso ripetuto all'interno di uno o più programmi vengono solitamente raggruppate in funzioni, che si utilizzano come un singolo comando
- le funzioni hanno un nome, una o più variabili in ingresso
e restituiscono una sola variabile,
con il comando
return
- le funzioni vanno definite prima di essere chiamate e possono essere simultaneamente definite e implementate (come nel caso sottostante)
int raddoppia (int input_value) { return 2 * input_value ; } int main (int arcg, char ** argv) { for (int i = 0 ; i < 5 ; ++i) { std::cout << "il doppio di " << i << " vale: " << raddoppia (i) << std::endl ; } return 0 ; }
- una funzione che non restituisce alcun valore si definisce
con la parola chiave
void
(indicatore del tipo di ritorno invece diint
,float
...) ed al suo interno l'istruzionereturn
è immediatamente seguita da una virgolaint raddoppia (int input_value) { return 2 * input_value ; } void scriviAschermo (int input_value) { std::cout << "ecco il numero da scrivere: " << input_value << std::endl ; return ; } int main (int arcg, char ** argv) { for (int i = 0 ; i < 5 ; ++i) { scriviAschermo (raddoppia (i)) ; } return 0 ; }
- il nome di una funzione, insieme ai suoi tipi in ingresso, la identifica univocamente
- nello stesso programma non possono esistere due funzioni diverse con lo stesso nome e gli stessi tipi in ingresso
- funzioni con lo stesso nome, ma con tipi in ingresso diversi, possono invece coesistere:
questa proprietà del
C++
si chiama overloadingint raddoppia (int input_value) { return 2 * input_value ; } float raddoppia (float input_value) { return 2 * input_value ; }
- definire una funzione prima di essere chiamata è necessario per permettere il controllo grammaticale del codice sorgente da parte del compilatore
- per effettuare il controllo grammaticale, al compilatore è sufficiente conoscere il nome della funzione, la variabili in ingresso e quelle in uscita
- è quindi lecito anticipare questa informazione sotto forma di prototipo,
posticipando la scrittura dell'implementazione della funzione:
int raddoppia (int) ; int main (int arcg, char ** argv) { for (int i = 0 ; i < 5 ; ++i) { std::cout << "il doppio di " << i << " vale: " << raddoppia (i) << std::endl ; } return 0 ; } int raddoppia (int input_value) { return 2 * input_value ; }
- ciò permette di lasciare più in evidenza la funzione
main
rispetto alle altre - nella scrittura del prototipo, non è necessario indicare il nome delle variabili (ma è permesso)
- ciò permette di lasciare più in evidenza la funzione
- nel prototipo, oppure nell'implementazione, si possono assegnare valori di default alle variabili,
questi saranno i valori utilizzati dalla funzione per quella variabile
nel caso in cui il valore non venga passato al momento della chiamata della funzione
int raddoppia (int input_value = 0) { return 2 * input_value ; }
- il valore di default deve essere attribuito solamente in uno dei due luoghi
- in caso di funzioni con più variabili in ingresso, se ad una variabile viene assegnato un valore di default anche le variabili seguenti devono possederlo, per evitare situazioni di ambiguità
- funzioni che vengono utilizzate in più di un programma
main
possono essere scritte in un file diverso, in modo che non sia necessario riscriverle ogni volta - ogni funzione, dopo essere stata compilata, diventa un oggetto del compilatore
- dopo la compilazione, il linker (che è il terzo passaggio della compilazione) connette le varie funzioni per costruire l'eseguibile finale
- per permettere al compilatore di controllare la grammatica in fase di compilazione,
è sempre necessario mettere nel codice sorgente del
main
il prototipo delle funzioni - questa struttura viene realizzata tipicamente con tre file:
libreria.h
,libreria.cc
,main.cpp
libreria.h
: è il file che contiene il codice sorgente dei prototipi delle altre funzioni#ifndef libreria_h #define libreria_h int raddoppia (int) ; #endif
- le linee che iniziano con
#
sono istruzioni al preprocessore, si tratta del controllo di una condizione: se non è definita una variabile (#ifndef
) con il nomelibreria_h
, si considera tutto quello che segue fino ad#endif
- questo permette di non definire due volte il prototipo di una funzione, che genererebbe un errore di compilazione
- chiamato in generale header file
- le linee che iniziano con
libreria.cc
: è il file che contiene il codice sorgente delle funzioni secondarie#include "libreria.h" int raddoppia (int input_value) { return 2 * input_value ; }
- il codice sorgente include
libreria.h
per ereditare tutte le definizioni e gli altri#include
che stanno al suo interno
- il codice sorgente include
main.cpp
: è il file che contiene il codice sorgente della funzionemain
#include <iostream> #include "libreria.h" int main (int arcg, char ** argv) { for (int i = 0 ; i < 5 ; ++i) { std::cout << "il doppio di " << i << " vale: " << raddoppia (i) << std::endl ; } return 0 ; }
- il codice sorgente include
libreria.h
per ereditare tutte le definizioni e gli altri#include
che stanno al suo interno - il file
libreria.cc
non viene mai incluso, ma va indicato nel comando di compilazione:> c++ -o main_16 libreria.cc main_16.cpp
- il codice sorgente include
- NOTA BENE i file indicati fra parentesi angolate nelle istruzioni
#include
vengono cercati, dal preprocessore, in cartelle predefinite- se il nome del file è racchiuso tra doppi apici il file viene dapprima cercato nella cartella in cui si sta compilando e successivamente in cartelle predefinite
- si possono creare ed includere più di una libreria in un programma
- le librerie di
C++
funzionano in questo modo, con i codici sorgente delle librerie spesso già compilati ed il file da includere indicato fra parentesi angolate, come ad esempio#include <iostream>
suggerimenti |
---|
- è utile organizzare le proprie librerie per funzionalità, sia per strutturazione logica del proprio programma che per decidere che cosa includere e compilare in ogni programma
- l'utilizzo di una funzione comporta rallentamento nel programma, perché richiede al calcolatore di cercare in memoria la funzione di passarle gli argomenti e di recuperarne l'output, che sono operazioni aggiuntive
-
si può utilizzare la parola chiave
inline
, per chiedere al compilatore di sostituire la funzione con la sua implementazione. -
questo si fa (è vantaggioso) solo per funzioni piccole per cui il tempo di esecuzione delle operazioni codificate è confrontabile con il tempo di chiamata di una funzione non
inline
#ifndef libreria_h #define libreria_h inline int raddoppia (int input_value) { return 2 * input_value ; } #endif
- in questo caso, la funzione va definita prima del
main
, quindi nel file.h
- il compilatore può decidere di ignorare la parola chiave
inline
quando non sono soddisfatti determinati criteri (quindi l'istruzioneinline
è una richiesta o proposta fatta al compilatore, non un comando)
- in questo caso, la funzione va definita prima del
- la libreria
cmath
offre un'utile estensione delle operazioni matematiche - per poterla utilizzare, bisogna includerne il file
.h
corrispondente:#include <cmath>
- la libreria contiene funzioni e variabili notevoli
- la lista delle funzioni notevoli si trova qui, contiene funzioni trigonometriche, funzioni di potenza, iperboliche...
- un esempio di utilizzo delle funzioni presenti in
cmath
riguarda l'elevamento a potenza e la radice quadrata:float num = 4.5 ; std::cout << "quadrato di " << num << ": " << pow (num, 2) << "\n" ; num = pow (num, 2) ; std::cout << "radice di " << num << ": " << sqrt (num) << "\n" ; std::cout << "radice di " << num << ": " << pow (num, 0.5) << "\n" ;
- la funzione
pow
ha come primo argomento la base della potenza, come secondo argomento il suo esponente - utiilzzare l'espressione
num * num
invece dipow (num, 2)
è meno dispendioso in termini di tempo di esecuzione
- un'altra libreria di uso frequente
ctime
- l'istruzione
clock ()
restituisce il tempo di calcolo del processore consumato dal programma, espresso in cicli di calcolo- la frequenza dei cicli di calcolo è disponibile nella variabile
CLOCKS_PER_SEC
- la frequenza dei cicli di calcolo è disponibile nella variabile
- l'istruzione
ctime ()
resituisce il tempo trascorso a partire dal primo gennaio 1970
- se volessimo confrontare la velocità di esecuzione della funzione
pow (x, 2)
rispetto all'operazionex * x
potremmo ripetere entrambe le operazioni molte (N
) volte e misurare il tempo di calcolo nei due casi:double start = (double) clock () / CLOCKS_PER_SEC ; for (double i = 0; i < N; ++i) { test += pow (i, 2) ; } double stop = (double) clock () / CLOCKS_PER_SEC ; std::cout << "tempo di esecuzione per pow: " << stop - start << " secondi\n" ; start = (double) clock () / CLOCKS_PER_SEC ; for (double i = 0; i < N; ++i) { test += i * i ; } stop = (double) clock () / CLOCKS_PER_SEC ; std::cout << "tempo di esecuzione per i*i: " << stop - start << " secondi\n" ;
- si otterrebbe un risultato di questo tipo:
tempo di esecuzione per pow: 30.2506 secondi
tempo di esecuzione per i*i: 3.91943 secondi
- l'insieme di istruzioni che iniziano con il simbolo
#
si chiamano direttive al preprocessore perché vengono lette ed interpretate prima della fase di compilazione - si tratta di istruzioni che non riguardano la fase di compilazione del programma,
quindi macro e variabili del preprocessore sono concetti diversi
rispetto alle funzioni e variabili di
C++
- come abbiamo già visto,
questa istruzione viene utilizzata quando si scrivono librerie di funzioni
in un file separato da quello che contiene il codice sorgente del
main
program - seguendo questa direttiva,
il preprocessore sostituisce alla linea l'intero file riportato dopo
#include
- la direttiva
#define
definisce variabili del preprocessore - viene estensivamente utilizzata, unitamente al controllo booleano
#ifndef
(se non è definita), per impedire la doppia definizione del prototipo di una funzione e per impedire che si crei un circolo infinito di istruzioni#include
:#ifndef libreria_h #define libreria_h int raddoppia (int) ; #endif
- è invalso nell'uso utilizzare
#define
anche in sostituzione di variabili delC++
#define NUMERO 150
- si tratta di una cattiva pratica di programmazione, perché può portare a comportamenti inattesi del programma (inclusi problemi di compilazione) e rende difficile la fattorizzazione del codice sorgente
- in questo caso,
NUMERO
non è una variabile delC++
, bensì il preprocessore sostituisce il testoNUMERO
con il testo100
nel programma prima della compilazione - quando si utilizzano questi metodi poco ortodossi,
è buona regola utilizzare prassi sintattiche che differenzino chiaramente
le effettive variabili del
C++
dalle sostituzioni di testo del preprocessore, ad esempio scrivendone il nome interamente in caratteri maiuscoli
- si possono anche definire macro del preprocessore,
che sono espressioni che richiamano in forma il comportamento delle funzioni del
C++
#define quadrato(a) a*a
- utilizzare le macro del processore come funzioni
può produrre disastri, questo programma:
produce come output:
int main (int argc, char ** argv) { double numero = 3. ; double risposta = quadrato (numero + 1.) ; std::cout << "Il quadrato di " << numero + 1. << " vale " << risposta << "\n" ; return 0 ; }
infatti la sostituzione operata dal preprocessore genera questa istruzioneIl quadrato di 4 vale 7
double risposta = numero + 1. * numero + 1. ;
- perché un codice sorgente compili,
bisogna rispettare sintassi e grammatica del
C++
- perché un programma funzioni,
bisogna evitare errori logici nell'uso del
C++
e nella funzionalità degli algoritmi - perché un codice sorgente sia leggibile, è buona cosa seguire regole aggiuntive di buon senso nella scrittura
- scegliete nomi di funzioni e variabili lunghi ed autoesplicativi
- scegliete nomi che riguardino il ruolo effettivo di variabili e funzioni: ad esempio, se una variabile o una funzione servono nel programma per ottenere un determinato calcolo, ma abbiano funzionalità più ampia, il loro nome deve riflettere l'effettiva funzionalità
- scegliete un sistema consistente di nomenclatura, ad esempio:
- le funzioni iniziano con lettere minuscole, le variabili con lettere maiuscole
- nei nomi compposti da più parole, si divide il nome con un
_
(e.g.calcola_media
), oppure rendendo maiuscola ogni parola all'interno (e.g.calcolaMedia
)
- scegliete di scrivere scope piccoli: se il numero di istruzioni in uno scope è molto alto, spezzatelo in sotto-gruppi tramite funzioni
- una numero indicativo di istruzioni oltre il quale pensare se spezzare lo scope in funzioni è 7
- molti commenti nel codice sorgente aiutano a capire cosa facciano funzioni e variabili, descrivendo il loro contenuto o la loro funzionalità
- i commenti possono essere utilizzati per chiarire che cosa sta succedendo nel codice sorgente
- la spiegazione di eventuali formule utilizzate, oppure il link a pagine web di riferimento, possono essere inseriti nei commenti
- nel caso di scope molto lunghi,
per cui la chiusura di parentesi graffe non si vede insieme all'apertura,
si possono usare commenti per ricordare al lettore
quale scope sia chiuso da una graffa:
int main (int argc, char ** argv) { for (int i1 = 0 ; i1 < 100 ; ++i1) { /* tante istruzioni che si susseguono */ } // ciclo su i1 return 0 ; }
- indentare il codice sorgente coerentemente aiuta enormemente la lettura del codice sorgente
- tutte le istruzioni di uno stesso scope devono inizare alla medesima colonna
- quando si apre uno scope,
le istruzioni devono inziare in posizione rientrata
rispetto allo scope precedente:
scegliete una regola (ad es. 2 colonne) ed attenetevi rigorosamente a quella
- imparate ad usare con cognizione di causa il tasto
TAB
, oppure non utilizzatelo
- imparate ad usare con cognizione di causa il tasto
- decidete se aprire le parentesi graffe alla fine di una linea, oppure dopo essere andati accapo
- non chiudete parentesi graffe su una linea in cui ci sono istruzioni
- molto spesso pezzi di codice sorgente vengono riciclati copiandoli da programmi vecchi ed incollandoli in programmi nuovi
- per facilitare questa operazione e per rendere il codice sorgente più comprensibile,
è buona norma mantenere il più vicino possible tutte le istruzioni relative
ad un medesimo blocco logico del programma
- definire le variabili appena prima che vengano utilizzate (cioè NON tutte all'inizio del programma)
- NON sparpagliare per il programma istruzioni che logicamente si susseguono
- il
C++
distingue maiuscole da minuscole, quindinum
eNum
sono due variabili diverse - il
C++
non riconosce caratteri speciali come lettere accentate, quindi non usatele nei codici sorgente - esistono caratteri riservati al
C++
, come le virgolette, gli apici, o il backslash: nell'output a schermo evitate di utilizzarli, oppure fateli precedere con il carattere\\
, che dice al compilatore di non utilizzare il carattere successivo come carattere riservato
- quando si scrive un nuovo programma, è utile compilare il codice sorgente molto spesso
- ad ogni passaggio importante del programma, fate uno unit test,
cioè test di compilazione ed esecuzione
- scrivere la funzione
main
vuota è un passaggio importante (una unit da testare) - includere una libreria è una unit da testare
- aggiungere la definizione di una variabile rilevante per il programma è una unit da testare
- aggiungere una struttura di controllo vuota, cioè ancora prima di avere scritto istruzioni all'interno, è una unit da testare
- scrivere la funzione
- procedendo in questo modo, si semplifica molto l'identificazione delle cause di errori, perché sono tipicamente da ricercare soltanto nelle ultime aggiunte al codice sorgente
- in caso di programmi molto complessi,
è buona pratica preparare tante versioni della funzione
main
, dove ciascuna fa da test di un aspetto specifico del programma, ad esempio una funzionemain
per fare il test di ogni libreria creata
- durante la compilazione di un codice sorgente,
il compilatore (
c++
) prende in ingresso diversi parametri. Alcuni sono elencati qui:parametro ruolo *.cc
,*.cpp
codice sorgente dell'implementazione: deve esserci una sola funzione main
-o eseguibile
nome da assegnare all'eseguibile: valore di default è a.out
-O0
compilazione veloce e non ottimizzata, esecuzione lenta -O2
compilazione ottimizzata e lenta, esecuzione più veloce -O3
compilazione ottimizzata e lenta, esecuzione più veloce -Wall
accende tutti i Warning: il compilatore avvisa in caso di problemi sospetti -Werror
trasforma Warning in errori: il compilatore non compila se ci sono Warning - provate a confrontare l'uso di
pow (x, 2)
conx * x
a diversi livelli di ottimizzazione: che cosa cambia? - una lista completa di opzioni di compilazione si trova qui
- provate a confrontare l'uso di
- nel caso si utilizzino librerie non di default,
si può istruire il compilatore riguardo alla loro posizione nel computer:
parametro ruolo -l[linalg]
nome (*) della libreria precompilata (oggetto del compilatore) da linkare al programma -L[/path/to/shared-libraries]
la cartella dove stanno le librerie da linkare -I[/path/to/header-files]
dove stanno gli header file da includere - (*) il nome della libreria deve essere linalg.dll in Windows, liblinalg.so su Unix-like (e.g. Linux), linalg.dylib su MacOSX
- spesso (come vedremo per ROOT) pacchetti esterni forniscono anche un comando che prepara queste opzioni per il compilatore
- in caso di errore di compilazione, il compilatore mostra a schemo la descrizione degli errori che ha riscontrato
- il messaggio di errore è solitamente utile a capire il problema:
> c++ -o main_05 main_05.cpp main_05.cpp:10:12: error: cannot assign to variable 'numerò with const-qualified type 'const int' numero = numero + 1 ; ~~~~~~ ^ main_05.cpp:9:15: note: variable 'numerò declared const here const int numero = 0 ; ~~~~~~~~~~^~~~~~~~~~ 1 error generated.
- in questo caso, il compilatore indica il file con il codice sorgente problematico, la linea alla quale ha trovato un errore e la ragione per la quale ha ritenuto che ci fosse un problema
- spesso un singolo errore genera altri errori in cascata,
quindi è consigliato risolvere gli errori uno per uno, iniziando dal primo che si trova
- Gli esercizi relativi alla lezione si trovano qui