Vai al contenuto

Tipi, Valori e il Sistema di Tipi

Esplora i tipi fondamentali del C++, gli interi a larghezza fissa, le forme di inizializzazione, la deduzione con auto e il sistema const che garantisce la correttezza a tempo di compilazione.

Il Sistema di Tipi: La Tua Prima Linea di Difesa

Il C++ è un linguaggio a tipizzazione statica — ogni espressione ha un tipo noto a tempo di compilazione. Il sistema di tipi non è solo contabilità; è il tuo strumento più potente per catturare bug prima che il codice venga eseguito. Un programma C++ ben tipizzato porta i suoi invarianti nel sistema di tipi stesso, rendendo intere classi di errori impossibili.

Il tipo di una variabile determina tre cose: quanta memoria occupa, quali valori può rappresentare, e quali operazioni sono valide su di essa. Definire correttamente i tipi è il fondamento di un codice C++ corretto.

Tipi Fondamentali

Il C++ fornisce un insieme di tipi integrati che si mappano direttamente sulle capacità dell'hardware. Le loro dimensioni esatte variano per piattaforma, ma lo standard garantisce intervalli minimi:

types.cppc++23
#include <iostream>
#include <cstdint>   // Fixed-width integer types
#include <limits>    // std::numeric_limits

int main() {
    // Boolean
    bool is_valid = true;           // true or false, typically 1 byte

    // Character types
    char letter = 'A';             // At least 8 bits, holds ASCII
    char8_t  u8ch  = u8'A';       // C++20: exactly 8 bits, UTF-8
    char16_t u16ch = u'\u00E9';   // At least 16 bits, UTF-16
    char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32

    // Integer types (sizes are minimums, typical x86-64 shown)
    short       s = 32000;         // At least 16 bits (16)
    int         i = 2'000'000;     // At least 16 bits (32) — digit separator C++14
    long        l = 100'000L;      // At least 32 bits (64 on Linux, 32 on Windows)
    long long  ll = 9'000'000'000'000'000'000LL;  // At least 64 bits

    // Unsigned variants
    unsigned int ui = 42u;         // Cannot be negative

    // Floating-point types
    float       f = 3.14f;         // ~7 decimal digits precision
    double      d = 3.141592653589793; // ~15 decimal digits precision
    long double ld = 3.14159265358979323846L;

    // Fixed-width types from <cstdint> — use these when size matters
    std::int32_t  exact = 42;      // Exactly 32 bits, guaranteed
    std::uint64_t big   = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
    std::int_fast32_t fast = 42;   // At least 32 bits, fastest available
    std::size_t index = 0;         // Unsigned, suitable for sizes and indices

    std::cout << "int: " << sizeof(int) << " bytes, max = "
              << std::numeric_limits<int>::max() << '\n';
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Inizializzazione: Il Modo Corretto

Il C++ ha sviluppato diverse sintassi di inizializzazione. L'inizializzazione uniforme (con parentesi graffe) {} è preferita nel C++ moderno perché impedisce le conversioni con restringimento:

initialization.cppc++23
#include <iostream>
#include <string>

int main() {
    // Direct initialization
    int a(42);              // OK but can be confused with function declaration

    // Copy initialization
    int b = 42;             // OK, familiar from C

    // Uniform (brace) initialization — preferred in modern C++
    int c{42};              // Direct-list-initialization
    int d = {42};           // Copy-list-initialization

    // Brace initialization PREVENTS narrowing conversions
    // double to int loses precision — the compiler will ERROR:
    // int narrow{3.14};    // ERROR: narrowing conversion
    int ok = 3.14;          // Compiles! Silently truncates to 3 (dangerous)

    // Value initialization (zero for built-in types)
    int zero{};             // Guaranteed to be 0
    double dzero{};         // Guaranteed to be 0.0
    bool bfalse{};          // Guaranteed to be false

    // auto deduction
    auto x = 42;            // int
    auto y = 3.14;          // double
    auto z = 'A';           // char
    auto s = std::string{"hello"};  // std::string (not const char*!)

    // decltype — queries the type of an expression
    decltype(x) another = 100;  // same type as x → int
    decltype(auto) ref = (x);   // int& because (x) is an lvalue expression

    std::cout << "zero-initialized int: " << zero << '\n';
    std::cout << "auto-deduced string: " << s << '\n';
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Buona pratica

In C, il puntatore null è rappresentato dalla macro NULL, che tipicamente si espande in 0 o (void*)0. Questo causa ambiguità in C++ perché 0 è anche un int valido. Considera una funzione con overload sia per int che per un tipo puntatore — passare NULL (che è 0) chiamerebbe l'overload int, non quello puntatore.

Il C++11 ha introdotto nullptr, una parola chiave di tipo std::nullptr_t che è implicitamente convertibile a qualsiasi tipo puntatore ma non a interi. Usa sempre nullptr invece di NULL o 0 per i puntatori null.

Analogamente, preferisci '\0' (il carattere null) quando intendi il carattere con valore zero, piuttosto che 0, per comunicare chiaramente l'intento.

  • Usa sempre nullptr invece di NULL o 0 per i puntatori null
  • nullptr ha il suo tipo (std::nullptr_t) ed evita l'ambiguità negli overload
  • NULL è una macro C che si espande in 0 — è un intero, non un puntatore
  • Usa '\0' per il carattere null per distinguerlo dall'intero 0

const e i Qualificatori di Tipo

Il qualificatore const indica al compilatore (e ai lettori umani) che un valore non deve essere modificato dopo l'inizializzazione. È uno degli strumenti più importanti in C++ per scrivere codice sicuro e auto-documentante.

const si legge da destra a sinistra: const int* è un puntatore a un int const (l'int non può essere cambiato, il puntatore sì). int* const è un puntatore const a int (il puntatore non può essere cambiato, l'int sì). const int* const è un puntatore const a un int const.

constexpr (C++11) va oltre: significa che il valore deve essere calcolabile a tempo di compilazione. Questo consente al compilatore di valutare le espressioni durante la compilazione, eliminando completamente il costo a runtime. Usa constexpr per le costanti veramente note a tempo di compilazione.

consteval (C++20) è ancora più rigoroso: contrassegna una funzione che deve essere valutata a tempo di compilazione — chiamarla a runtime è un errore di compilazione.

Il qualificatore volatile indica al compilatore che il valore di una variabile potrebbe cambiare al di fuori del controllo del programma (es. registri hardware, I/O mappato in memoria). Impedisce certe ottimizzazioni. Non usare volatile per il threading — usa std::atomic invece.

Insidia

Uno dei bug più insidiosi nel C e nel C++ vecchio stile deriva dalle conversioni con restringimento — conversioni implicite che perdono informazioni. Per esempio, assegnare un double a un int tronca silenziosamente la parte frazionaria. Assegnare un int grande a uno short causa silenziosamente un overflow.

L'inizializzazione con parentesi graffe {} impedisce le conversioni con restringimento a tempo di compilazione. Questa è una delle ragioni più forti per preferirla all'inizializzazione con =. Se scrivi int x{3.14};, il compilatore emetterà un errore. Con int x = 3.14;, tronca silenziosamente a 3.

Analogamente, mescolare tipi con segno e senza segno nell'aritmetica causa conversioni implicite che possono produrre risultati sorprendenti. Un int negativo confrontato con un unsigned int verrà prima convertito in unsigned, diventando potenzialmente un numero positivo molto grande.

  • L'inizializzazione con parentesi graffe {} cattura le conversioni con restringimento a tempo di compilazione
  • L'inizializzazione con assegnamento (=) permette silenziosamente il restringimento — i dati vengono persi senza warning
  • Mescolare tipi con segno e senza segno causa la conversione implicita a unsigned
  • Un int negativo convertito a unsigned diventa un numero positivo molto grande

Alias di Tipo

Gli alias di tipo migliorano la leggibilità e rendono più facile cambiare i tipi in seguito. Preferisci using rispetto a typedef nel C++ moderno:

aliases.cppc++23
#include <cstdint>
#include <vector>
#include <string>

// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;

// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;

// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;

int main() {
    Byte b{255};
    StudentId sid{12345};
    Names names{"Alice", "Bob", "Charlie"};
    Vec<double> values{1.0, 2.0, 3.0};
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Concetti chiave
  • Ogni espressione C++ ha un tipo noto a tempo di compilazione — il tipo determina la dimensione, i valori validi e le operazioni valide
  • Usa i tipi a larghezza fissa da (int32_t, uint64_t) quando la dimensione esatta conta
  • Preferisci l'inizializzazione con parentesi graffe {} — impedisce le pericolose conversioni con restringimento
  • Usa nullptr (non NULL o 0) per i puntatori null per evitare ambiguità negli overload
  • auto deduce il tipo dall'inizializzatore; decltype interroga il tipo di un'espressione senza valutarla
  • Usa constexpr per le costanti a tempo di compilazione e 'using' per gli alias di tipo

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Qual è il vantaggio dell'inizializzazione con parentesi graffe `int x{42};` rispetto all'inizializzazione con assegnamento `int x = 42;`?

2. Perché dovresti usare `nullptr` invece di `NULL` in C++?

3. A quale tipo viene dedotto `x` con `auto x = 42;`?