Programmazione Scientifica++: Lezione 6

Costanti, Aritmetica dei Puntatori e Introduzione alle Classi

Cristiano De Michele

Argomenti di oggi

  • Costanti (come in C, con una differenza)
    • Puntatori costanti e la lettura da destra a sinistra
  • Ancora su puntatori e array (come in C)
    • Aritmetica dei puntatori e salti in memoria
  • Interfacce delle funzioni
    • Ripasso del passaggio per valore e per reference, su uno scheduler
    • Restituire più valori: dalla struct del C alle classi
  • Introduzione alle classi (C++)
    • Il paradigma a oggetti; classi, metodi, costruttori, public e private

Costanti

Come in C, const dichiara una variabile in sola lettura: il valore si fissa all’inizializzazione e ogni assegnazione successiva è un errore di compilazione (per esempio sul learning_rate di una rete).

#include <iostream>

int main() {
    const double learning_rate = 0.01;
1    // learning_rate = 0.05;
2    // const int epochs;
    std::cout << "learning rate: " << learning_rate << std::endl;
    return 0;
}
1
error: assignment of read-only variable 'learning_rate'.
2
error: uninitialized 'const epochs': una costante va inizializzata quando la si dichiara.

Nota

Una differenza con il C. In C++ una const int N = 5; è una costante a tempo di compilazione e può dare la dimensione di un array: double v[N];. In C no: double v[N] = {0}; dà error: variable-sized object may not be initialized.

const e constexpr

constexpr impone un valore noto a tempo di compilazione; const vieta solo le modifiche.

#include <iostream>
int leggi() { int n; std::cin >> n; return n; }   // valore noto solo in esecuzione
int main() {
1    const int N1 = 5;
2    const int N2 = leggi();
3    // constexpr int N3 = leggi();
4    const double h1 = 0.5;
5    constexpr double h2 = 0.5;
    std::cout << N1 + N2 << " " << h1 + h2 << std::endl;
    return 0;
}
1
Costante di compilazione: const intero con valore costante, come constexpr int N1 = 5;.
2
Lecito, ma non è una costante di compilazione: double v[N2]; sarebbe un array di lunghezza variabile.
3
error: call to non-'constexpr' function 'int leggi()'.
4
Si usa in ogni calcolo (const double h3 = 2 * h1;), non dove serve una costante di compilazione: constexpr double h4 = 2 * h1; è un errore (not usable in a constant expression).
5
Costante di compilazione anche per un double.

constexpr al posto di #define

