C++ - tipi, variabili, espressioni e dichiarazioni
In questa pagina 6
I mattoni di ogni programma: come si rappresenta un dato (tipo, oggetto, valore), come si scrive e si legge (inizializzazione, assegnamento, conversioni), come si combina (espressioni e operatori) e come si introducono i nomi (dichiarazioni e definizioni). Il luogo in cui un nome è valido è in C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →; la vita in memoria delle variabili è in C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →. I tipi int, double, char e gli operatori sono quelli già visti in Tipi, operatori e controllo del flusso in CTipi interi e reali del C con dimensioni e limiti, conversioni implicite e cast, divisione intera, operatori di incremento, logici e bit a bit; if, switch, while, do-while, for, break e continue.Tipi, operatori e controllo del flusso in C →; qui interessano le differenze di C++.
Tipo, oggetto, valore, variabile
Definizione.
- Tipo: stabilisce la natura del contenuto, l'intervallo dei valori e l'insieme delle operazioni possibili su un oggetto.
- Oggetto: una regione di memoria che ha un tipo; il tipo dice quale dato ci può stare.
- Valore: la configurazione di bit contenuta nell'oggetto, interpretata secondo il tipo.
- Variabile: un oggetto con un nome (named object).
int age = 42; crea un oggetto di tipo int (4 byte), lo chiama age, e ci scrive il valore 42. Lo stesso schema di bit può significare un intero o un carattere: è il tipo a decidere. Ogni tipo ha il suo formato di literal:
| literal | tipo |
|---|---|
39 |
int |
3.5 |
double |
'.' |
char |
"Annemarie" |
stringa |
true |
bool |
Il tipo determina anche le operazioni consentite e il loro significato: + fra due int somma, fra due string concatena, fra due bool è una somma di interi, e - fra due string non esiste.
#include <iostream>
int main()
{
int age = 42; // definizione + inizializzazione
double pi = 3.14159;
char c = 'x';
bool ok = true;
std::cout << "age = " << age << ", pi = " << pi << ", c = " << c
<< ", ok = " << ok << '\n'; // un bool si stampa come 0 o 1
// sizeof: dimensione in byte, accetta un tipo o una variabile
std::cout << "sizeof(char) = " << sizeof(char) << '\n';
std::cout << "sizeof(bool) = " << sizeof(bool) << '\n';
std::cout << "sizeof(int) = " << sizeof(int) << " = sizeof(age) = " << sizeof(age) << '\n';
std::cout << "sizeof(double) = " << sizeof(double) << '\n';
age = 7; // assegnamento: cambia il valore dell'oggetto esistente
age += 3; // assegnamento composto: age = age + 3
std::cout << "age = " << age << '\n';
return 0;
}age = 42, pi = 3.14159, c = x, ok = 1
sizeof(char) = 1
sizeof(bool) = 1
sizeof(int) = 4 = sizeof(age) = 4
sizeof(double) = 8
age = 10sizeof dà la dimensione in byte di un tipo o di una variabile. I valori dipendono dall'implementazione: qui sono quelli di una macchina a 32 bit di indirizzi (WebAssembly), int = 4 byte; su altre architetture long o i puntatori cambiano.
Inizializzazione e assegnamento
Il simbolo = compare in due operazioni diverse:
| operazione | quando | esempio |
|---|---|---|
| inizializzazione | alla nascita dell'oggetto: il primo valore | int a = 3; |
| assegnamento | su un oggetto già esistente: ne cambia il valore | a = 4; |
| assegnamento composto | combina operazione e assegnamento | a += 7; (a = a + 7), b -= 9, c *= 2 |
Inizializzare sempre le variabili. Una variabile locale non inizializzata ha un valore indeterminato: leggerla è un errore che si manifesta a caso (a volte funziona, a volte no, a seconda di cosa c'era in memoria). Nel codice
void f(int z)
{
int x; // non inizializzato
if (x > z) { /* ... */ } // legge x: comportamento non definito
x = 7;
}il bug dipende da ciò che la memoria conteneva prima: è "la ricetta per bug oscuri". Le variabili globali e static invece partono da 0 (C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →).
Forme di inizializzazione
#include <iostream>
#include <vector>
int main()
{
int a; // NON inizializzata: valore indeterminato (mai leggerla!)
a = 7; // dopo l'assegnamento va bene
double d = 7; // inizializzazione con =: 7 diventa 7.0
std::vector<int> vi(10); // sintassi (): 10 elementi, tutti 0
std::vector<int> vi2 {1, 2, 3, 4}; // sintassi {}: 4 elementi con quei valori
const int x = 7; // inizializzatore obbligatorio per una costante
const int x2 {9};
// const int y; // errore: costante senza inizializzatore
std::cout << a << ' ' << d << ' ' << vi.size() << ' ' << vi[3] << ' '
<< vi2.size() << ' ' << vi2[3] << ' ' << x << ' ' << x2 << '\n';
std::vector<int> v1(3); // tre elementi a 0
std::vector<int> v2 {3}; // UN elemento che vale 3
std::cout << v1.size() << ' ' << v2.size() << ' ' << v2[0] << '\n';
return 0;
}7 7 10 0 4 4 7 9
3 1 3=,()e{}sono tre sintassi per inizializzare. La graffa{}è consigliata: è dedicata all'inizializzazione, è esplicita e vieta le conversioni che perdono informazioni (sotto). L'uguale resta per le inizializzazioni semplici.- Attenzione al caso
vector<int> v1(3)controvector<int> v2 {3}: le parentesi chiedono tre elementi a zero, la graffa dà un solo elemento col valore 3 (ci arriveremo in C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_listpermette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento → ). - Una costante deve avere l'inizializzatore;
const int y;dà errore:default initialization of an object of const type 'const int'.
Type safety e conversioni
C++ è fortemente tipizzato: ogni oggetto ha un tipo e le operazioni lo rispettano. Passare un dato da una variabile a una di tipo diverso pone il problema della type safety.
| conversione | verso | esempi | sicura? |
|---|---|---|---|
| sicura (type safe) | un tipo con maggiore capacità: preserva il valore (o la sua miglior approssimazione) | bool char, bool int, bool double, char int, char double, int double |
sì |
| non sicura (narrowing) | un tipo con minore capacità: il valore può cambiare | double int, double char, double bool, int char, int bool, char bool |
no |
Le conversioni non sicure sono accettate per ragioni storiche (eredità del C) e il compilatore le segnala di rado: le sapete riconoscere voi. Il programma seguente mostra le più importanti.
#include <iostream>
int main()
{
// 1. conversione NON sicura: char non contiene 20000
int a = 20000;
char c = a; // troncamento silenzioso (narrowing)
int b = c;
if (a != b)
std::cout << "Ops! a = " << a << ", b = " << b << '\n';
else
std::cout << "Wow! Char e' molto grande!\n";
// 2. conversioni sicure: nessuna perdita
double d1 = 2.3;
double d2 = d1 + 2; // 2 diventa 2.0
std::cout << "d2 = " << d2 << '\n';
// 3. int e double mescolati: la divisione dipende dai tipi
double d = 2.5;
int i = 2;
double q1 = d / i; // double / int -> double / double = 1.25
int q2 = d / i; // 1.25 viene troncato a 1
int q3 = 5 / 2; // divisione fra interi: 2
double q4 = 5 / 2; // divisione fra interi (2), poi 2 -> 2.0
double q5 = 5.0 / 2; // 2.5
std::cout << q1 << ' ' << q2 << ' ' << q3 << ' ' << q4 << ' ' << q5 << '\n';
// 4. due modi per convertire esplicitamente
int t1 = int(d); // senza controllo
int t2 = static_cast<int>(d); // esplicito e cercabile nel codice
std::cout << t1 << ' ' << t2 << '\n';
// 5. la graffa vieta il narrowing
double x {2.7}; // ok
int ok1 {1000}; // ok
char ok2 {48}; // ok: il literal 48 sta in un char
std::cout << x << ' ' << ok1 << ' ' << ok2 << '\n';
// int y {x}; // errore di compilazione: narrowing da double a int
// char b1 {1000}; // errore: 1000 non sta in un char
return 0;
}Ops! a = 20000, b = 32
d2 = 4.3
1.25 1 2 2 2.5
2 2
2.7 1000 0Lettura dell'uscita:
char c = a;: 20000 in binario è0100 1110 0010 0000; unchartiene solo gli 8 bit bassi,0010 0000. Quindibvale 32,a != be il programma stampaOps!: nessun errore, solo un valore sbagliato. Se uncharfosse "molto grande" varrebbeWow.2diventa2.0ind1 + 2: conversione sicura.d / icond = 2.5ei = 2converteiindouble, dà1.25; assegnato a unintsi tronca a1(la parte decimale si perde).5 / 2fra interi è2edouble q4 = 5 / 2stampa2: la divisione è già stata fatta fra interi.int(d)estatic_cast<int>(d)convertono entrambi senza controllo; il secondo si cerca facilmente nel codice e dice agli altri programmatori che il troncamento è intenzionale.char ok2 {48}va bene perché 48 sta in unchar, ma stampa0: il carattere con codice ASCII 48 è la cifra0.
La graffa blocca il narrowing
La notazione {} (universal and uniform initialization, C++11) trasforma le conversioni non sicure in errori di compilazione. Il compilatore controlla anche i literal:
double x {2.7}; // ok
int y {x}; // errore: type 'double' cannot be narrowed to 'int'
int a {1000}; // ok
char b {a}; // errore: narrowing da int a char
char b1 {1000}; // errore: costante 1000 che non sta in un char
char b2 {48}; // okLe parentesi Tipo (valore) convertono invece senza controllo. Con -Wall -Wextra int w = x; (da double) non produce nemmeno un avviso: il controllo è solo con {}.
Dare nomi alle variabili
I nomi sono scritti per gli umani: significativi ed evocativi, ma non troppo lunghi (element_count meglio di the_number_of_elements); evitare acronimi che confondono (mtbf, TLA, nbv). Un nome breve va bene solo se è una convenzione o ha uno scope ristretto (l'indice i di un ciclo). Più è grande lo scope, più il nome deve essere descrittivo. Scegliere una convenzione e rispettarla rende il codice molto più leggibile; quella suggerita è la Google C++ Style Guide (sezione Naming): per esempio snake_case per variabili e funzioni, CamelCase per i tipi, kNomeCostante per le costanti, membro_ con trattino finale per i dati membro.
Espressioni
Definizione. Un'espressione calcola un risultato da uno o più operandi. È il più piccolo elemento con cui si esprime la computazione. Le espressioni semplici sono i literal (
10,'a',3.14,"Norah") e i nomi di variabili; si combinano con gli operatori in espressioni più complesse:int perimeter = (length + width) * 2;.
lvalue e rvalue
Un lvalue (left value) è ciò che può stare a sinistra di un assegnamento: un oggetto in memoria, come una variabile. Un nome di variabile può essere usato in due modi:
- come lvalue: si riferisce all'oggetto, la scatola in cui scrivere (
length = 99;); - come rvalue: si riferisce al valore contenuto (
width = length * 2;).
length /= 2; li usa entrambi: legge length e poi ci riscrive. Un literal come 99 è solo un rvalue: 99 = length; non ha senso.
Costanti: constexpr e const
Le costanti simboliche vanno scritte con un nome, non come magic number nel codice, e meglio con una variabile costante che con una macro #define perché la variabile ha un tipo.
| keyword | richiede | esempio |
|---|---|---|
constexpr |
valore noto a tempo di compilazione | constexpr int max = 100; |
const |
un valore che non cambia dopo l'inizializzazione, anche calcolato solo a tempo di esecuzione | const int c2 = n + 7; |
#include <iostream>
constexpr double pi = 3.14159; // noto a tempo di compilazione
constexpr int max_n = 100;
double perimetro(double r)
{
return 2 * pi * r; // pi e' di sola lettura
}
void usa(int n)
{
constexpr int c1 = max_n + 7; // ok: costruita da costanti
const int c2 = n + 7; // ok: const, valore noto solo a run-time
// constexpr int c3 = n + 7; // errore: n non e' una costante di compilazione
// c2 = 7; // errore: c2 e' costante
std::cout << "c1 = " << c1 << ", c2 = " << c2 << '\n';
}
int main()
{
std::cout << "perimetro = " << perimetro(2.0) << '\n';
usa(5);
// pi = 7; // errore: assegnamento a una costante
return 0;
}perimetro = 12.5664
c1 = 107, c2 = 12constexpr int c3 = n + 7; è un errore perché n è un parametro, noto solo a run-time. Una constexpr è anche const; il contrario no.
Side effect e ordine di valutazione
Gli operatori hanno 1, 2 o 3 operandi e danno un risultato. Alcuni hanno anche un side effect: modificano un operando oltre a dare un risultato. Hanno side effect ++, --, +=, -=, =... Esempio: con i che vale 5, ++i dà 6 e porta i a 6; i++ dà il vecchio valore (6 se i era già 6) e solo dopo incrementa.
#include <iostream>
int main()
{
int length = 99; // length e' un lvalue: e' una "scatola"
length /= 2; // la usa come lvalue (a sinistra) e come rvalue (letta)
int width = length * 2; // length come rvalue: ne conta il valore
int perimeter = (length + width) * 2;
std::cout << "length = " << length << ", width = " << width
<< ", perimeter = " << perimeter << '\n';
// operatori con side effect: modificano l'operando E danno un risultato
int i = 5;
int pre = ++i; // i diventa 6, il risultato e' 6
int post = i++; // il risultato e' 6 (il vecchio valore), poi i diventa 7
std::cout << "pre = " << pre << ", post = " << post << ", i = " << i << '\n';
// MAI usare la stessa variabile due volte se uno dei due usi ha un side effect:
// v[i] = ++i; ordine di valutazione non definito
// int x = ++i + ++i; idem
// Si scrive in due istruzioni, con un ordine chiaro:
int v[10] = {0};
i = 3;
++i;
v[i] = i;
std::cout << "v[4] = " << v[4] << '\n';
return 0;
}length = 49, width = 98, perimeter = 294
pre = 6, post = 6, i = 7
v[4] = 4L'ordine in cui sono valutati gli operandi non è definito: in (length + width) * 2 è obbligatorio fare prima la somma e poi il prodotto, ma non è stabilito se si legge prima length o prima width. Lasciare libertà al compilatore permette ottimizzazioni, ma significa che il risultato è indefinito se la stessa variabile compare più volte e almeno uno degli usi ha un side effect:
v[i] = ++i; // i a sinistra è letto prima o dopo ++i? non definito
v[++i] = i; // idem
int x = ++i + ++i; // lettura e incremento non necessariamente consecutivi
cout << ++i << ' ' << i << '\n'; // non definitoNemmeno = è esente: non è stabilito quale dei due operandi si valuti per primo. Regola pratica: mai usare la stessa variabile più di una volta in un'espressione in cui un uso la modifica; si spezza in due istruzioni, come nel programma sopra.
Operatori sensibili al tipo
Il significato di un operatore dipende dai tipi degli operandi. << (inserimento in uno stream) e il suo complementare >> (estrazione da uno stream) trasformano fra la forma testuale e la rappresentazione interna, quindi guardano il tipo della variabile.
#include <iostream>
#include <string>
int main()
{
std::cout << "Please enter your first name and age\n";
std::string first_name;
int age;
std::cin >> first_name; // >> legge secondo il TIPO di destinazione
std::cin >> age;
std::cout << "Hello, " << first_name << " (age " << age << ")\n";
// altri operatori sensibili al tipo
int count = age;
std::string name = first_name;
int c2 = count + 2; // somma di interi
std::string s2 = name + " Jr."; // concatenazione di stringhe
int c3 = count - 2; // sottrazione di interi
// std::string s3 = name - " Jr."; // errore: '-' non e' definito per le stringhe
std::cout << c2 << ' ' << c3 << ' ' << s2 << '\n';
return 0;
}Con l'ingresso Norah 21:
Please enter your first name and age
Hello, Norah (age 21)
23 19 Norah Jr.cin >> first_name legge fino al primo spazio e riempie una stringa; cin >> age legge le cifre e le converte in int. count + 2 somma, name + " Jr." concatena; name - " Jr." non compila perché - non è definito per le stringhe. Il C++ permette di definire gli operatori per i tipi nuovi (C++ - enumerazioni e overloading degli operatoriun'enumerazione è un tipo definito dall'utente che specifica un insieme di valori simbolici (enumeratori); enum class crea uno scope (Month::jan), non si converte implicitamente in int, e i valori partono da 0 o dal valore del precedente più uno; l'overloading degli operatori permette di definire gli operatori esistenti per tipi definiti dall'utente (almeno un operando UDT, stesso numero di operandi e stessa sintassi) scrivendo una funzione di nome operator+, operator++, operator[], ...; si può fare con una helper function (due argomenti per un operatore binario, il primo può essere di qualunque tipo) o con una funzione membro (un argomento in meno, perché il primo operando è l'oggetto, quindi l'UDT deve stare a sinistra: T + int sì, int + T no); << e >> sono helper function che prendono e restituiscono ostream& e istream&; ++ ha due forme (pre e post, quest'ultima con un int fittizio); gli operatori più utili sono =, ==, !=, <, [], ().C++ - enumerazioni e overloading degli operatori →).
Istruzioni, dichiarazioni, definizioni
Un'istruzione (statement) è una parte di codice che specifica un'azione e termina con ; (necessario per interpretare il codice). Le direttive del preprocessore come #include non sono istruzioni. I tipi di istruzione: di espressione (l'azione è la valutazione di un'espressione), dichiarazioni, di selezione (if, switch), di iterazione (while, for).
Dichiarazione e definizione
Definizione (dichiarazione). Una dichiarazione introduce un nome in uno scope, specificando il nome, il tipo e, facoltativamente, un inizializzatore. Un nome deve essere dichiarato prima di essere usato.
Definizione (definizione). Una definizione è una dichiarazione che specifica completamente l'entità dichiarata. Una definizione è anche una dichiarazione; ma alcune dichiarazioni non sono definizioni.
Che cosa significa "completamente" dipende dall'entità:
| entità | la dichiarazione dà | la definizione dà in più |
|---|---|---|
| variabile | nome e tipo (nulla sulla memoria) | un oggetto: riserva memoria |
| funzione | nome e tipo (argomenti e valore restituito) | il corpo |
| classe | nome | variabili e funzioni membro |
#include <iostream>
int func(); // dichiarazione di funzione (non e' una definizione)
extern int contatore; // dichiarazione di variabile: nessuna memoria riservata qui
int contatore = 7; // definizione: riserva la memoria
int main()
{
int x = func(); // uso PRIMA della definizione di func: va bene, e' dichiarata
std::cout << "x = " << x << ", contatore = " << contatore << '\n';
return 0;
}
int func() // definizione: fornisce anche il corpo
{
return 2;
}x = 2, contatore = 7Altri esempi, riga per riga:
int x = 7; // definizione (riserva memoria)
extern int x; // dichiarazione (extern: "definita altrove")
extern int x; // un'altra dichiarazione: ripetere è lecito
double sqrt(double); // dichiarazione
double sqrt(double d) { /* ... */ }// definizione
double sqrt(double); // altra dichiarazione, lecita
int sqrt(double); // ERRORE: dichiarazione incoerente col tipo di ritornoUna definizione può comparire una volta sola, le dichiarazioni quante se ne vuole purché coerenti. In C++ si definiscono variabili, costanti, funzioni, namespace, tipi (classi, enum) e template.
Perché esistono entrambe? Per usare un'entità prima di darne la definizione completa e, soprattutto, per gestire più file sorgente: la dichiarazione permette a tutti i file di conoscere un'entità senza conoscere i dettagli implementativi. Dichiarazione contro definizione è lo stesso che interfaccia contro implementazione: la prima dice cosa serve per usare qualcosa, la seconda cosa serve perché faccia quel che deve. Le dichiarazioni si raccolgono negli header (C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →).
In alcuni contesti "dichiarazione" significa soltanto "dichiarazione che non è anche una definizione"; in queste note e nel corso la definizione è anche dichiarazione.
Domande d'esame
1. Differenza tra const e constexpr.
Traccia. Entrambe indicano un valore che non si può cambiare. constexpr richiede un valore noto a tempo di compilazione; const va bene anche con un valore noto solo a tempo di esecuzione (const int c2 = n + 7;). Non sono equivalenti: ogni constexpr è const, non il contrario. Meglio delle macro perché hanno un tipo.
2. float x {3}; e int y {2.5}; compilano? Che cos'è il narrowing?
Traccia. float x {3}; compila e inizializza x a 3: il literal 3 ha un valore rappresentabile esattamente. int y {2.5}; no: conversione double int che perde informazione (narrowing), vietata dalla graffa. Con = o () sarebbe accettata e darebbe 2.
3. Che cosa fa v[i] = ++i;? Perché è un errore?
Traccia. Il valore dell'indice non è conoscibile a priori: l'ordine di valutazione degli operandi di = non è definito, quindi i a sinistra può essere letto prima o dopo ++i. Comportamento indefinito (anche accesso fuori dal vettore). Si risolve con due istruzioni.
4. Differenza tra dichiarazione e definizione, con tre esempi.
Traccia. La dichiarazione introduce nome e tipo; la definizione fornisce anche l'entità (memoria, corpo, membri). extern int a; e double sqrt(double); sono dichiarazioni; int a = 7; e double sqrt(double d) { ... } sono definizioni.
Versione ripasso
- Tipo = contenuto + valori + operazioni; oggetto = memoria con tipo; valore = bit letti secondo il tipo; variabile = oggetto con nome. Literal:
39int,3.5double,'.'char,"s"stringa,truebool.sizeof(tipo o variabile)= byte (dipende dall'implementazione). Rimando: Tipi, operatori e controllo del flusso in CTipi interi e reali del C con dimensioni e limiti, conversioni implicite e cast, divisione intera, operatori di incremento, logici e bit a bit; if, switch, while, do-while, for, break e continue.Tipi, operatori e controllo del flusso in C →. - Inizializzazione (primo valore:
int a = 3;,int a {3};,vector<int> v(10);) assegnamento (a = 4;,a += 7;). Inizializzare sempre: una variabile non inizializzata ha valore indeterminato; globali e statiche partono da 0. Unaconstva inizializzata sempre. - Conversioni sicure (verso tipo più capiente):
boolcharintdouble. Non sicure (narrowing):doubleint/char/bool,intchar/bool,charbool. - Graffe
{}vietano il narrowing a tempo di compilazione (anche sui literal:char b {1000}errore,char b {48}ok);Tipo(valore)e=convertono senza controllo. Esempio:char c = 20000;tiene gli 8 bit bassi, 32.int i2 = 2.5/2;dà 1;5/2= 2 (divisione fra interi). Cast esplicito:static_cast<int>(d). - Nomi: significativi, non troppo lunghi, convenzione unica (Google C++ Style Guide:
snake_case,membro_,kCostante). - Espressione = operandi + operatori. lvalue = può stare a sinistra di
=(oggetto in memoria); rvalue = valore. Un nome di variabile è lvalue (length = 99) o rvalue (length * 2). constexpr= valore noto in compilazione;const= non cambia dopo l'inizializzazione (anche calcolato a run-time); meglio dei#defineperché tipizzate.- Side effect:
++,--,+=,=modificano un operando. Ordine di valutazione non definito:v[i] = ++i;,v[++i] = i;,x = ++i + ++i;sono indefiniti non usare la stessa variabile più volte se una modifica; si spezza in due istruzioni. - Operatori sensibili al tipo:
<<inserisce,>>estrae (cin >> name >> age);string + stringconcatena,string - stringnon esiste. - Dichiarazione = nome + tipo (+ inizializzatore); definizione = dichiarazione completa (variabile: riserva memoria; funzione: corpo; classe: membri).
extern int x;edouble sqrt(double);dichiarano;int x = 7;edouble sqrt(double d){...}definiscono. Dichiarazioni ripetibili e coerenti, definizione una volta sola. Dichiarazione = interfaccia, definizione = implementazione: serve per usare un'entità prima di definirla e per i file multipli. C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →. - Esempio:
int x = func();conint func();dichiarata sopra e definita dopomaincompila.
In codice
int age = 42; // definizione + inizializzazione
age = 7; age += 3; // assegnamenti (age = 10)
int a = 20000; char c = a; // narrowing silenzioso: c = 32, int b = c; a != b
double d = 2.5; int i = 2;
double q1 = d / i; // 1.25 (i diventa 2.0)
int q2 = d / i; // 1 (si tronca)
double q4 = 5 / 2; // 2 (divisione fra interi, poi 2 -> 2.0)
int t = static_cast<int>(d); // cast esplicito, 2
int y {d}; // ERRORE: type 'double' cannot be narrowed to 'int'
char b {48}; // ok, ma stampa il carattere '0'
constexpr int max = 100; // noto in compilazione
const int c2 = n + 7; // noto solo a run-time: const, non constexprint func(); // dichiarazione (poi usata in main)
extern int contatore; // dichiarazione di variabile
int contatore = 7; // definizione: riserva memoria
int func() { return 2; } // definizione: ha il corpoCome rispondere alle domande tipiche
constcontroconstexpr: entrambe immutabili;constexpresige un valore noto a tempo di compilazione,constanche a tempo di esecuzione; non sono equivalenti maconstexprimplicaconst.Perché
v[i] = ++i;è sbagliato:iè letto e modificato nella stessa espressione e l'ordine non è definito valore dell'indice non conoscibile, possibile accesso fuori dal vettore (segmentation fault). Si scrive++i; v[i] = i;.Narrowing: conversione verso un tipo meno capiente; accettata da
=e()per eredità dal C, vietata da{}ed evidenziata dal compilatore solo con la graffa.Dichiarazione e definizione: la prima introduce nome e tipo, la seconda crea anche l'entità; tre coppie di esempi (variabile, funzione, classe); servono per usare un'entità prima di definirla e per i file multipli (C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →).
Forme di inizializzazione:
int a = 3;,double d = 7;(7 diventa 7.0),vector<int> vi(10);(10 elementi a 0),vector<int> vi2 {1,2,3,4};,const int x2 {9};; attenzionevector<int> v1(3)(tre zeri) controvector<int> v2 {3}(un solo elemento, 3).const int y;errore.Esempio di lettura: con ingresso
Norah 21,cin >> first_name >> age;produceHello, Norah (age 21);count + 2= 23,count - 2= 19,name + " Jr."=Norah Jr..Esempio di lvalue/rvalue:
int length = 99; length /= 2;(lvalue e rvalue insieme) 49;width = length * 2= 98;perimeter = (length + width) * 2= 294.++idà il valore nuovo,i++il vecchio.Conversioni, uscita del programma:
Ops! a = 20000, b = 32;d2 = 4.3;1.25 1 2 2 2.5(cioèd/i,inttroncato,5/2,double q4 = 5/2,5.0/2);2 2(cast);2.7 1000 0(ilchar48 stampa il carattere0).Costanti, uscita:
perimetro = 12.5664conconstexpr double pi = 3.14159;;c1 = 107(constexprda costanti),c2 = 12(const int c2 = n + 7;,n = 5).Errori tipici: credere che
char c = 20000;dia errore (tronca in silenzio);double q = 5/2;che dà 2; usare una variabile non inizializzata;v[i] = ++i;;constexprcon un parametro di funzione; scambiare dichiarazione e definizione.
Esercizi su questo argomento
- Esercizio - Hello world, variabili e funzioni (laboratorio 1)
- Esercizio - Quiz su constexpr, ordine di valutazione, dynamic_cast e lambda (primo appello 2022-23, quiz)
- Esercizio - Quiz su conversioni, constexpr, free store e downcasting (terzo appello 2021-22, quiz)
- Esercizio - Quiz su inizializzazione, free store, polimorfismo e passaggio di argomenti (quarto appello 2022-23, quiz)
- Esercizio - Quiz su operatore =, dangling pointer, enumerazioni e at() (quarto appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)
- Esercizio - Quiz su scope, riferimenti, conversioni e puntatori (secondo appello 2022-23, quiz)