Programmazione Scientifica++: Lezione 5

Array C-Style, Puntatori, Reference e Funzioni

Docente

Array (Vettori Statici) in C++

  • Gli Array allocano blocchi di memoria contigui.
  • La dimensione deve essere nota a tempo di compilazione.
#include <iostream>

int main() {
    float v1[3] = {0.4, 1.34, 56.156}; 
1    float v2[3];
2    float v3[] = {0.9, -0.1, -0.65};
    
    for(int i = 0; i < 3; ++i) {
        std::cout << "v1[" << i << "]: " << v1[i] << std::endl;
    }
    return 0;
}
1
NON INIZIALIZZATO: contiene spazzatura di memoria!
2
Dimensione calcolata in automatico dal compilatore (3).

Important

L’indice degli array in C++ parte sempre da 0! (v1[0], v1[1], v1[2]).

Il pericolo: Nessun controllo sui limiti

In C++, non c’è nessun controllo sui limiti dell’array a runtime per massimizzare le performance.

#include <iostream>

int main() {
    float vect[3] = {0.4, 1.34, 56.156}; 
    
1    for(int i = 0; i < 5; ++i) {
        std::cout << "vect[" << i << "]: " << vect[i] << std::endl;
    }
    return 0;
}
1
PERICOLO: iteriamo fino a i=4 ma l’array arriva solo a i=2.

Warning

Si accede a porzioni di memoria adiacenti (Buffer Over-read/Over-write). Può stampare valori casuali o causare un crash critico (Segmentation Fault).

Array Multidimensionali (Matrici)

Possiamo definire array a più dimensioni. In memoria, la matrice 2D è comunque immagazzinata come un singolo blocco 1D contiguo (“Row-major order”).

#include <iostream>

int main() {
1    double matrix[2][3] = {
        {1.0, 2.0, 3.0}, // Riga 0
        {4.0, 5.0, 6.0}  // Riga 1
    };
    
    // Accesso agli elementi: matrix[riga][colonna]
2    std::cout << "Elemento (1, 2): " << matrix[1][2] << std::endl;
    
    // Somma di tutti gli elementi
    double sum = 0.0;
    for(int i = 0; i < 2; ++i) {
        for(int j = 0; j < 3; ++j) {
            sum += matrix[i][j];
        }
    }
    return 0;
}
1
Matrice 2x3 (2 righe, 3 colonne).
2
Stampa 6: cout non mostra gli zeri finali di un double (per cambiare formato serve <iomanip>).

Puntatori e Reference

  • Una variabile è un’etichetta assegnata a una locazione di memoria.
  • Un Reference (&) è un alias (un altro nome) per la stessa identica variabile.
  • Un Puntatore (*) è una variabile che contiene l’indirizzo di memoria di un’altra variabile.
int a = 1;

1int* b = &a;
2int& x = a;

3*b = 3;
4x = 45;
1
b è un puntatore. Contiene l’indirizzo di a.
2
x è un reference. È un sinonimo di a.
3
Modifica il valore PUNTATO da b (quindi modifica a!)
4
Modifica x (che essendo un sinonimo di a, modifica a!)

Modificare valori tramite puntatori

#include <iostream>

int main() {
    int a = 1;
    int* b = &a; 
    int& x = a;
    
1    *b = 3;
    std::cout << "valore di a: " << a << std::endl;
    
2    x = 45;
    std::cout << "valore di a: " << a << std::endl; 
    
3    std::cout << "Indirizzo di a: " << &a << std::endl;
    return 0;
}
1
Cambiamo il valore dereferenziando il puntatore (*).
2
Cambiamo il valore tramite il reference (non serve *).
3
Stampiamo l’indirizzo fisico in memoria.

Terminale:

valore di a: 3
valore di a: 45
Indirizzo di a: 0x7ffc3a5b1e4c

Bad Pointers e Null Pointers

I puntatori possono puntare a locazioni di memoria invalide, causando disastri:

#include <iostream>

