Programmazione Scientifica++: Lezione 5

Array C-Style, Puntatori, Reference e Funzioni

Cristiano De Michele

Argomenti di oggi

Quasi tutto è ripasso del C, e lo segnaliamo con come in C. Le novità del C++ sono reference, nullptr, const reference, overloading e argomenti di default.

  • Array (come in C)
    • Memoria contigua, nessun controllo sui limiti, array multidimensionali
  • Puntatori (come in C) e reference (C++)
    • Differenze e criteri di scelta
  • Funzioni (come in C)
    • Passaggio per valore e per puntatore (come in C), per reference e per const reference (C++)
    • Array nelle funzioni: il decay a puntatore (come in C)
    • Prototipi (come in C), overloading e argomenti di default (C++)

Array (vettori statici)

  • Come in C: un blocco di memoria contiguo, con l’indice che parte da 0.
  • La dimensione deve essere nota a tempo di compilazione: float v[n] con n variabile è lecito in C99, non nello standard C++.
#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) {
        v2[i] = v1[i] + v3[i];
        std::cout << v2[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
1
Non inizializzato: il suo contenuto è indeterminato.
2
Il compilatore ricava la dimensione (3) dall’inizializzazione.

Importante

L’indice parte sempre da 0: v1[0], v1[1], v1[2].

Nota

float v[n] compila lo stesso: g++ e clang++ lo accettano come estensione. Con -Wpedantic g++ avvisa; clang++ recente avvisa anche senza opzioni.

Nessun controllo sui limiti

Come in C, in esecuzione non c’è alcun controllo sui limiti dell’array: richiederebbe un test a ogni accesso.

#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
Il ciclo arriva a i = 4, ma l’ultimo elemento è vect[2].

Avviso

Si leggono le celle di memoria adiacenti (buffer over-read; scrivendo, over-write). È undefined behaviour: può stampare valori privi di significato (per esempio 9.18341e-41, -1.05467e+36) oppure terminare con Segmentation fault. Nella Lezione 08 vedremo l’AddressSanitizer, che rileva questi errori in esecuzione.

Array multidimensionali (matrici)

Come in C. In memoria è un unico blocco contiguo, riga dopo riga (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
    };

2    std::cout << "Elemento (1, 2): " << matrix[1][2] << std::endl;
    double sum = 0.0;
    for (int i = 0; i < 2; ++i) {
        for (int j = 0; j < 3; ++j) {
            sum += matrix[i][j];
        }
    }
    std::cout << "Somma: " << sum << std::endl; // stampa 21
    return 0;
}
1
Matrice 2×3 (2 righe, 3 colonne).
2
Gli elementi si indicano con matrix[riga][colonna]. Stampa 6: cout non mostra gli zeri finali di un double (per cambiare formato serve <iomanip>).

Puntatori e reference

  • Una variabile è un nome dato a una locazione di memoria.
  • Un puntatore (*), come in C, è una variabile che contiene l’indirizzo di un’altra variabile.
  • Un reference (&), novità del C++, è un alias: un altro nome per la stessa 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 altro nome di a.
3
Modifica il valore puntato da b, cioè a.
4
Modifica x, cioè ancora a: è lo stesso oggetto.

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
Si modifica il valore dereferenziando il puntatore (*).
2
Si modifica il valore tramite il reference (senza *).
3
Si stampa l’indirizzo di a.

Terminale:

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

Puntatori non validi e puntatore nullo

Un puntatore può contenere un indirizzo non valido:

#include <iostream>

int main() {
1    int* c;
    // std::cout << *c << std::endl;    // undefined behaviour
2    c = nullptr;
3    if (c != nullptr) {
        std::cout << *c << std::endl;
    }
    return 0;
}
1
Non inizializzato: contiene un indirizzo indeterminato, e già leggerlo è undefined behaviour.
2
nullptr è il NULL del C++ (dal C++11): ha un tipo proprio (std::nullptr_t) e, a differenza di 0 o NULL, non si confonde con un intero (lo vedremo con l’overloading).
3
Prima di dereferenziare si controlla: dereferenziare un puntatore nullo è undefined behaviour. Su Linux e macOS in pratica il programma termina con Segmentation fault, ma lo standard non lo garantisce.

Reference e puntatori a confronto

In C++ si preferiscono i reference e si usano i puntatori solo quando necessario.

Reference (&) Puntatore (*)
Inizializzazione Obbligatoria Facoltativa (anche nullptr)
Riassegnazione Mai: resta legato all’oggetto iniziale Può ricevere un altro indirizzo
Sintassi Come la variabile Va dereferenziato (*p = 5)
Sicurezza Mai nullo Nullo o non valido: va controllato

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

Funzioni (come in C)

  • Hanno un input (i parametri) e un output (il valore di ritorno).
  • Le funzioni che non restituiscono nulla hanno tipo 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;
}

Passaggio per valore

Come in C, di default i parametri si passano per valore: la funzione riceve una copia, e le modifiche all’interno della funzione non si riflettono sulla variabile del chiamante.

#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
Si modifica solo la copia locale.
2
Stampa ancora 10.

Il costo: passare per valore un oggetto grande, per esempio una matrice 1000×1000 di double, comporta la copia di 8 MB a ogni chiamata.

Passaggio per reference

Con un reference (&) la funzione riceve un alias della variabile: nessuna copia, e le modifiche sono visibili al chiamante (serve anche a restituire più di un risultato). In C si usa un puntatore.

