Salta al contenuto
Note per Studenti C++ - tipi, variabili, espressioni e dichiarazioni

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.

tipi_sizeof.cppcpp
#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 = 10

sizeof 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

La differenza non si vede per i tipi semplici ma conta per le classi (costruire non è lo stesso che sovrascrivere: 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_list permette 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 →).

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

cpp
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

init_forme.cppcpp
#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

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 →\to char, bool →\to int, bool →\to double, char →\to int, char →\to double, int →\to double sì
non sicura (narrowing) un tipo con minore capacità: il valore può cambiare double →\to int, double →\to char, double →\to bool, int →\to char, int →\to bool, char →\to 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.

conversioni.cppcpp
#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 0

Lettura dell'uscita:

  1. char c = a;: 20000 in binario è 0100 1110 0010 0000; un char tiene solo gli 8 bit bassi, 0010 0000 =32=32. Quindi b vale 32, a != b e il programma stampa Ops!: nessun errore, solo un valore sbagliato. Se un char fosse "molto grande" varrebbe Wow.
  2. 2 diventa 2.0 in d1 + 2: conversione sicura.
  3. d / i con d = 2.5 e i = 2 converte i in double, dà 1.25; assegnato a un int si tronca a 1 (la parte decimale si perde). 5 / 2 fra interi è 2 e double q4 = 5 / 2 stampa 2: la divisione è già stata fatta fra interi.
  4. int(d) e static_cast<int>(d) convertono entrambi senza controllo; il secondo si cerca facilmente nel codice e dice agli altri programmatori che il troncamento è intenzionale.
  5. char ok2 {48} va bene perché 48 sta in un char, ma stampa 0: il carattere con codice ASCII 48 è la cifra 0.

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:

cpp
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};          // ok

Le 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;
costanti.cppcpp
#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 = 12

constexpr 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.

espressioni.cppcpp
#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] = 4

L'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:

cpp
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 definito

Nemmeno = è 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.

input_tipi.cppcpp
#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
dichiarazioni.cppcpp
#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 = 7

Altri esempi, riga per riga:

cpp
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 ritorno

Una 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 →\to 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

In codice

cpp
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 constexpr
cpp
int 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 corpo

Come rispondere alle domande tipiche

Esercizi su questo argomento

Teoria collegata