int main() {
1    int* c;
    
    // std::cout << *c << std::endl; // PERICOLO: Stampa "spazzatura" o va in crash!
    
2    c = nullptr;
    
3    // std::cout << *c << std::endl;
    
    return 0;
}
1
Puntatore non inizializzato, punta in un’area casuale!
2
Puntatore nullo in C++11 (molto meglio di 0 o NULL).
3
Dereferenziare (leggere o scrivere) un puntatore nullo è undefined behaviour: su Linux e macOS in pratica il programma muore con Segmentation fault (core dumped), ma non è garantito dallo standard — il compilatore, sapendo che non può accadere, è autorizzato a ottimizzare via il codice che lo circonda.

Reference vs Puntatori: Le Regole

In C++ moderno si preferiscono sempre i Reference, usando i Puntatori solo se strettamente necessario.

Caratteristica Reference (&) Puntatore (*)
Inizializzazione Deve essere assegnato subito Può essere dichiarato vuoto (nullptr)
Riassegnazione NON può cambiare bersaglio Può essere riassegnato
Sintassi Usato come una normale variabile Richiede dereferenziazione (*a = 5;)
Sicurezza Non può essere nullo Rischio di Memory Leak e Crash

Note

Quando usare un puntatore? Quando l’oggetto “può non esistere” (nullptr), o per gli array dinamici del C.

Funzioni e Metodi

  • Hanno Input (argomenti/parametri) e Output (valore di ritorno).
  • Le funzioni che non restituiscono nulla usano il tipo speciale void.
#include <iostream>

double calcola_energia_cinetica(double massa, double velocita) {
    return 0.5 * massa * velocita * velocita;
}

void print_risultato(double res) {
    std::cout << "Risultato: " << res << " J\n";
}

int main() {
    double E = calcola_energia_cinetica(2.0, 10.0);
    print_risultato(E);
    return 0;
}

Pass-by-Value (Passaggio per Valore)

Di default in C++, i parametri vengono passati Per Valore. Ciò significa che la funzione riceve una copia della variabile. Le modifiche fatte dentro la funzione non hanno alcun effetto sulla variabile esterna.

#include <iostream>

void raddoppia(int numero) {
1    numero = numero * 2;
}

int main() {
    int x = 10;
    raddoppia(x);
    
2    std::cout << "x vale: " << x << std::endl;
    return 0;
}
1
Modifichiamo solo la copia locale.
2
Stampa ancora 10!

Warning

Contro: Copiare oggetti di grandi dimensioni (es. Matrici 1000x1000) devasta le performance!

Pass-by-Reference

Se usiamo un Reference (&), passiamo un alias della variabile originale. Non viene fatta alcuna copia, e possiamo modificare la variabile originale.

#include <iostream>

1void raddoppia(int& numero) {
2    numero = numero * 2;
}

int main() {
    int x = 10;
    raddoppia(x);
    
    std::cout << "x vale: " << x << std::endl; // Stampa 20!
    return 0;
}
1
Notare l’aggiunta dell’e-commerciale &!
2
Stiamo modificando la variabile originale.

Questo approccio è utile per restituire “più di un risultato” da una funzione.

Il Santo Graal: Pass-by-Const-Reference

Quando vogliamo passare un oggetto “pesante” (come una Rete Neurale o una Matrice) ma NON vogliamo modificarlo, il Pass-by-Value è lentissimo. La soluzione universale è il Pass-by-Const-Reference.

#include <iostream>
#include <string>

1void analizza_testo(const std::string& testo_molto_lungo) {
    std::cout << "Lunghezza: " << testo_molto_lungo.size();
2    // testo_molto_lungo += "a";
}

int main() {
    std::string testo = "..."; // Molti MB di dati
    analizza_testo(testo); // Zero copie, zero rischi!
    return 0;
}
1
const vieta le modifiche, & evita la copia in memoria!
2
ERRORE DI COMPILAZIONE (è const).

Arrivano dopo: la sintassi completa di const nella Lezione 6, std::string nella Lezione 11.

Passaggio di Array alle Funzioni

In C++ non si può passare un array nativo per valore. Gli array decadono (decay) sempre a un Puntatore al loro primo elemento.

