Introduzione a C++¶
Parte 2 · Linguaggi di programmazione · Capitolo 6 · Dispensa, volume 2 (PDF) · Esercizi: Introduzione a C++
Introduzione¶
Nel capitolo «Pseudocodice» gli algoritmi sono scritti in un linguaggio pensato per le persone: è preciso, ma non viene eseguito da un calcolatore. Per eseguire un algoritmo dobbiamo tradurlo in un linguaggio di programmazione. In questo capitolo introduciamo il linguaggio di programmazione C++, che usiamo per implementare gli algoritmi del corso.
Il capitolo segue lo stesso percorso del capitolo «Pseudocodice»:
-
le caratteristiche principali del linguaggio e la struttura di un programma;
-
variabili, tipi di dato e flusso di ingresso e uscita;
-
operatori, istruzioni condizionali e cicli;
-
gli array, la cui dimensione può essere fissata o decisa durante l'esecuzione;
-
le funzioni, con la funzione
maincome punto di ingresso di ogni programma; -
un algoritmo completo, la ricerca del valore massimo in un array, scritto in pseudocodice e in
C++; -
gli operatori bit a bit, che agiscono direttamente sulla rappresentazione binaria dei numeri interi.
Ogni frammento di codice del capitolo è stato compilato con il compilatore g++ (standard C++17) ed eseguito: gli output riportati negli esempi sono quelli stampati dai programmi.
Nello pseudocodice le componenti di un array \(a[1..n]\) sono indicizzate da \(1\) a \(n\); in C++ le componenti di un array di dimensione \(n\) sono indicizzate da \(0\) a \(n-1\). La componente \(a[i]\) dello pseudocodice corrisponde alla componente a[i-1] del codice C++.
6.1 Caratteristiche principali del linguaggio di programmazione¶
Le caratteristiche principali di C++ sono le seguenti.
-
C++è un linguaggio imperativo che dà accesso diretto alla memoria e alle risorse hardware del calcolatore. È molto efficiente per la programmazione di sistema a basso livello e allo stesso tempo supporta astrazioni di alto livello, il che lo rende versatile sia per le applicazioni in cui le prestazioni sono critiche sia per lo sviluppo software moderno. -
C++supporta le funzioni, che permettono un approccio strutturato e modulare alla programmazione. Le funzioni consentono di riutilizzare il codice e di organizzarlo meglio. -
Il linguaggio di programmazione
C++comprende un insieme predefinito di simboli riservati usati per dichiarare variabili, definire funzioni e specificare le istruzioni eseguibili. Il codice sorgente è costituito da una sequenza di operazioni atomiche eseguite in ordine, una dopo l'altra, su variabili di tipi di dato specifici. -
Il codice sorgente in
C++può contenere commenti, che servono come documentazione del codice e vengono ignorati dal compilatore. InC++, un commento su una sola riga inizia con il simbolo riservato “//”. I commenti su più righe sono racchiusi fra i simboli riservati “/*” e “*/”.
Esempio 6.1: commenti in C++
Questo è un esempio di commento su una sola riga:
Questo è un esempio di commento su più righe:
6.1.1 La struttura di un programma¶
Prima di descrivere le singole istruzioni, osserviamo lo scheletro che ogni programma ha in comune. Un programma C++ è un file di testo (con estensione .cpp) che viene tradotto da un compilatore in un file eseguibile. L'esecuzione del programma parte dalla funzione main, il punto di ingresso di ogni programma.
Esempio 6.2: lo scheletro di un programma
#include <iostream>
using namespace std;
int main()
{
// Qui si scrivono le istruzioni del programma
cout << "Ciao, mondo!" << endl;
return 0;
}
Se il codice sorgente è salvato nel file hello.cpp, lo si compila e lo si esegue con i comandi
e l'output è
Negli esempi seguenti scriviamo solo le istruzioni che contano: a meno che non contengano un programma completo, i frammenti di codice vanno inseriti fra le parentesi graffe di main.
Ogni istruzione di un codice sorgente C++ deve terminare con il simbolo riservato “;”.
6.2 Variabili e tipi di dato¶
Il codice sorgente opera su variabili, e ogni variabile ha un tipo di dato.
-
Le informazioni sono classificate in diversi tipi di dato. Le quattro famiglie fondamentali di tipi di dato sono:
-
Il tipo di dato “
int” rappresenta un numero intero, cioè un numero scritto senza la virgola decimale. Comprende i numeri interi positivi e negativi, e lo zero. -
Il tipo di dato “
bool” rappresenta un valore booleano:trueofalse. -
Il tipo di dato “
float” rappresenta un numero in virgola mobile, che comprende i numeri scritti con la virgola decimale. Il tipo di dato “double” rappresenta un numero in virgola mobile a precisione doppia, che offre maggiore accuratezza e un intervallo più ampio difloat. -
Il tipo di dato “
char” rappresenta un singolo carattere, come una lettera, una cifra o un simbolo, racchiuso fra apici singoli (ad esempio'A','7','!').
-
-
Una sequenza di caratteri racchiusa fra virgolette doppie (ad esempio
"Ada") è memorizzata in una variabile di tipo “string”, fornito dalla libreria standard<string>. -
Una variabile funziona come un contenitore che memorizza un valore di un tipo di dato specifico per un uso successivo. Ogni variabile ha un nome univoco e contiene un solo valore in ogni istante.
-
Alle variabili si assegnano valori con l'operatore di assegnamento “
=”. Quando si assegna un nuovo valore, il valore precedente viene sovrascritto.
Le variabili devono essere dichiarate prima dell'uso, specificandone il tipo di dato.
Esempio 6.3: dichiarazioni di variabili e assegnamenti di valori
int age = 25; // Variabile intera
bool isRaining = false; // Variabile booleana
float smallNumber = 3.14; // Variabile in virgola mobile
double largeNumber = 123456.789; // Variabile in virgola mobile a doppia precisione
char character = 'a'; // Variabile carattere
string name = "Ada"; // Variabile stringa
age = 26; // Il valore precedente 25 viene sovrascritto
cout << age << " " << isRaining << " " << smallNumber << " "
<< largeNumber << " " << character << " " << name << endl;
Output:
Il valore booleano false viene visualizzato come 0, e il numero 123456.789 viene visualizzato come 123457: per impostazione predefinita, cout visualizza al più sei cifre significative (il valore memorizzato nella variabile non cambia, e il numero di cifre visualizzate può essere scelto, come vediamo ora).
6.3 Flusso di ingresso e uscita¶
Per visualizzare sullo schermo i valori delle variabili, un programma usa la libreria del flusso di ingresso e uscita.
- L'istruzione di inclusione “
#include <iostream>” permette di usare inC++la libreria del flusso di ingresso e uscita.
-
I valori delle variabili possono essere visualizzati come testo in uscita sullo schermo usando il flusso di uscita “
std::cout”. L'operatore di inserimento “< <” serve a inviare i dati al flusso di uscita. -
In una sola istruzione
coutsi possono visualizzare più valori concatenandoli con l'operatore “< <”. Il testo va racchiuso fra virgolette doppie “"”, mentre i valori delle variabili si scrivono direttamente. -
Il manipolatore “
std::endl” si usa constd::coutper andare a capo sullo schermo e svuotare il buffer di uscita.
Esempio 6.5: stampe sullo schermo
Output:
-
L'istruzione “
using namespace std;” semplifica l'uso delle funzioni della libreria standard, eliminando la necessità di anteporrestd::. D'ora in poi la usiamo sempre, e scriviamocouteendl. -
L'istruzione di inclusione “
#include <iomanip>” fornisce i manipolatori “fixed” e “setprecision(k)”. Quando sono scritti insieme in un'istruzionecout, visualizzano i numeri in virgola mobile che seguono con esattamente \(k\) cifre dopo la virgola decimale.
6.4 Operatori¶
Gli operatori sono simboli che eseguono operazioni su variabili e valori. In base alla loro funzione si classificano in cinque categorie principali: operatori aritmetici, di assegnamento, di incremento e decremento, relazionali e logici.
6.4.1 Operatori aritmetici¶
Operatori aritmetici, usati per eseguire le operazioni matematiche di base:
-
+(addizione) \(\rightsquigarrow\) somma due valori. -
-(sottrazione) \(\rightsquigarrow\) sottrae un valore da un altro. -
*(moltiplicazione) \(\rightsquigarrow\) moltiplica due valori. -
/(divisione) \(\rightsquigarrow\) divide un valore per un altro. -
%(modulo) \(\rightsquigarrow\) restituisce il resto di una divisione.
Esempio 6.6: operatori aritmetici
int a = 10, b = 3;
cout << "Addizione: " << (a + b) << endl;
cout << "Sottrazione: " << (a - b) << endl;
cout << "Moltiplicazione: " << (a * b) << endl;
cout << "Divisione: " << (a / b) << endl; // Divisione intera
cout << "Resto: " << (a % b) << endl; // Resto della divisione
Output:
-
L'operatore di divisione
/esegue la divisione intera quando è usato con numeri interi: il risultato di10 / 3è3. Per ottenere un risultato in virgola mobile, almeno un operando deve essere di tipofloatodouble. Ad esempio,10.0 / 3restituisce3.33333invece di3. -
Un valore di tipo
intviene convertito in un valore di tipodoublescrivendo “(double)” davanti ad esso (conversione di tipo). Ad esempio, seaebsono variabili di tipoint, l'espressione(double)a / besegue la divisione in virgola mobile.
Esempio 6.7: divisione intera e divisione in virgola mobile
int a = 10, b = 3;
cout << a / b << endl; // Divisione intera: 3
cout << 10.0 / 3 << endl; // Divisione in virgola mobile
cout << (double)a / b << endl; // Conversione di a in double
cout << fixed << setprecision(2) << (double)a / b << endl; // Due cifre decimali
Output (l'ultima riga richiede #include <iomanip>):
6.4.2 Operatori di assegnamento¶
Operatori di assegnamento, usati per assegnare valori alle variabili:
-
=(assegnamento) \(\rightsquigarrow\) assegna un valore a una variabile. -
+=(assegnamento con addizione) \(\rightsquigarrow\) somma un valore a una variabile e assegna il risultato. -
-=(assegnamento con sottrazione) \(\rightsquigarrow\) sottrae un valore da una variabile e assegna il risultato. -
*=(assegnamento con moltiplicazione) \(\rightsquigarrow\) moltiplica una variabile per un valore e assegna il risultato. -
/=(assegnamento con divisione) \(\rightsquigarrow\) divide una variabile per un valore e assegna il risultato. -
%=(assegnamento con modulo) \(\rightsquigarrow\) applica l'operazione di modulo e assegna il risultato.
Esempio 6.8: operatori di assegnamento
int x = 10; // Assegnamento
cout << "Valore iniziale: " << x << endl;
x += 5; // Equivale a: x = x + 5
cout << "Dopo += 5: " << x << endl;
x -= 3; // Equivale a: x = x - 3
cout << "Dopo -= 3: " << x << endl;
x *= 2; // Equivale a: x = x * 2
cout << "Dopo *= 2: " << x << endl;
x /= 4; // Equivale a: x = x / 4
cout << "Dopo /= 4: " << x << endl;
x %= 3; // Equivale a: x = x % 3
cout << "Dopo %= 3: " << x << endl;
Output:
6.4.3 Operatori di incremento e decremento¶
Operatori di incremento e decremento:
-
++(incremento) \(\rightsquigarrow\) aumenta di 1 il valore di una variabile. -
- -(decremento) \(\rightsquigarrow\) diminuisce di 1 il valore di una variabile.
-
L'operatore di incremento
++si può usare in due forme:-
pre-incremento (“
++x”): incrementa il valore prima di restituirlo; -
post-incremento (“
x++”): restituisce prima il valore, poi lo incrementa.
La stessa logica vale per l'operatore di decremento (
- -xex- -). -
Esempio 6.9: operatori di incremento e decremento
int x = 5;
cout << "Valore iniziale: " << x << endl; // Stampa 5
cout << "Pre-incremento (++x): " << ++x << endl; // Stampa 6
cout << "Post-incremento (x++): " << x++ << endl; // Stampa 6
cout << "Valore dopo il post-incremento: " << x << endl; // Stampa 7
cout << "Pre-decremento (--x): " << --x << endl; // Stampa 6
cout << "Post-decremento (x--): " << x-- << endl; // Stampa 6
cout << "Valore dopo il post-decremento: " << x << endl; // Stampa 5
Output:
6.4.4 Operatori relazionali¶
Operatori relazionali, usati per confrontare valori e restituire un risultato booleano:
-
==(uguale a) \(\rightsquigarrow\) verifica se due valori sono uguali. -
!=(diverso da) \(\rightsquigarrow\) verifica se due valori sono diversi. -
<(minore di) \(\rightsquigarrow\) verifica se un valore è minore di un altro. -
>(maggiore di) \(\rightsquigarrow\) verifica se un valore è maggiore di un altro. -
<=(minore o uguale a) \(\rightsquigarrow\) verifica se un valore è minore o uguale a un altro. -
>=(maggiore o uguale a) \(\rightsquigarrow\) verifica se un valore è maggiore o uguale a un altro.
-
Gli operatori relazionali restituiscono un risultato booleano:
-
true(visualizzato come1) se la condizione è soddisfatta; -
false(visualizzato come0) se la condizione non è soddisfatta.
Questi operatori sono usati comunemente nelle istruzioni condizionali e nei cicli.
-
Esempio 6.10: operatori relazionali
int a = 10, b = 5;
cout << "a == b: " << (a == b) << endl; // Uguale a
cout << "a != b: " << (a != b) << endl; // Diverso da
cout << "a < b: " << (a < b) << endl; // Minore di
cout << "a > b: " << (a > b) << endl; // Maggiore di
cout << "a <= b: " << (a <= b) << endl; // Minore o uguale a
cout << "a >= b: " << (a >= b) << endl; // Maggiore o uguale a
Output:
6.4.5 Operatori logici¶
Operatori logici, usati per le operazioni logiche, spesso nelle istruzioni condizionali:
-
&&(AND logico) \(\rightsquigarrow\) restituiscetruese entrambe le condizioni sono vere. -
||(OR logico) \(\rightsquigarrow\) restituiscetruese almeno una condizione è vera. -
!(NOT logico) \(\rightsquigarrow\) inverte il valore booleano.
- Come gli operatori relazionali, gli operatori logici restituiscono valori booleani,
true(1) ofalse(0), e sono usati comunemente nelle istruzioni condizionali e nei cicli.
Esempio 6.11: operatori logici
bool condition1 = true;
bool condition2 = false;
cout << "condition1: " << condition1 << endl;
cout << "condition2: " << condition2 << endl;
// AND logico: vero solo se entrambe le condizioni sono vere
cout << "condition1 && condition2: " << (condition1 && condition2) << endl;
// OR logico: vero se almeno una condizione è vera
cout << "condition1 || condition2: " << (condition1 || condition2) << endl;
// NOT logico: inverte il valore booleano
cout << "!condition1: " << !condition1 << endl;
cout << "!condition2: " << !condition2 << endl;
Output:
6.5 Istruzioni condizionali¶
Gli operatori relazionali e logici producono le condizioni che decidono quali istruzioni vengono eseguite.
-
Le istruzioni condizionali permettono di eseguire blocchi di codice diversi in base a condizioni specifiche. L'istruzione
ifè la struttura di controllo fondamentale di questo tipo. -
L'istruzione
ifvaluta una condizione scritta fra parentesi tonde(). Se la condizione è vera, viene eseguito il blocco di codice fra le parentesi graffe{}. Se la condizione è falsa, il programma salta il blocco ed esegue il blocco di codice fra le parentesi graffe{}della parteelse, se presente.
Esempio 6.12: istruzione condizionale
int number = 10;
// Controlla se il numero è positivo
if (number > 0)
{
cout << "Il numero è positivo." << endl;
}
else
{
cout << "Il numero non è positivo." << endl;
}
Output:
6.5.1 Operatore ternario (operatore condizionale)¶
-
L'operatore ternario
?:offre un modo compatto per scrivere semplici istruzioniif-else. -
È l'unico operatore di
C++che ha tre operandi. -
Sintassi:
condizione ? valore_se_vera : valore_se_falsa -
Se la condizione è vera, l'espressione vale
valore_se_vera; altrimenti valevalore_se_falsa.
Esempio 6.13: operatore ternario
int number = 10;
// Con l'istruzione if-else
string result1;
if (number > 0)
result1 = "positivo";
else
result1 = "non positivo";
cout << "Il numero è " << result1 << endl;
// Con l'operatore ternario (forma compatta)
string result2 = (number > 0) ? "positivo" : "non positivo";
cout << "Il numero è " << result2 << endl;
Output:
Esempio 6.14: calcolo del massimo fra due numeri
int a = 15, b = 20;
// Con l'istruzione if-else
int max1;
if (a > b)
max1 = a;
else
max1 = b;
cout << "Massimo (if-else): " << max1 << endl;
// Con l'operatore ternario (forma compatta)
int max2 = (a > b) ? a : b;
cout << "Massimo (ternario): " << max2 << endl;
Output:
Esempio 6.15: verifica se un numero è pari o dispari
int n = 7;
// Con l'operatore ternario
string parity = (n % 2 == 0) ? "pari" : "dispari";
cout << n << " è " << parity << endl;
Output:
Esempio 6.16: assegnamento del valore assoluto
int x = -5;
// Con l'operatore ternario per calcolare il valore assoluto
int absValue = (x >= 0) ? x : -x;
cout << "Il valore assoluto di " << x << " è " << absValue << endl;
Output:
Esempio 6.17: operatori ternari annidati
int score = 85;
// Operatori ternari annidati per assegnare un voto
string grade = (score >= 90) ? "A" :
(score >= 80) ? "B" :
(score >= 70) ? "C" :
(score >= 60) ? "D" : "F";
cout << "Punteggio: " << score << ", Voto: " << grade << endl;
Output:
Le condizioni sono valutate dall'alto verso il basso: score >= 90 è falsa e score >= 80 è vera, quindi il voto è "B". Per la leggibilità, le condizioni complesse è meglio gestirle con istruzioni if-else.
6.6 Cicli¶
Le istruzioni condizionali scelgono se un blocco di codice viene eseguito; i cicli decidono quante volte viene eseguito.
- I cicli servono a eseguire ripetutamente un blocco di codice finché una condizione è soddisfatta. Sono essenziali per automatizzare i compiti ripetitivi e ridurre il codice ridondante. In
C++, i cicli più usati sono il ciclowhilee il ciclofor.
6.6.1 Il ciclo while¶
- Il ciclo
whileesegue un blocco di codice finché la condizione fra parentesi tonde()vale vero. Quando la condizione è falsa, il ciclo termina.
Esempio 6.18: ciclo while
int i = 1; // Inizializzazione
while (i <= 5) // Condizione
{
cout << "Numero: " << i << endl;
i++; // Incremento
}
Output:
La condizione i <= 5 viene valutata sei volte: cinque volte è vera, e la sesta volta (i uguale a 6) è falsa e il ciclo termina.
6.6.2 Il ciclo for¶
- Il ciclo
forè una struttura di ciclo compatta che combina in un'unica istruzione inizializzazione, condizione e incremento (o decremento).
Esempio 6.19: ciclo for
for (int i = 1; i <= 5; i++) // Inizializzazione, condizione, incremento
{
cout << "Numero: " << i << endl;
}
L'output è lo stesso del ciclo while dell'Esempio 6.18.
6.6.3 Le istruzioni break e continue¶
-
L'istruzione
breaktermina immediatamente il ciclo. -
L'istruzione
continuesalta il resto dell'iterazione corrente e passa alla successiva.
Esempio 6.20: break e continue
for (int i = 1; i <= 10; i++)
{
if (i % 2 == 0)
{
continue; // Salta i numeri pari
}
if (i > 7)
{
break; // Interrompe il ciclo al primo numero dispari maggiore di 7
}
cout << i << " ";
}
cout << endl;
Output:
I valori pari di i sono saltati da continue; quando i è uguale a 9 l'istruzione break termina il ciclo, e il valore 10 non viene mai raggiunto.
6.7 Array¶
I cicli diventano davvero utili quando scorrono collezioni di valori. Una collezione di variabili dello stesso tipo di dato è memorizzata in un array.
6.7.1 Array di dimensione fissa¶
-
Un array i cui valori sono noti quando si scrive il programma può essere dichiarato elencando i suoi valori fra parentesi graffe. La dimensione dell'array è il numero di valori elencati, e non può cambiare durante l'esecuzione.
-
Ogni elemento di un array è identificato dal suo indice, che parte da zero, scritto fra parentesi quadre “
[]”: gli elementi di un array di dimensione \(n\) sonob[0],b[1], …,b[n-1].
Esempio 6.21: array di dimensione fissa
int b[] = {9, 12, 3, 16}; // Array di 4 interi
int n = 4;
b[2] = 5; // Assegna un nuovo valore al terzo elemento
for (int i = 0; i < n; i++)
{
cout << b[i] << " ";
}
cout << endl;
Output:
- Un array bidimensionale di dimensione fissa si dichiara con due dimensioni, il numero di righe e il numero di colonne, e i suoi valori si elencano riga per riga:
int c[2][3] = {{3, 1, 4}, {1, 5, 9}};dichiara un array con \(2\) righe e \(3\) colonne, ec[1][2]è l'elemento della seconda riga e terza colonna, uguale a \(9\).
6.7.2 Array di dimensione dinamica¶
-
Un array di dimensione dinamica è una collezione di variabili dello stesso tipo di dato, memorizzate in locazioni di memoria contigue. La dimensione dell'array viene determinata durante l'esecuzione dell'algoritmo.
-
Il simbolo riservato “
*” si usa per dichiarare un puntatore, che memorizza l'indirizzo di memoria del primo elemento di un array di dimensione dinamica. L'operatorenewsi usa per allocare la memoria di un array di dimensione dinamica, e deve specificare il tipo di dato degli elementi insieme alla dimensione dell'array fra parentesi quadre “[]”. -
Ogni elemento di un array di dimensione dinamica è identificato dal suo indice, che parte da zero, scritto fra parentesi quadre “
[]”. Un array di dimensione dinamica richiede la deallocazione della memoria con l'operatoredelete[], per liberare la memoria ed evitare perdite di memoria (memory leak).
Figura 6.1: Un array di dimensione dinamica \(n\): il puntatore a memorizza l'indirizzo del primo elemento, e gli elementi sono indicizzati da \(0\) a \(n-1\).
Esempio 6.22: array di dimensione dinamica
int n = 5; // Dimensione dell'array
int* a = new int[n]; // Allocazione di un array di dimensione n
for (int i = 0; i < n; i++)
{
a[i] = i * 2; // Esempio di assegnamento di valori
}
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
cout << endl;
delete[] a; // Libera la memoria allocata
Output:
-
Gli array di dimensione dinamica possono essere multidimensionali. Ad esempio, il simbolo riservato “
**” si usa per dichiarare un puntatore a puntatore, che memorizza l'indirizzo di memoria del primo elemento di un array bidimensionale di dimensione dinamica: un array di \(m\) puntatori, ciascuno dei quali punta a una riga di \(n\) elementi. -
L'operatore
newsi usa anche per allocare la memoria degli array bidimensionali di dimensione dinamica: prima l'array degli \(m\) puntatori alle righe, poi ogni riga. Ogni elemento di un array bidimensionale di dimensione dinamica è identificato da due indici, entrambi a partire da zero, scritti fra parentesi quadre “[][]”. La memoria viene liberata in ordine inverso: prima ogni riga, poi l'array dei puntatori alle righe.
Esempio 6.23: array bidimensionali di dimensione dinamica
int m = 3; // Numero di righe
int n = 4; // Numero di colonne
int** matrix = new int*[m]; // Allocazione di una matrice di dimensione m per n
for (int i = 0; i < m; i++)
{
matrix[i] = new int[n]; // Alloca ogni riga
}
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
matrix[i][j] = i * j; // Esempio di assegnamento di valori
cout << matrix[i][j] << " ";
}
cout << endl;
}
for (int i = 0; i < m; i++) // Libera la memoria allocata
{
delete[] matrix[i]; // Libera ogni riga
}
delete[] matrix; // Libera i puntatori alle righe
Output:
6.8 La funzione main e le funzioni definite dall'utente¶
Come nello pseudocodice, un algoritmo è scritto come una funzione che riceve l'input e restituisce l'output.
-
Le funzioni permettono di scrivere codice modulare e riutilizzabile raggruppando le istruzioni in un'unica unità. Una funzione può essere chiamata più volte per eseguire lo stesso compito, riducendo la ridondanza e migliorando l'organizzazione del codice.
-
Di solito le funzioni restituiscono un valore e accettano più parametri in ingresso. Quando si chiama una funzione, gli argomenti vengono passati ai parametri.
-
Una funzione in
C++è costituita da:-
un tipo di ritorno: specifica il tipo del valore restituito dalla funzione (
voidse non viene restituito nulla); -
un nome della funzione: l'identificatore univoco della funzione;
-
una lista dei parametri (facoltativa): specifica i valori in ingresso della funzione;
-
un corpo: contiene il blocco di codice eseguito quando la funzione viene chiamata;
-
un'istruzione di ritorno (facoltativa): serve a restituire un valore se la funzione restituisce un valore.
-
-
La funzione
mainè il punto di ingresso di un programmaC++. Viene eseguita per prima quando il programma inizia l'esecuzione. Ogni programmaC++deve contenere esattamente una funzionemain. Il suo valore di ritorno0comunica al sistema operativo che il programma è terminato correttamente. -
Una funzione deve essere definita (o dichiarata) prima del punto in cui viene chiamata: per questo motivo le funzioni definite dall'utente si scrivono prima di
main. -
Il codice sorgente
C++può anche essere compilato ed eseguito online, ad esempio al seguente indirizzo: www.mycompiler.io/it/online-c++-compiler
Esempio 6.24: funzioni
#include <iostream>
using namespace std;
// Definizione della funzione
int addition(int a, int b)
{
return a + b; // Restituisce la somma
}
// Definizione della funzione
int multiplication(int a, int b)
{
return a * b; // Restituisce il prodotto
}
int main()
{
int sum = addition(5, 10); // Chiamata della funzione
cout << "Somma: " << sum << endl;
int prod = multiplication(5, 10); // Chiamata della funzione
cout << "Prodotto: " << prod << endl;
return 0;
}
Output:
6.8.1 Parametri passati per valore e per riferimento¶
Una funzione restituisce al più un valore. Quando una funzione deve produrre più di un risultato, oppure deve modificare una variabile del chiamante, i suoi parametri vengono passati per riferimento.
-
Per impostazione predefinita, un parametro è passato per valore: la funzione riceve una copia dell'argomento, e le modifiche fatte alla copia non influenzano la variabile del chiamante.
-
Un parametro è passato per riferimento scrivendo il simbolo riservato “
&” dopo il suo tipo di dato (ad esempioint& x): il parametro è un altro nome per la variabile del chiamante, e ogni modifica fatta dalla funzione è visibile al chiamante. -
Un array è passato a una funzione tramite il puntatore al suo primo elemento (ad esempio
int *a), insieme alla sua dimensione: la funzione lavora direttamente sugli elementi dell'array del chiamante.
Esempio 6.25: parametri passati per valore e per riferimento
#include <iostream>
using namespace std;
void incrementByValue(int x)
{
x = x + 1; // Modifica solo la copia locale
}
void incrementByReference(int& x)
{
x = x + 1; // Modifica la variabile del chiamante
}
void fillWithZeros(int n, int* a)
{
for (int i = 0; i < n; i++)
{
a[i] = 0; // Modifica l'array del chiamante
}
}
int main()
{
int k = 5;
incrementByValue(k);
cout << "Dopo incrementByValue: " << k << endl;
incrementByReference(k);
cout << "Dopo incrementByReference: " << k << endl;
int b[] = {4, 7, 1};
fillWithZeros(3, b);
cout << "Array: " << b[0] << " " << b[1] << " " << b[2] << endl;
return 0;
}
Output:
6.9 Un algoritmo completo: il valore massimo in un array¶
Mettiamo ora insieme tutti i pezzi: una funzione, un ciclo, un'istruzione condizionale e un array di dimensione dinamica.
Consideriamo il problema di trovare il valore massimo in un array \(a\) di \(n \ge 1\) numeri interi.
- Questo è lo pseudocodice dell'algoritmo
FindMaximumdel capitolo «Pseudocodice»: le componenti sono indicizzate da \(1\) a \(n\).
Algoritmo: FindMaximum
-
Questa è l'implementazione in
C++: le componenti sono indicizzate da \(0\) a \(n-1\), quindi l'inizializzazione usaa[0]e il ciclo parte dai = 1. La funzione assume \(n \ge 1\), come fa lo pseudocodice: per \(n = 0\) l'elementoa[0]non esiste.#include <iostream> using namespace std; // Funzione che trova il valore massimo di un array int ArrayMaxValue(int n, int *a) { int max = a[0]; // Inizializza con il primo elemento for (int i = 1; i < n; i++) { if (a[i] > max) { max = a[i]; // Aggiorna il massimo } } return max; } int main() { int n = 7; // Dimensione dell'array int* a = new int[n]; // Allocazione di un array di dimensione n int values[] = {9, 12, 3, 16, 19, 11, 12}; for (int i = 0; i < n; i++) { a[i] = values[i]; } int max = ArrayMaxValue(n, a); cout << "Valore massimo: " << max << endl; delete[] a; // Libera la memoria allocata return 0; }Output:
Esempio 6.26: esecuzione passo per passo
In ogni tabella, la colonna \(i\) è l'indice del ciclo C++ e la colonna \(i+1\) è l'indice corrispondente dello pseudocodice. La prima riga è l'inizializzazione max = a[0].
Istanza 1: \(a = [9, 12, 3, 16, 19, 11, 12]\), \(n=7\)
\(i\) (C++) |
\(i+1\) (pseudocodice) | a[i] |
a[i] > max? |
max |
|---|---|---|---|---|
| – | – | – | – | \(9\) |
| \(1\) | \(2\) | \(12\) | Sì | \(12\) |
| \(2\) | \(3\) | \(3\) | No | \(12\) |
| \(3\) | \(4\) | \(16\) | Sì | \(16\) |
| \(4\) | \(5\) | \(19\) | Sì | \(19\) |
| \(5\) | \(6\) | \(11\) | No | \(19\) |
| \(6\) | \(7\) | \(12\) | No | \(19\) |
Return: max \(= 19\)
Istanza 2: \(a = [3, 7, 2, 9, 1]\), \(n=5\)
\(i\) (C++) |
\(i+1\) (pseudo-code) | a[i] |
a[i] > max? |
max |
|---|---|---|---|---|
| – | – | – | – | \(3\) |
| \(1\) | \(2\) | \(7\) | Sì | \(7\) |
| \(2\) | \(3\) | \(2\) | No | \(7\) |
| \(3\) | \(4\) | \(9\) | Sì | \(9\) |
| \(4\) | \(5\) | \(1\) | No | \(9\) |
Return: max \(= 9\)
6.10 Operatori bit a bit¶
I numeri interi gestiti da un programma sono memorizzati in binario, come descritto nel capitolo «Rappresentazione dei numeri». C++ fornisce operatori che agiscono direttamente su queste cifre binarie.
-
Gli operatori bit a bit eseguono operazioni sui singoli bit dei valori interi. Sono essenziali per la programmazione a basso livello, per l'ottimizzazione e per lavorare con le rappresentazioni binarie.
-
I bit sono numerati da destra, a partire dalla posizione \(0\) (il bit meno significativo).
Operatori bit a bit in C++:
-
&(AND bit a bit) \(\rightsquigarrow\) restituisce 1 solo se entrambi i bit corrispondenti sono 1. -
|(OR bit a bit) \(\rightsquigarrow\) restituisce 1 se almeno uno dei bit corrispondenti è 1. -
(XOR bit a bit) \(\rightsquigarrow\) restituisce 1 se i bit corrispondenti sono diversi. -
~(NOT bit a bit) \(\rightsquigarrow\) inverte tutti i bit (0 diventa 1, 1 diventa 0). -
< <(scorrimento a sinistra) \(\rightsquigarrow\) sposta i bit a sinistra di un numero specificato di posizioni. -
> >(scorrimento a destra) \(\rightsquigarrow\) sposta i bit a destra di un numero specificato di posizioni.
6.10.1 AND bit a bit (&)¶
-
L'operatore AND bit a bit confronta ogni bit di due numeri e restituisce 1 solo se entrambi i bit sono 1.
-
Tavola di verità dell'AND:
| Bit A | Bit B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Esempio 6.27: AND bit a bit
int a = 12; // Binario: 1100
int b = 10; // Binario: 1010
int result = a & b; // Binario: 1000 (decimale: 8)
cout << "a & b = " << result << endl; // Stampa: 8
Operazione bit per bit:
| Decimale | |||||
|---|---|---|---|---|---|
a |
1 | 1 | 0 | 0 | 12 |
b |
1 | 0 | 1 | 0 | 10 |
a & b |
1 | 0 | 0 | 0 | 8 |
6.10.2 OR bit a bit (|)¶
-
L'operatore OR bit a bit confronta ogni bit di due numeri e restituisce 1 se almeno uno dei bit è 1.
-
Tavola di verità dell'OR:
| Bit A | Bit B | A | B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
Esempio 6.28: OR bit a bit
int a = 12; // Binario: 1100
int b = 10; // Binario: 1010
int result = a | b; // Binario: 1110 (decimale: 14)
cout << "a | b = " << result << endl; // Stampa: 14
Operazione bit per bit:
| Decimale | |||||
|---|---|---|---|---|---|
a |
1 | 1 | 0 | 0 | 12 |
b |
1 | 0 | 1 | 0 | 10 |
a | b |
1 | 1 | 1 | 0 | 14 |
6.10.3 XOR bit a bit ()¶
-
L'operatore XOR bit a bit (OR esclusivo) confronta ogni bit di due numeri e restituisce 1 se i bit sono diversi.
-
Tavola di verità dello XOR:
| Bit A | Bit B | A B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Esempio 6.29: XOR bit a bit
int a = 12; // Binario: 1100
int b = 10; // Binario: 1010
int result = a ^ b; // Binario: 0110 (decimale: 6)
cout << "a ^ b = " << result << endl; // Stampa: 6
Operazione bit per bit:
| Decimale | |||||
|---|---|---|---|---|---|
a |
1 | 1 | 0 | 0 | 12 |
b |
1 | 0 | 1 | 0 | 10 |
a b |
0 | 1 | 1 | 0 | 6 |
6.10.4 NOT bit a bit (~)¶
-
L'operatore NOT bit a bit inverte tutti i bit di un numero (0 diventa 1, 1 diventa 0).
-
Questo è un operatore unario (opera su un solo operando).
-
Il risultato dipende dal numero di bit usati per rappresentare l'intero. Nell'esempio usiamo il tipo di dato
unsigned char, che memorizza i numeri naturali da \(0\) a \(255\) con \(8\) bit; la conversione(int)visualizza il risultato come numero invece che come carattere.
Esempio 6.30: NOT bit a bit
unsigned char a = 12; // Binario (8 bit): 00001100
unsigned char result = ~a; // Binario: 11110011 (decimale: 243)
cout << "~a = " << (int)result << endl; // Stampa: 243
Operazione bit per bit (usando la rappresentazione a 8 bit):
| Decimale | |||||||||
|---|---|---|---|---|---|---|---|---|---|
a |
0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 12 |
~a |
1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 243 |
Infatti \(243 = 255 - 12\): invertire tutti gli \(8\) bit di un numero \(x\) dà \(255 - x\).
6.10.5 Scorrimento a sinistra (< <)¶
-
L'operatore di scorrimento a sinistra sposta tutti i bit a sinistra di un numero specificato di posizioni.
-
Vengono aggiunti zeri sul lato destro.
-
Scorrere a sinistra di \(k\) posizioni equivale a moltiplicare per \(2^k\) (purché il risultato sia rappresentabile dal tipo di dato).
Esempio 6.31: scorrimento a sinistra
int a = 5; // Binario: 0101
int result = a << 2; // Scorrimento a sinistra di 2 posizioni: 10100 (decimale: 20)
cout << "a << 2 = " << result << endl; // Stampa: 20
// Equivale a: 5 * 2^2 = 5 * 4 = 20
Operazione bit per bit:
| Decimale | Equivalente | |||||||
|---|---|---|---|---|---|---|---|---|
a |
0 | 0 | 0 | 1 | 0 | 1 | 5 | \(5\) |
a < < 1 |
0 | 0 | 1 | 0 | 1 | 0 | 10 | \(5 \times 2^1\) |
a < < 2 |
0 | 1 | 0 | 1 | 0 | 0 | 20 | \(5 \times 2^2\) |
a < < 3 |
1 | 0 | 1 | 0 | 0 | 0 | 40 | \(5 \times 2^3\) |
6.10.6 Scorrimento a destra (> >)¶
-
L'operatore di scorrimento a destra sposta tutti i bit a destra di un numero specificato di posizioni; i bit che escono a destra vanno persi.
-
Per i numeri interi non negativi, vengono aggiunti zeri sul lato sinistro.
-
Per i numeri interi non negativi, scorrere a destra di \(k\) posizioni equivale alla divisione intera per \(2^k\), cioè a \(\lfloor x / 2^k \rfloor\).
Esempio 6.32: scorrimento a destra
int a = 20; // Binario: 10100
int result = a >> 2; // Scorrimento a destra di 2 posizioni: 00101 (decimale: 5)
cout << "a >> 2 = " << result << endl; // Stampa: 5
// Equivale a: 20 / 2^2 = 20 / 4 = 5
Operazione bit per bit:
| Decimale | Equivalente | |||||||
|---|---|---|---|---|---|---|---|---|
a |
0 | 1 | 0 | 1 | 0 | 0 | 20 | \(20\) |
a > > 1 |
0 | 0 | 1 | 0 | 1 | 0 | 10 | \(20 / 2^1\) |
a > > 2 |
0 | 0 | 0 | 1 | 0 | 1 | 5 | \(20 / 2^2\) |
a > > 3 |
0 | 0 | 0 | 0 | 1 | 0 | 2 | \(\lfloor 20 / 2^3 \rfloor\) |
Nell'ultima riga \(20 / 2^3 = 2.5\), e lo scorrimento a destra dà la parte intera \(2\): il bit \(1\) in posizione \(0\) di a > > 2 va perso.
6.10.7 Applicazioni pratiche¶
Gli operatori bit a bit offrono soluzioni brevi e veloci per compiti comuni. Negli esempi seguenti, una maschera è un numero la cui rappresentazione binaria ha il bit \(1\) solo nelle posizioni su cui vogliamo agire: la maschera 1 < < k ha il bit \(1\) solo nella posizione \(k\).
Esempio 6.33: verifica se un numero è pari o dispari
int n = 15;
if (n & 1)
cout << n << " è dispari" << endl;
else
cout << n << " è pari" << endl;
// Spiegazione: n & 1 controlla il bit meno significativo
// Se vale 1 il numero è dispari, se vale 0 è pari
Output:
Esempio 6.34: scambio di due numeri senza una variabile temporanea
int a = 5, b = 9;
cout << "Prima dello scambio: a = " << a << ", b = " << b << endl;
a = a ^ b; // ora a contiene a XOR b
b = a ^ b; // b = (a XOR b) XOR b = a
a = a ^ b; // a = (a XOR b) XOR a = b
cout << "Dopo lo scambio: a = " << a << ", b = " << b << endl;
Output:
Lo scambio funziona perché x x vale \(0\) e x 0 vale x per ogni intero x, e lo XOR è commutativo e associativo.
Esempio 6.35: verifica se un bit specifico è impostato
int n = 12; // Binario: 1100
int position = 2; // Controlla il bit in posizione 2 (da destra, a partire da 0)
if (n & (1 << position))
cout << "Il bit " << position << " vale 1" << endl;
else
cout << "Il bit " << position << " vale 0" << endl;
// Spiegazione: (1 << position) crea una maschera in cui è acceso solo il bit in posizione position
// n & mask controlla se quel bit di n vale 1
Output:
Esempio 6.36: impostazione di un bit specifico a 1
Esempio 6.37: azzeramento di un bit specifico a 0
int n = 14; // Binario: 1110
int position = 1; // Azzera il bit in posizione 1
n = n & ~(1 << position); // Pone il bit in posizione 1 a 0
cout << "Risultato: " << n << endl; // Stampa: 12 (binario: 1100)
La maschera ~(1 < < position) ha il bit \(0\) nella posizione data e il bit \(1\) in tutte le altre posizioni.
Esempio 6.38: inversione di un bit specifico
Punti chiave del capitolo
-
Un programma
C++viene compilato (ad esempio cong++) e la sua esecuzione parte dalla funzionemain; ogni istruzione termina con “;”. -
Ogni variabile è dichiarata con il suo tipo di dato prima dell'uso; la divisione fra due valori
intè la divisione intera. -
Le istruzioni condizionali (
if,else,?:) scelgono quali istruzioni vengono eseguite; i cicli (while,for) le ripetono. -
Gli array di
C++sono indicizzati da \(0\) a \(n-1\), mentre gli array dello pseudocodice sono indicizzati da \(1\) a \(n\); un array di dimensione dinamica si alloca connewe si libera condelete[]. -
Una funzione riceve i suoi parametri per valore (una copia) o per riferimento (
&); un array è passato tramite il puntatore al suo primo elemento, insieme alla sua dimensione. -
Gli operatori bit a bit agiscono sulla rappresentazione binaria dei numeri interi:
x < < kmoltiplica \(x\) per \(2^k\) e, per \(x \ge 0\),x > > kcalcola \(\lfloor x/2^k \rfloor\).
Esercizi su questo capitolo: Introduzione a C++