In C le costanti si definiscono spesso con il preprocessore (#define N_PIXEL 784, Lezione 04). In C++ si preferisce constexpr.

#include <iostream>
1#define N 10 + 1
2constexpr int n = 10 + 1;
int main() {
3    std::cout << 2 * N << " " << 2 * n << std::endl;
    return 0;
}
1
Il preprocessore sostituisce il testo prima della compilazione: 2 * N diventa 2 * 10 + 1.
2
Una variabile con tipo e scope, controllata dal compilatore e utilizzabile come costante di compilazione.
3
Stampa 21 22.

#define non ha tipo né scope, e nei messaggi d’errore compare il testo sostituito, non il nome.

Linea guida: const o constexpr

  • constexpr per i valori noti a tempo di compilazione: costanti fisiche, dimensioni di array.
  • const per i valori calcolati in esecuzione che non devono più cambiare, e per i parametri in sola lettura.
  • #define solo per le direttive del preprocessore, per esempio le header guard (Lezione 08).
#include <iostream>
#include <cmath>
constexpr double g = 9.81;              // m/s^2: noto a tempo di compilazione
constexpr double pi = 3.141592653589793;
constexpr int N_MISURE = 5;             // dimensione di un array
double media(const double* x, int n) {  // x in sola lettura
    double s = 0.0;
    for (int i = 0; i < n; ++i) s += x[i];
    return s / n;
}
int main() {
    double L[N_MISURE] = {0.50, 0.51, 0.49, 0.50, 0.50};    // lunghezze del pendolo (m)
    const double L_media = media(L, N_MISURE);             // calcolata in esecuzione
    std::cout << "T = " << 2 * pi * std::sqrt(L_media / g) << " s" << std::endl;
    return 0;
}

Leggere le dichiarazioni da destra a sinistra

Come in C, le dichiarazioni con const e puntatori si leggono da destra verso sinistra.

Puntatore costante:

int * const b = &a;

Lettura: b → const, è costante → *, puntatore → int, a un intero.

Puntatore a costante:

const int * b = &a;

Lettura: b → *, è un puntatore → int, a un intero → const, costante.

Nota

Con questa regola si leggono senza ambiguità anche le dichiarazioni più complicate, e i messaggi d’errore che le riportano.

1. Puntatore costante (int * const)

L’indirizzo contenuto nel puntatore non può cambiare, ma attraverso il puntatore si può modificare il valore puntato.

#include <iostream>

int main() {
    int a = 1;
    int c = 3;
1    int * const b = &a;

2    *b = 5;
3    // b = &c;
    std::cout << a << " " << c << std::endl; // stampa 5 3
    return 0;
}
1
b è un puntatore costante: contiene l’indirizzo di a e non può contenerne un altro.
2
Lecito: modifica a attraverso b.
3
error: assignment of read-only variable 'b'.

2. Puntatore a costante (const int *)

Il valore puntato non si può modificare attraverso il puntatore, ma al puntatore si può assegnare un altro indirizzo.

#include <iostream>

int main() {
    int a = 1;
    int c = 3;
1    const int * b = &a;

2    b = &c;
3    // *b = 5;
    std::cout << *b << std::endl; // stampa 3
    return 0;
}
1
Attraverso b l’intero è in sola lettura; a resta modificabile direttamente.
2
Lecito: ora b punta a c.
3
error: assignment of read-only location '* b'.

3. Entrambi costanti (const float * const)

Le due restrizioni insieme: non si può cambiare né l’indirizzo contenuto nel puntatore né il valore puntato. È l’accesso in sola lettura più restrittivo.

#include <iostream>

int main() {
    float a = 1.0f;
    float c = 3.0f;
1    const float * const b = &a;

2    // *b = 5.0f;
3    // b = &c;
    std::cout << *b << " " << c << std::endl; // stampa 1 3
    return 0;
}
1
Puntatore costante a float costante.
2
error: assignment of read-only location '*(const float*)b'.
3
error: assignment of read-only variable 'b'.

Reference e puntatori costanti

Un reference è un puntatore costante dereferenziato in automatico e mai nullo.

Reference Puntatore equivalente Attraverso di lui
int& r = a; int * const p = &a; si modifica a; indirizzo fisso
const int& r = a; const int * const p = &a; a in sola lettura
(nessuno) const int * p = &a; a in sola lettura; p può ricevere un altro indirizzo
int a = 1;   int& r = a;   int * const p = &a;
r = 5;       *p = 5;       // stessa operazione: a vale 5

Nota

Un reference resta legato all’oggetto iniziale: il suo const riguarda sempre il valore.

Array e puntatori

Il decay della Lezione 05 non riguarda solo i parametri di funzione: il nome di un array decade a un puntatore al primo elemento in quasi ogni espressione. Non è però un puntatore: con sizeof non decade, come mostra il primo esercizio lampo.

#include <iostream>

int main() {
    int v3[] = { 10, 20, 30, 40, 50 };
1    int* d = v3;

2    std::cout << "d punta a: " << *d << "\n";
    return 0;
}
1
Non serve &: v3 decade automaticamente all’indirizzo del primo elemento, &v3[0].
2
Stampa 10.

Aritmetica dei puntatori

Come in C, sommando un intero k a un puntatore si ottiene l’indirizzo dell’elemento k posizioni più avanti.

#include <iostream>

int main() {
    int v3[] = { 10, 20, 30, 40, 50 };
    int* d = v3;

1    d++;
2    std::cout << "*d: " << *d << "\n";

3    d = d + 2;
4    std::cout << "*d: " << *d << "\n";
    return 0;
}
1
Avanza di un elemento.
2
Stampa 20.
3
Avanza di altri due elementi.
4
Stampa 40.

Salti in memoria e sizeof

Di quanti byte si sposta un puntatore con d++? Dipende dal tipo puntato:

Tipo Dimensione tipica Avanzamento di d++
char 1 byte 1 byte
short 2 byte 2 byte
int / float 4 byte 4 byte
double 8 byte 8 byte

Se d (un int*) contiene l’indirizzo 0x7ffc3a5b1e40, dopo d++ contiene 0x7ffc3a5b1e44: l’aritmetica dei puntatori conta in elementi, non in byte.

Nota

L’operatore sizeof(tipo), come in C, restituisce quanti byte occupa un tipo sulla macchina in uso.

Accessi fuori dai limiti dell’array

Come per gli indici (Lezione 05), l’aritmetica dei puntatori non conosce i limiti dell’array: né il compilatore né il programma in esecuzione impediscono di uscirne.

#include <iostream>

int main() {
    int v3[] = { 10, 20, 30 }; // array di 3 elementi
    int* d = v3;

1    d = d + 100;
2    std::cout << "Dato ignoto: " << *d << "\n";
    return 0;
}
1
Incrementando d di 100 si va ben oltre la fine dell’array: già il calcolo di questo indirizzo è undefined behaviour, anche senza leggerlo, e il compilatore non emette alcun avviso.
2
Può stampare 0 come un valore qualsiasi preso dalla memoria dello stesso processo (quella di altri programmi è protetta dal sistema operativo), oppure terminare con Segmentation fault.

Operazioni tra puntatori

Come in C, la differenza fra due puntatori allo stesso array è la loro distanza in elementi.

#include <iostream>

int main() {
    int v[] = { 1, 2, 3, 4, 5, 6, 7 };

    int* inizio = &v[0]; // oppure: int* inizio = v;
    int* fine   = &v[4]; // punta al quinto elemento (5)

1    int distanza = fine - inizio;
    std::cout << "Distanza: " << distanza << " elementi\n";
    return 0;
}
1
Vale 4: da v[0] a v[4] ci sono quattro passi.

Interfacce delle funzioni: ripasso

Il meccanismo è quello della Lezione 05; qui lo rivediamo su un caso del framework. Uno scheduler dimezza il learning rate a fine epoca:

#include <iostream>

1void decay(double eta) {
    eta = eta * 0.5;
}

int main() {
    double learning_rate = 0.2;
    decay(learning_rate);
2    std::cout << "eta = " << learning_rate << "\n";
    return 0;
}
1
Passaggio per valore: decay riceve una copia.
2
Stampa ancora 0.2: lo scheduler ha dimezzato la sua copia, che viene distrutta all’uscita dalla funzione; learning_rate nel main non cambia.

Passaggio per reference

Con una &, eta diventa un alias di learning_rate: nessuna copia.

#include <iostream>

1void decay(double& eta) {
    eta = eta * 0.5;
}

int main() {
    double learning_rate = 0.2;
    decay(learning_rate);
2    std::cout << "eta = " << learning_rate << "\n";
    return 0;
}
1
L’unica differenza è la &.
2
Stampa 0.1: ora decay modifica direttamente learning_rate.

Nota

Nel framework (Laboratorio 10) fixedScheduler deve agire sull’Optimizer per tutto l’addestramento: ne conserva l’indirizzo in un puntatore e lo usa con optimizer->set_lr(...). Un reference come membro sarebbe possibile, ma va inizializzato nella lista di inizializzazione del costruttore (Lezione 07) e rende l’oggetto non riassegnabile.

Il risultato per reference

Una funzione che riceve due array (come puntatori, Lezione 05) e scrive il risultato in un parametro per reference. computeLoss e Result sono esempi: nel framework la loss è la classe MSELoss (Laboratorio 10).

1void computeLoss(const double* pred, const double* target, int n, double& loss) {
    loss = 0.0;
    for (int i = 0; i < n; ++i) {
        double d = pred[i] - target[i];
        loss += d * d;
    }
    loss /= n;
}

int main() {
    double pred[]   = { 1.2, 0.9, 1.3, 1.7 };
    double target[] = { 1.0, 1.0, 1.0, 2.0 };
    double loss_finale;
2    computeLoss(pred, target, 4, loss_finale);
    return 0;
}
1
const double*: gli array si leggono soltanto; double&: qui va il risultato. È l’errore quadratico medio della Lezione 03, \(\frac1n\sum_i (p_i - y_i)^2\).
2
loss_finale va dichiarata prima della chiamata, che le assegna il valore.

Un problema di leggibilità

Dalla sola chiamata computeLoss(a, b, c, d) non si distingue quali argomenti sono input e quale è l’output. È preferibile restituire il risultato:

1double computeLoss(const double* pred, const double* target, int n) {
    double loss = 0.0;
    for (int i = 0; i < n; ++i) {
        double d = pred[i] - target[i];
        loss += d * d;
    }
2    return loss / n;
}
1
Firma chiara: tre input, un double in uscita.
2
Il risultato è il valore di ritorno.

Importante

Per calcolare sia la loss sia l’accuratezza con una sola chiamata, però, il return non basta: restituisce un solo valore.

Restituire più valori: una struct, come in C

La soluzione del C vale anche in C++: una struct che raccoglie i due valori e viene restituita per intero.

#include <iostream>

1struct Result { double loss; double accuracy; };

2Result computeMetrics() {
    Result res = { 0.5, 95.0 };    // valori di esempio
    return res;                    // un solo return, due valori
}

int main() {
    Result r = computeMetrics();
    std::cout << r.loss << " " << r.accuracy << std::endl; // stampa 0.5 95
    return 0;
}
1
Come in C. In C++ il tipo si chiama Result, senza struct davanti (Lezione 05).
2
In C la firma sarebbe struct Result computeMetrics(void).

Il paradigma a oggetti

La programmazione orientata agli oggetti (OOP) riunisce dati e operazioni in oggetti, e si basa su tre concetti:

  • Incapsulamento: l’interfaccia (public) è separata dall’implementazione (private); chi usa un oggetto conosce solo l’interfaccia. Oggi e Lezione 07.
  • Polimorfismo: il comportamento di un’operazione dipende dal tipo degli oggetti; a tempo di compilazione con l’overloading (funzioni, Lezione 05; operatori, Lezione 10), in esecuzione con le funzioni virtual (Lezione 12).
  • Ereditarietà: una classe ne estende un’altra (Lezione 12); nel framework DenseLayer, SigmoidLayer e Sequential derivano da layer.
double d;                  // d: istanza del tipo fondamentale double
Particella p(2.0, 10.0);   // p: istanza della classe Particella, cioè un oggetto

Con l’overloading degli operatori si potrà scrivere c = a + b; anche per i propri tipi, come per i double.

Le classi in C++

Una classe è un tipo definito dall’utente: ne descrive i dati e le operazioni, e gli oggetti ne sono le istanze. Rispetto alla struct del C aggiunge le funzioni e il controllo dell’accesso.

  • Attributi (dati): le caratteristiche dell’oggetto.
  • Metodi (comportamento): le funzioni che operano sui dati dell’oggetto.

Esempio dal framework: Classe: DenseLayer Attributi: la matrice dei pesi, il numero di neuroni. Metodi: forward(), backward().

Sintassi di una classe

#include <iostream>

class Result {
1public:
    double loss;
    double accuracy;

2    void print() {
        std::cout << "Loss: " << loss << " Acc: " << accuracy << "%\n";
    }
3};

int main() {
4    Result r1;
    r1.loss = 1.2;
    r1.accuracy = 85.5;
    r1.print();
    return 0;
}
1
public: rende accessibili dall’esterno (per esempio dal main) i membri che seguono.
2
Un metodo: una funzione che sta dentro la classe e ha accesso diretto agli attributi.
3
Il ; dopo la parentesi graffa finale è obbligatorio, come per le struct del C.
4
Crea un’istanza (un oggetto) della classe.

Incapsulamento: public e private

I metodi accedono a tutti i membri della classe, pubblici e privati; dall’esterno solo a quelli pubblici. È buona pratica tenere i dati privati e consentirne l’accesso solo attraverso metodi pubblici, che possono controllarli.

#include <iostream>

class Temperatura {
private:
1    double kelvin_;

public:
2    void imposta(double k) {
        if (k >= 0.0) kelvin_ = k;
    }
3    double kelvin() { return kelvin_; }
};
1
Attributo privato. Il suffisso _ è una convenzione diffusa, non una regola del linguaggio (Lezione 07).
2
Modifica il valore solo dopo averlo verificato.
3
Getter: legge il dato privato (Lezione 07).
int main() {
    Temperatura t;
    t.imposta(300.0);
4    t.imposta(-10.0);
5    // t.kelvin_ = -10.0;
    std::cout << t.kelvin() << " K\n";
    return 0;
}
4
Temperatura assoluta negativa: rifiutata, kelvin_ resta 300. Il programma stampa 300 K.
5
error: 'double Temperatura::kelvin_' is private within this context.

Costruttori

Per garantire che kelvin_ abbia un valore definito fin dalla creazione dell’oggetto si usa un costruttore.

#include <iostream>

class Temperatura {
private:
    double kelvin_;

public:
1    Temperatura() {
        kelvin_ = 273.15;
        std::cout << "Temperatura creata: "
                  << kelvin_ << " K\n";
    }
};

int main() {
2    Temperatura t;
    return 0;
}
1
Costruttore di default, senza parametri.
2
Crea l’oggetto e chiama il costruttore: stampa Temperatura creata: 273.15 K.

Il costruttore:

  • ha lo stesso nome della classe;
  • non ha tipo di ritorno, neanche void;
  • viene eseguito automaticamente quando l’oggetto viene creato.

Senza costruttore, kelvin_ avrebbe un valore indeterminato finché non gli si assegna un valore.

Costruttori con parametri

Al costruttore si possono passare argomenti quando si crea l’oggetto.

class Result {
private:
    double loss_;
    double accuracy_;
public:
1    Result(double l, double a) {
        loss_ = l;
        accuracy_ = a;
    }
};

int main() {
2    // Result r1;
3    Result r2(1.1, 0.95);
    return 0;
}
1
I double passano per valore: il const reference della Lezione 05 serve per gli oggetti pesanti; su un double non porta vantaggi: copiarlo costa quanto passarne l’indirizzo.
2
error: no matching function for call to 'Result::Result()': mancano i parametri.
3
Chiama il costruttore con i valori iniziali.

class e struct

In C++ struct e class differiscono solo per la visibilità di default.

  • In una struct tutto è public, finché non si scrive private:.
  • In una class tutto è private, finché non si scrive public:.

Per il resto sono identiche: anche una struct del C++ può avere metodi e costruttori, a differenza di quella del C.

Nota

Per convenzione si usa struct per i semplici contenitori di dati (plain old data), come la Result restituita da computeMetrics, e class quando ci sono un comportamento e dei dati da proteggere.

Esercizio in classe

La classe Particella. Scrivete un programma C++ che:

  1. definisca una classe Particella con due attributi privati, massa_ e velocita_;
  2. abbia un costruttore che riceve due double e li assegna agli attributi;
  3. abbia un metodo pubblico calcola_energia() che restituisce l’energia cinetica \(\frac{1}{2}mv^2\);
  4. nel main() crei una Particella con massa \(2{,}0\) e velocità \(10{,}0\), chiami calcola_energia() e stampi il risultato.

Compilatelo dal terminale, con -Wall.

Soluzione dell’esercizio: la classe

particella.cpp
#include <iostream>

class Particella {
1private:
    double massa_;
    double velocita_;

2public:
3    Particella(double m, double v) {
        massa_ = m;
        velocita_ = v;
    }

4    double calcola_energia() {
        return 0.5 * massa_ * velocita_ * velocita_;
    }
};
1
Incapsulamento: gli attributi non sono accessibili dall’esterno.
2
L’interfaccia: ciò che il resto del programma può usare.
3
Il costruttore inizializza gli attributi.
4
I metodi accedono direttamente agli attributi privati.

Soluzione dell’esercizio: il main

particella.cpp
int main() {
    // 1. creazione (istanziazione) dell'oggetto
1    Particella p(2.0, 10.0);

2    // std::cout << p.massa_;

    // 2. chiamata al metodo con l'operatore punto (.)
3    double e = p.calcola_energia();

    std::cout << "Energia cinetica: " << e << " J\n";
    return 0;
}
1
L’oggetto è inizializzato dal costruttore al momento della creazione.
2
error: 'double Particella::massa_' is private within this context.
3
L’operatore . chiama il metodo sull’oggetto. Il programma stampa Energia cinetica: 100 J.

Esercizi lampo: cosa stampa?

Senza portatile. In entrambi i casi la risposta più immediata è sbagliata.

1.

#include <iostream>

void f(int v[]) { std::cout << sizeof(v) << "\n"; }

int main() {
    int v[5];
    std::cout << sizeof(v) << "\n";
    f(v);
}

2.

int v[] = {10, 20, 30, 40, 50};
int* p = v + 3;
std::cout << p - v << " " << *(v + 1) << " " << 2[v] << "\n";

Esercizi lampo: le risposte

1. Stampa 20 e poi 8. Nel main v è un array di 5 interi e sizeof restituisce 20 byte. Passato a f, l’array decade a int* e sizeof misura il puntatore: 8 byte su una macchina a 64 bit. Questo dimostra che un array non è un puntatore: lo diventa nel passaggio alla funzione, e l’informazione sulla lunghezza va persa. Per questo computeLoss riceve anche n. In questo caso g++ -Wall avvisa, con 'sizeof' on array function parameter 'v' will return size of 'int*'.

2. Stampa 3 20 30. I primi due sono normale aritmetica dei puntatori. Il terzo è 2[v]: sembra un errore di battitura e invece compila, perché a[i] è definito, come in C, come *(a + i); la somma è commutativa, quindi 2[v] è *(2 + v), cioè v[2]. Non va usato in pratica, ma mostra che [] è solo una notazione per l’aritmetica dei puntatori.

Esercizio finale: misure e statistiche

Due classi: Misure conserva i dati; Statistiche ne calcola media e deviazione standard. x_ è privato in Misure: Statistiche deve usarne l’interfaccia pubblica.

1. La classe Misure

  • Prima della classe, la costante const int MAX_MISURE = 100;.
  • Attributi privati: double x_[MAX_MISURE] (le misure) e int n_ (quante sono).
  • Costruttore senza parametri, che pone n_ a zero.
  • void aggiungi(double v): se n_ < MAX_MISURE scrive v in x_[n_] e incrementa n_, altrimenti ignora v.
  • int numero() restituisce n_; double valore(int i) restituisce x_[i].

Esercizio finale: misure e statistiche (segue)

2. La classe Statistiche

  • Attributi privati: double media_ e double sigma_.
  • Costruttore Statistiche(Misure& m): con m.numero() e m.valore(i) calcola la media \(\bar x = \sum_i x_i / n\) e la deviazione standard campionaria \(\sigma = \sqrt{\sum_i (x_i - \bar x)^2 / (n-1)}\) (std::sqrt è in <cmath>), e le salva negli attributi.
  • double media() e double sigma() restituiscono i valori calcolati.

3. Il main: crea un oggetto Misure, vi aggiunge cinque misure del periodo di un pendolo (2,01; 1,98; 2,03; 1,99; 2,00 s), crea un oggetto Statistiche passandogli le misure e stampa media e deviazione standard.

Soluzione dell’esercizio finale: la classe Misure

#include <iostream>
#include <cmath>
1const int MAX_MISURE = 100;
class Misure {
private:
    double x_[MAX_MISURE];
    int n_;
public:
2    Misure() { n_ = 0; }
    void aggiungi(double v) {
3        if (n_ < MAX_MISURE) {
            x_[n_] = v;
            ++n_;
        }
    }
    int numero() { return n_; }
    double valore(int i) { return x_[i]; }
};
1
Costante di compilazione: può dare la dimensione dell’array.
2
Senza costruttore n_ avrebbe un valore indeterminato.
3
Come imposta di Temperatura: il metodo impedisce di uscire dall’array.

Soluzione dell’esercizio finale: la classe Statistiche

class Statistiche {
private:
    double media_;
    double sigma_;
public:
1    Statistiche(Misure& m) {
        int n = m.numero();
        double somma = 0.0;
        for (int i = 0; i < n; ++i) somma += m.valore(i);
        media_ = somma / n;
        double s2 = 0.0;
        for (int i = 0; i < n; ++i) {
2            double d = m.valore(i) - media_;
            s2 += d * d;
        }
        sigma_ = std::sqrt(s2 / (n - 1));
    }
    double media() { return media_; }
    double sigma() { return sigma_; }
};
1
Per reference (Lezione 05): nessuna copia delle misure.
2
m.x_[i] darebbe error: 'double Misure::x_ [100]' is private within this context.

Soluzione dell’esercizio finale: il main

int main() {
    Misure periodo;
    periodo.aggiungi(2.01);
    periodo.aggiungi(1.98);
    periodo.aggiungi(2.03);
    periodo.aggiungi(1.99);
    periodo.aggiungi(2.00);

1    Statistiche s(periodo);
    std::cout << "T = " << s.media() << " s, sigma = " << s.sigma() << " s\n";
    return 0;
}
1
Il costruttore calcola tutto una volta sola; media() e sigma() leggono soltanto. Il programma stampa T = 2.002 s, sigma = 0.0192354 s.

Nota

Il calcolo richiede almeno due misure: con n < 2 la formula divide per zero. Per renderla robusta, il costruttore dovrebbe controllare n prima di dividere.