// Sembra che chiediamo un array, ma il compilatore lo traduce in: 
// void stampa_array(int* array, int lunghezza)
#include <iostream>

1void stampa_array(int array[], int lunghezza) {
    for(int i=0; i<lunghezza; ++i) {
        std::cout << array[i] << " ";
    }
}

int main() {
    int dati[5] = {1, 2, 3, 4, 5};
2    stampa_array(dati, 5);
    return 0;
}
1
Questo parametro è in realtà un int* array.
2
Passa in realtà l’indirizzo &dati[0].

Dichiarazione prima dell’Uso

In C++, il compilatore legge il file dall’alto verso il basso. Non può usare una funzione di cui non conosce l’esistenza!

Sbagliato:

#include <iostream>

int main() {
1    print();
    return 0;
}

void print() {
    std::cout << "Ciao";
}
1
Il compilatore non sa cosa sia “print()”.

Corretto:

#include <iostream>

1void print();

int main() {
    print(); // OK!
    return 0;
}

2void print() {
    std::cout << "Ciao";
}
1
Prototipo (Dichiarazione).
2
Implementazione (Definizione).

Overloading delle Funzioni

In C++ possiamo avere più funzioni con lo stesso nome, a patto che abbiano una firma (signature) diversa (cioè parametri diversi in numero o tipo).

// Funzione 1
#include <iostream>

void stampa(int x) {
    std::cout << "Intero: " << x << std::endl;
}

// Funzione 2
void stampa(double x) {
    std::cout << "Reale: " << x << std::endl;
}

// Funzione 3
void stampa(int x, int y) {
    std::cout << "Coordinate: (" << x << ", " << y << ")" << std::endl;
}

int main() {
    stampa(5);        // Chiama Funzione 1
    stampa(3.14);     // Chiama Funzione 2
    stampa(10, 20);   // Chiama Funzione 3
    return 0;
}

Argomenti di Default

Le funzioni possono avere valori predefiniti per gli ultimi argomenti della lista. Se l’utente non li fornisce, usa quelli di default.

#include <cmath>

1double calcola_distanza(double x, double y, double z = 0.0) {
    return std::sqrt(x*x + y*y + z*z);
}

int main() {
    // Distanza nel piano 2D (non passiamo z)
2    double d2 = calcola_distanza(3.0, 4.0);
    
    // Distanza nello spazio 3D
    double d3 = calcola_distanza(3.0, 4.0, 5.0); 
    return 0;
}
1
L’argomento z è opzionale. Se omesso, varrà 0.0.
2
Usa automaticamente z=0.0.

Warning

Regola ferrea: i parametri di default devono sempre stare in fondo alla lista. Con void f(int a=1, int b); il compilatore risponde error: default argument missing for parameter 2 of 'void f(int, int)'.

Esercizio in Classe (10 min)

Scrivi un programma in C++ che:

  1. Contenga una funzione void normalizza(double& x, double& y).
  2. La funzione deve prendere in input due coordinate (passate per reference) e le normalizzi affinché la lunghezza del vettore \(\sqrt{x^2 + y^2}\) sia 1.
  3. Nel main, definisci due variabili \(x=3.0, y=4.0\).
  4. Stampa le variabili.
  5. Passale alla funzione normalizza.
  6. Stampa nuovamente le variabili. Cosa è successo?

(Suggerimento: includi la libreria <cmath> per la funzione std::sqrt())

Soluzione dell’Esercizio

#include <iostream>
#include <cmath>

1void normalizza(double& x, double& y) {
    double norma = std::sqrt(x*x + y*y);
    if(norma > 0) {
        x = x / norma;
        y = y / norma;
    }
}

int main() {
    double x = 3.0;
    double y = 4.0;
    
    std::cout << "Prima: (" << x << ", " << y << ")\n";
    
2    normalizza(x, y);
    
    std::cout << "Dopo : (" << x << ", " << y << ")\n";
    return 0;
}
1
Abbiamo usato & (reference) perché vogliamo modificare le variabili del main.
2
Stampa finale: Dopo : (0.6, 0.8)