Vai al contenuto

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 main come 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. In C++, 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:

// Questa è una riga di commento

Questo è un esempio di commento su più righe:

/* This is a
   multi-line comment */

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

g++ -Wall -std=c++17 -o hello hello.cpp
./hello

e l'output è

Ciao, mondo!

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:

    1. 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.

    2. Il tipo di dato “bool” rappresenta un valore booleano: true o false.

    3. 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 di float.

    4. 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:

26 0 3.14 123457 a Ada

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 in C++ la libreria del flusso di ingresso e uscita.

Esempio 6.4: inclusione

#include <iostream>
  • 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 cout si 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 con std::cout per andare a capo sullo schermo e svuotare il buffer di uscita.

Esempio 6.5: stampe sullo schermo

int n = 7;
std::cout << "Il valore della variabile è " << n << std::endl;

Output:

Il valore della variabile è 7
  • L'istruzione “using namespace std;” semplifica l'uso delle funzioni della libreria standard, eliminando la necessità di anteporre std::. D'ora in poi la usiamo sempre, e scriviamo cout e endl.

  • L'istruzione di inclusione “#include <iomanip>” fornisce i manipolatori “fixed” e “setprecision(k)”. Quando sono scritti insieme in un'istruzione cout, 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:

Addizione: 13
Sottrazione: 7
Moltiplicazione: 30
Divisione: 3
Resto: 1
  • L'operatore di divisione / esegue la divisione intera quando è usato con numeri interi: il risultato di 10 / 3 è 3. Per ottenere un risultato in virgola mobile, almeno un operando deve essere di tipo float o double. Ad esempio, 10.0 / 3 restituisce 3.33333 invece di 3.

  • Un valore di tipo int viene convertito in un valore di tipo double scrivendo “(double)” davanti ad esso (conversione di tipo). Ad esempio, se a e b sono variabili di tipo int, l'espressione (double)a / b esegue 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>):

3
3.33333
3.33333
3.33

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:

Valore iniziale: 10
Dopo += 5: 15
Dopo -= 3: 12
Dopo *= 2: 24
Dopo /= 4: 6
Dopo %= 3: 0

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 (- -x e x- -).

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:

Valore iniziale: 5
Pre-incremento (++x): 6
Post-incremento (x++): 6
Valore dopo il post-incremento: 7
Pre-decremento (--x): 6
Post-decremento (x--): 6
Valore dopo il post-decremento: 5

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 come 1) se la condizione è soddisfatta;

    • false (visualizzato come 0) 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:

a == b: 0
a != b: 1
a < b: 0
a > b: 1
a <= b: 0
a >= b: 1

6.4.5 Operatori logici

Operatori logici, usati per le operazioni logiche, spesso nelle istruzioni condizionali:

  • && (AND logico) \(\rightsquigarrow\) restituisce true se entrambe le condizioni sono vere.

  • || (OR logico) \(\rightsquigarrow\) restituisce true se almeno una condizione è vera.

  • ! (NOT logico) \(\rightsquigarrow\) inverte il valore booleano.

  • Come gli operatori relazionali, gli operatori logici restituiscono valori booleani, true (1) o false (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:

condition1: 1
condition2: 0
condition1 && condition2: 0
condition1 || condition2: 1
!condition1: 0
!condition2: 1

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 if valuta 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 parte else, 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:

Il numero è positivo.

6.5.1 Operatore ternario (operatore condizionale)

  • L'operatore ternario ?: offre un modo compatto per scrivere semplici istruzioni if-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 vale valore_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:

Il numero è positivo
Il numero è positivo

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:

Massimo (if-else): 20
Massimo (ternario): 20

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:

7 è dispari

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:

Il valore assoluto di -5 è 5

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:

Punteggio: 85, Voto: B

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 ciclo while e il ciclo for.

6.6.1 Il ciclo while

  • Il ciclo while esegue 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:

Numero: 1
Numero: 2
Numero: 3
Numero: 4
Numero: 5

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 break termina immediatamente il ciclo.

  • L'istruzione continue salta 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:

1 3 5 7

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\) sono b[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:

9 12 5 16
  • 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, e c[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'operatore new si 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'operatore delete[], per liberare la memoria ed evitare perdite di memoria (memory leak).

Figura 1

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:

0 2 4 6 8
  • 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 new si 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:

0 0 0 0
0 1 2 3
0 2 4 6

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:

    1. un tipo di ritorno: specifica il tipo del valore restituito dalla funzione (void se non viene restituito nulla);

    2. un nome della funzione: l'identificatore univoco della funzione;

    3. una lista dei parametri (facoltativa): specifica i valori in ingresso della funzione;

    4. un corpo: contiene il blocco di codice eseguito quando la funzione viene chiamata;

    5. 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 programma C++. Viene eseguita per prima quando il programma inizia l'esecuzione. Ogni programma C++ deve contenere esattamente una funzione main. Il suo valore di ritorno 0 comunica 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:

Somma: 15
Prodotto: 50

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 esempio int& 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:

Dopo incrementByValue: 5
Dopo incrementByReference: 6
Array: 0 0 0

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 FindMaximum del capitolo «Pseudocodice»: le componenti sono indicizzate da \(1\) a \(n\).

Algoritmo: FindMaximum

Input: Un array \(a[1..n]\) di \(n \ge 1\) numeri interi
Output: Il valore massimo nell'array \(a\)
1function FindMaximum(a, n)
2\(max := a[1]\)
3for \(i := 2\) to \(n\) do
4if \(a[i] > max\) then
5\(max \leftarrow a[i]\)
6end
7end
8return \(max\)
9end

  • Questa è l'implementazione in C++: le componenti sono indicizzate da \(0\) a \(n-1\), quindi l'inizializzazione usa a[0] e il ciclo parte da i = 1. La funzione assume \(n \ge 1\), come fa lo pseudocodice: per \(n = 0\) l'elemento a[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:

    Valore massimo: 19
    

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:

15 è dispari

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:

Prima dello scambio: a = 5, b = 9
Dopo lo scambio: a = 9, b = 5

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:

Il bit 2 vale 1

Esempio 6.36: impostazione di un bit specifico a 1

int n = 12;  // Binario: 1100
int position = 1;  // Imposta il bit in posizione 1

n = n | (1 << position);  // Pone il bit in posizione 1 a 1

cout << "Risultato: " << n << endl;  // Stampa: 14 (binario: 1110)

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

int n = 12;  // Binario: 1100
int position = 0;  // Inverte il bit in posizione 0

n = n ^ (1 << position);  // Inverte il bit in posizione 0

cout << "Risultato: " << n << endl;  // Stampa: 13 (binario: 1101)

Punti chiave del capitolo

  • Un programma C++ viene compilato (ad esempio con g++) e la sua esecuzione parte dalla funzione main; 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 con new e si libera con delete[].

  • 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 < < k moltiplica \(x\) per \(2^k\) e, per \(x \ge 0\), x > > k calcola \(\lfloor x/2^k \rfloor\).


Esercizi su questo capitolo: Introduzione a C++