Costanti, Aritmetica dei Puntatori e Introduzione alle Classi
struct del C alle classipublic e privateCome 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).
error: assignment of read-only variable 'learning_rate'.
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 constexprconstexpr 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;
}const intero con valore costante, come constexpr int N1 = 5;.
double v[N2]; sarebbe un array di lunghezza variabile.
error: call to non-'constexpr' function 'int leggi()'.
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).
double.
constexpr al posto di #defineIn C le costanti si definiscono spesso con il preprocessore (#define N_PIXEL 784, Lezione 04). In C++ si preferisce constexpr.
2 * N diventa 2 * 10 + 1.
21 22.
#define non ha tipo né scope, e nei messaggi d’errore compare il testo sostituito, non il nome.
const o constexprconstexpr 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;
}Come in C, le dichiarazioni con const e puntatori si leggono da destra verso sinistra.
Puntatore costante:
Lettura: b → const, è costante → *, puntatore → int, a un intero.
Nota
Con questa regola si leggono senza ambiguità anche le dichiarazioni più complicate, e i messaggi d’errore che le riportano.
int * const)L’indirizzo contenuto nel puntatore non può cambiare, ma attraverso il puntatore si può modificare il valore puntato.
b è un puntatore costante: contiene l’indirizzo di a e non può contenerne un altro.
a attraverso b.
error: assignment of read-only variable 'b'.
const int *)Il valore puntato non si può modificare attraverso il puntatore, ma al puntatore si può assegnare un altro indirizzo.
b l’intero è in sola lettura; a resta modificabile direttamente.
b punta a c.
error: assignment of read-only location '* b'.
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.
float costante.
error: assignment of read-only location '*(const float*)b'.
error: assignment of read-only variable 'b'.
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 |
Nota
Un reference resta legato all’oggetto iniziale: il suo const riguarda sempre il valore.
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.
&: v3 decade automaticamente all’indirizzo del primo elemento, &v3[0].
10.
Come in C, sommando un intero k a un puntatore si ottiene l’indirizzo dell’elemento k posizioni più avanti.
20.
40.
sizeofDi 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.
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.
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.
0 come un valore qualsiasi preso dalla memoria dello stesso processo (quella di altri programmi è protetta dal sistema operativo), oppure terminare con Segmentation fault.
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;
}4: da v[0] a v[4] ci sono quattro passi.
Il meccanismo è quello della Lezione 05; qui lo rivediamo su un caso del framework. Uno scheduler dimezza il learning rate a fine epoca:
decay riceve una copia.
0.2: lo scheduler ha dimezzato la sua copia, che viene distrutta all’uscita dalla funzione; learning_rate nel main non cambia.
Con una &, eta diventa un alias di learning_rate: nessuna copia.
&.
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.
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;
}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\).
loss_finale va dichiarata prima della chiamata, che le assegna il valore.
Dalla sola chiamata computeLoss(a, b, c, d) non si distingue quali argomenti sono input e quale è l’output. È preferibile restituire il risultato:
double in uscita.
Importante
Per calcolare sia la loss sia l’accuratezza con una sola chiamata, però, il return non basta: restituisce un solo valore.
struct, come in CLa 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;
}Result, senza struct davanti (Lezione 05).
struct Result computeMetrics(void).
La programmazione orientata agli oggetti (OOP) riunisce dati e operazioni in oggetti, e si basa su tre concetti:
public) è separata dall’implementazione (private); chi usa un oggetto conosce solo l’interfaccia. Oggi e Lezione 07.virtual (Lezione 12).DenseLayer, SigmoidLayer e Sequential derivano da layer.Con l’overloading degli operatori si potrà scrivere c = a + b; anche per i propri tipi, come per i double.
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.
Esempio dal framework: Classe: DenseLayer Attributi: la matrice dei pesi, il numero di neuroni. Metodi: forward(), backward().
public: rende accessibili dall’esterno (per esempio dal main) i membri che seguono.
; dopo la parentesi graffa finale è obbligatorio, come per le struct del C.
public e privateI 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.
_ è una convenzione diffusa, non una regola del linguaggio (Lezione 07).
Per garantire che kelvin_ abbia un valore definito fin dalla creazione dell’oggetto si usa un costruttore.
Temperatura creata: 273.15 K.
Il costruttore:
void;Senza costruttore, kelvin_ avrebbe un valore indeterminato finché non gli si assegna un valore.
Al costruttore si possono passare argomenti quando si crea l’oggetto.
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.
error: no matching function for call to 'Result::Result()': mancano i parametri.
class e structIn C++ struct e class differiscono solo per la visibilità di default.
struct tutto è public, finché non si scrive private:.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.
La classe Particella. Scrivete un programma C++ che:
Particella con due attributi privati, massa_ e velocita_;double e li assegna agli attributi;calcola_energia() che restituisce l’energia cinetica \(\frac{1}{2}mv^2\);main() crei una Particella con massa \(2{,}0\) e velocità \(10{,}0\), chiami calcola_energia() e stampi il risultato.Compilatelo dal terminale, con -Wall.
particella.cpp
mainparticella.cpp
error: 'double Particella::massa_' is private within this context.
. chiama il metodo sull’oggetto. Il programma stampa Energia cinetica: 100 J.
Senza portatile. In entrambi i casi la risposta più immediata è sbagliata.
1.
2.
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.
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
const int MAX_MISURE = 100;.double x_[MAX_MISURE] (le misure) e int n_ (quante sono).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].2. La classe Statistiche
double media_ e double sigma_.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.
Misuren_ avrebbe un valore indeterminato.
imposta di Temperatura: il metodo impedisce di uscire dall’array.
Statisticheclass 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_; }
};m.x_[i] darebbe error: 'double Misure::x_ [100]' is private within this context.
mainint 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;
}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.