In C, con un puntatore:

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

// nel main:
raddoppia(&x);

In C++, con un reference:

#include <iostream>

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

int main() {
    int x = 10;
2    raddoppia(x);
    std::cout << "x vale: " << x << std::endl;
    return 0;
}
1
Con & nel tipo, numero è un altro nome di x.
2
Né & nella chiamata né * nella funzione. Stampa 20.

Passaggio per const reference

Per passare un oggetto grande senza modificarlo si usa un const reference: & evita la copia, const (per esteso nella Lezione 06) vieta le modifiche.

#include <iostream>

1struct Strato {
    double pesi[10000];                  // 80 KB
};

2double primo_peso(const Strato& s) {
3    // s.pesi[0] = 1.0;
    return s.pesi[0];
}

int main() {
    Strato s = {};
    s.pesi[0] = 0.5;
    std::cout << primo_peso(s) << std::endl;   // nessuna copia; stampa 0.5
    return 0;
}
1
Una struct come in C. In C++ il tipo si chiama semplicemente Strato, senza struct davanti.
2
In C si scriverebbe const struct Strato* s.
3
Errore di compilazione: assignment of read-only location 's.Strato::pesi[0]'.

Passaggio di array alle funzioni

Come in C, un array nativo non si può passare per valore: decade (decay) a un puntatore al suo primo elemento.

#include <iostream>

// Il parametro sembra un array, ma il compilatore lo tratta come:
// void stampa_array(int* array, int lunghezza)

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

int main() {
    int dati[5] = {1, 2, 3, 4, 5};
2    stampa_array(dati, 5);
    return 0;
}
1
Il compilatore lo tratta come int* array.
2
Viene passato l’indirizzo &dati[0].

Dichiarazione prima dell’uso

Come in C, il compilatore legge il file dall’alto verso il basso: una funzione va dichiarata prima del suo uso.

Sbagliato:

#include <iostream>

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

void print() {
    std::cout << "Ciao\n";
}
1
error: 'print' was not declared in this scope. In C i compilatori meno recenti davano solo un avviso (implicit declaration); in C++ è sempre un errore.

Corretto:

#include <iostream>

1void print();

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

2void print() {
    std::cout << "Ciao\n";
}
1
Prototipo (dichiarazione).
2
Implementazione (definizione).

Overloading delle funzioni

Novità del C++ (in C due funzioni non possono avere lo stesso nome): più funzioni con lo stesso nome, purché la firma sia diversa, cioè i parametri in numero o tipo. Il solo tipo di ritorno non basta.

#include <iostream>

void stampa(int x)        { std::cout << "Intero: " << x << std::endl; }
void stampa(double x)     { std::cout << "Reale: " << x << std::endl; }
void stampa(int x, int y) { std::cout << "Coordinate: (" << x << ", " << y << ")\n"; }

int main() {
    stampa(5);        // stampa(int)
    stampa(3.14);     // stampa(double)
    stampa(10, 20);   // stampa(int, int)
    return 0;
}

Per questo nullptr è preferibile a NULL: con g(int) e g(int*), la chiamata g(NULL) è ambigua, mentre g(nullptr) sceglie g(int*).

Argomenti di default

Novità del C++: gli ultimi parametri possono avere un valore predefinito, usato quando la chiamata li omette.

#include <iostream>
#include <cmath>

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

int main() {
    std::cout << calcola_distanza(3.0, 4.0) << std::endl;       // 2D: stampa 5
    std::cout << calcola_distanza(3.0, 4.0, 12.0) << std::endl; // 3D: stampa 13
    return 0;
}
1
z è opzionale: se manca vale 0.0. Se c’è un prototipo, il default si scrive solo lì: ripeterlo nella definizione dà error: default argument given for parameter 3.

Avviso

I parametri con un default stanno sempre in fondo alla lista. Con void f(int a=1, int b); il compilatore segnala error: default argument missing for parameter 2 of 'void f(int, int)'.

Esercizio in classe

Scrivete un programma che:

  1. contenga una funzione void normalizza(double& x, double& y), che riceve per reference le due componenti di un vettore e le divide per la sua lunghezza \(\sqrt{x^2 + y^2}\), così che il vettore abbia lunghezza 1;
  2. nel main definisca \(x = 3{,}0\) e \(y = 4{,}0\) e le stampi;
  3. le passi a normalizza e le stampi di nuovo. Cosa è cambiato, e perché?

(Per std::sqrt() serve #include <cmath>.)

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
Il & è necessario per modificare le variabili del main.
2
Stampa finale: Dopo : (0.6, 0.8).

Esercizi lampo: cosa stampa?

Due frammenti. Rispondete prima di passare alla slide successiva: in entrambi i casi la risposta più immediata è sbagliata.

1. Con le tre funzioni stampa della slide sull’overloading:

stampa('A');

2.

int a = 1, b = 2;
int& r = a;
r = b;
b = 5;
std::cout << a << " " << r << "\n";

Esercizi lampo: le risposte

1. Stampa Intero: 65. Non esiste una stampa(char): il char 'A' viene promosso a int, che ha la precedenza sulla conversione a double, e il suo codice ASCII è 65.

2. Stampa 2 2: r = b non lega r a b: copia in a il valore di b, perché un reference resta legato all’oggetto con cui è stato inizializzato. Poi b = 5 non tocca né a né r.