Costanti, Aritmetica dei Puntatori e Introduzione alle Classi
Il C++ permette di assicurarsi che il valore di una variabile non cambi mai all’interno del suo scope tramite la keyword const. È fondamentale come contratto di sicurezza (es. il learning_rate di una Rete Neurale).
Leggere i puntatori costanti in C++ crea molta confusione. Il trucco è leggere sempre la dichiarazione da destra verso sinistra.
Puntatore Costante:
Lettura (da destra a sinistra): - b - const: è costante - *: puntatore - int: a un intero
Note
Imparare questo trucco ti salva ore di debug e di errori incomprensibili del compilatore.
int * const)Il puntatore stesso non può cambiare bersaglio, ma puoi usarlo per modificare il valore a cui punta!
b è un puntatore COSTANTE ad a. Il “tubo” è fisso.
c.
const int *)Il bersaglio è considerato intoccabile, ma il puntatore può muoversi altrove.
b.
c.
b.
const float* const)Unisce entrambe le restrizioni: Non puoi cambiare né il puntatore né il valore puntato! È l’accesso in sola lettura più sicuro del C++.
In C++ il nome di un array decade (decay) a un puntatore al suo primo elemento appena lo si usa in un’espressione. Non è un puntatore — la slide sul sizeof(), fra due, mostra la differenza — ma quasi sempre si comporta come tale.
v3 al puntatore d. Non serve usare & perché v3 è GIÀ l’indirizzo del primo elemento!
10.
Cosa succede se aggiungiamo o sottraiamo numeri a un puntatore? Il puntatore si sposta lungo la memoria RAM!
20.
40.
sizeof()Se avanzo un puntatore a int di 1 (d++), di quanti byte si sposta fisicamente nella RAM? Dipende dal tipo di dato!
| Tipo | Memoria (Tipica) | Salto in RAM per ptr++ |
|---|---|---|
char |
1 byte | Salta 1 byte |
short |
2 bytes | Salta 2 bytes |
int / float |
4 bytes | Salta 4 bytes |
double |
8 bytes | Salta 8 bytes |
Se d contiene l’indirizzo 0x7ffc3a5b1e40, fare d++ lo farà puntare a 0x7ffc3a5b1e44 (perché un int pesa 4 byte). L’aritmetica dei puntatori gestisce questa matematica automaticamente!
Note
La funzione sizeof(tipo) restituisce esattamente quanti byte occupa un tipo sulla tua macchina!
L’aritmetica non ha confini. Il C++ ti permette allegramente di superare la fine dell’array ed esplorare l’ignoto!
Puoi anche sottrarre due puntatori che guardano allo stesso array per calcolare la loro “distanza” in termini di elementi.
#include <iostream>
int main() {
int v[] = { 1, 2, 3, 4, 5, 6, 7 };
int* inizio = &v[0]; // Oppure solo: inizio = v;
int* fine = &v[4]; // Punta al quinto elemento (5)
1 int distanza = fine - inizio;
std::cout << "Distanza: " << distanza << " elementi\n";
return 0;
}Il meccanismo è quello della Lezione 5; qui lo rivediamo su un caso che vi servirà davvero. Uno scheduler dimezza il learning rate a fine epoca:
decay riceve una copia indipendente in RAM.
0.2. Lo scheduler ha dimezzato la sua copia, e l’ha buttata uscendo dalla funzione: il learning rate vero non si è mosso di un ε.
Basta una & perché eta sia un alias del learning rate vero, senza copie.
&, e cambia tutto.
0.1: adesso lo scheduler ha davvero schedulato.
Note
Nel Laboratorio 10 scriverete l’interfaccia Scheduler per davvero. Il fixedScheduler del framework tiene un puntatore all’Optimizer proprio per questo motivo: per poter mettere le mani sul learning rate di qualcun altro.
Come si passa un Array a una funzione per elaborarne i dati?
// Usiamo const double* per leggere l'array in sicurezza
// Usiamo double& per salvare il risultato
1void computeLoss(const double* predictions, int nData, double& loss) {
loss = 0.;
for(int i=0; i<nData; ++i) {
loss += predictions[i];
}
loss /= nData;
}
int main() {
double batch_preds[] = { 1.2, 0.9, 1.34, 1.67 };
double error_finale;
2 computeLoss( batch_preds, 4, error_finale );
return 0;
}void). L’output è scritto nella variabile passata per reference loss.
error_finale vuoto prima di poterlo riempire chiamando la funzione.
Il metodo appena visto è Anti-intuitivo. Leggendo computeLoss(a, b, c), chiunque non abbia scritto la funzione non sa capire quale sia l’input e quale l’output!
Facciamo restituire alla funzione direttamente il valore calcolato:
double in uscita.
Important
Ma cosa succede se volevamo calcolare sia la Loss che l’Accuratezza in un colpo solo? Una funzione in C++ può restituire return di un solo valore alla volta!
Immagina se potessimo dire al C++ di creare una nostra “Scatola” contenente due valori, e potessimo restituire l’intera scatola con il return!
Result creato da noi.
Questo ci porta a uno dei pilastri del C++…
La Programmazione Orientata agli Oggetti (OOP) ci permette di unire dati e funzioni all’interno di “Oggetti”. Una classe è il progetto (il “blueprint”) per creare questi oggetti.
Esempio (il nostro): Classe: DenseLayer Attributi: la matrice dei pesi, il numero di neuroni. Metodi: forward(), backward().
public: significa che i membri successivi sono accessibili dal main.
; alla fine della definizione della classe!
public vs privateIn C++, la buona norma è “nascondere” i dati all’utente (private) e consentire l’accesso solo tramite metodi controllati (public).
conto.saldo = 1000000; e arricchirsi illecitamente!
Come facciamo ad assicurarci che la variabile saldo inizi esattamente a zero alla nascita dell’oggetto, senza “spazzatura” di memoria? Con i Costruttori!
void).class ContoBancario {
private:
double saldo;
public:
1 ContoBancario() {
saldo = 0.0;
std::cout << "Conto creato con successo!\n";
}
};ContoBancario c; e mette saldo a zero. Senza, saldo conterrebbe spazzatura.
Possiamo passare argomenti al costruttore nel momento in cui creiamo l’oggetto.
_ alla fine dei nomi delle variabili private.
double passati per valore: il const-reference della Lezione 5 serve per gli oggetti pesanti, su un double costerebbe più di quanto risparmia.
Una domanda frequentissima: qual è la differenza in C++ tra struct e class?
Nessuna (quasi)! Fanno esattamente la stessa cosa e supportano le stesse identiche feature (metodi, costruttori, ecc.). L’unica, minuscola differenza è la visibilità di default:
struct: Tutto ciò che scrivi è public di default (fino a quando non scrivi private:).class: Tutto ciò che scrivi è private di default (fino a quando non scrivi public:).Note
Si usa struct quando si vuole creare solo un contenitore per trasportare pacchetti di variabili grezze senza molta logica (“Plain Old Data”). Si usa class quando ci sono comportamenti complessi e si vuole proteggere la memoria.
La classe Particella!
Particella.massa_ e velocita_.double e li assegni agli attributi privati.calcola_energia() che restituisca l’energia cinetica \(\frac{1}{2}mv^2\).main(), istanzia un oggetto di tipo Particella passandogli massa=2.0 e velocità=10.0.calcola_energia() sull’oggetto appena creato e stampa il risultato!(Provate a compilarlo dal terminale!)
Ecco la nostra definizione:
E ora mettiamola all’opera!
massa_ è privata, il main non può sbirciarci dentro.
.) per far agire l’oggetto. Restituisce 100.0.
Due minuti, senza portatile. In entrambi la risposta istintiva è sbagliata.
1.
2.
1. Stampa 20 e poi 8. Nel main v è un array di 5 interi e sizeof lo sa: 20 byte. Passato a f, l’array decade a int* e sizeof misura il puntatore: 8 byte su una macchina a 64 bit. È la dimostrazione che un array non è un puntatore — lo diventa attraversando la chiamata, e la lunghezza si perde per strada. Per questo computeLoss riceve anche nData. Buona notizia: g++ -Wall in questo caso vi avvisa, con 'sizeof' on array function parameter 'v' will return size of 'int*'.
2. Stampa 3 20 30. I primi due sono aritmetica dei puntatori normale. Il terzo è 2[v]: sembra un errore di battitura e invece compila, perché a[i] in C++ è definito come *(a + i) — e la somma è commutativa, quindi 2[v] è *(2 + v), cioè v[2]. Non scrivetelo mai, ma sapere perché funziona vuol dire aver capito cosa sono davvero le parentesi quadre.