Vai al contenuto

Classi, Struct e Incapsulamento

Comprendi la differenza tra struct e class, gli specificatori di accesso, l'incapsulamento, le funzioni membro const, i membri statici e l'inizializzazione moderna degli aggregati.

Cos'è una Classe?

Una classe in C++ è un tipo definito dall'utente che raggruppa dati (variabili membro) e operazioni su quei dati (funzioni membro) in un'unica unità. Questo raggruppamento si chiama incapsulamento — uno dei quattro pilastri dell'OOP.

L'idea chiave è che una classe definisce un invariante: un insieme di regole che devono essere sempre vere riguardo al suo stato interno. Per esempio, una classe Date potrebbe richiedere che il mese sia sempre compreso tra 1 e 12. L'incapsulamento ti permette di far rispettare questi invarianti controllando l'accesso allo stato interno.

struct vs class

In C++, struct e class sono quasi identici. L'unica differenza è lo specificatore di accesso predefinito: struct usa public come impostazione predefinita, mentre class usa private. Convenzione: usa struct per semplici aggregati di dati, class quando hai invarianti da mantenere.

struct_vs_class.cpp
#include <iostream>
#include <string>

// struct: members are public by default
struct Point {
    double x;  // public
    double y;  // public
};

// class: members are private by default
class BankAccount {
    std::string owner;   // private
    double balance;      // private

public:
    BankAccount(const std::string& name, double initial)
        : owner{name}, balance{initial} {}

    double get_balance() const { return balance; }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
};

int main() {
    Point p{3.0, 4.0};  // aggregate initialization
    std::cout << p.x << ", " << p.y << '\n';

    BankAccount acct{"Alice", 1000.0};
    acct.deposit(500.0);
    std::cout << acct.get_balance() << '\n';  // 1500
    // acct.balance = -999;  // ERROR: private member

    return 0;
}

Specificatori di Accesso e il Puntatore this

Il C++ offre tre livelli di accesso: public (chiunque può accedere), private (solo la classe e i suoi friend) e protected (la classe, i suoi friend e le classi derivate). Il puntatore this è un puntatore implicito all'oggetto corrente, disponibile all'interno di ogni funzione membro non statica.

access_specifiers.cpp
#include <iostream>
#include <string>

class Employee {
private:
    std::string name_;
    int id_;

protected:
    double salary_;  // accessible to derived classes

public:
    Employee(const std::string& name, int id, double salary)
        : name_{name}, id_{id}, salary_{salary} {}

    // const member function: promises not to modify the object
    const std::string& get_name() const { return name_; }
    int get_id() const { return id_; }

    // 'this' pointer usage — return *this for method chaining
    Employee& set_name(const std::string& name) {
        this->name_ = name;  // 'this->' disambiguates
        return *this;
    }

    Employee& set_id(int id) {
        this->id_ = id;
        return *this;
    }

    void print() const {
        std::cout << "Employee{" << name_ << ", #" << id_
                  << ", $" << salary_ << "}\n";
    }
};

int main() {
    Employee e{"Bob", 42, 75000.0};
    e.set_name("Robert").set_id(43);  // method chaining via *this
    e.print();  // Employee{Robert, #43, $75000}
    return 0;
}

Membri Statici e friend

I membri statici appartengono alla classe, non a una singola istanza. Usali per stato condiviso o funzioni di utilità. La parola chiave friend concede a una funzione non membro o a un'altra classe l'accesso ai membri privati — usala con parsimonia, perché rompe l'incapsulamento.

static_friend.cpp
#include <iostream>

class Widget {
    int value_;

    // C++17: inline static — define in header, no .cpp needed
    inline static int count_ = 0;

public:
    explicit Widget(int v) : value_{v} { ++count_; }
    ~Widget() { --count_; }

    static int get_count() { return count_; }

    // friend function can access private members
    friend std::ostream& operator<<(std::ostream& os, const Widget& w);
};

std::ostream& operator<<(std::ostream& os, const Widget& w) {
    return os << "Widget(" << w.value_ << ')';  // accesses private value_
}

int main() {
    Widget a{10}, b{20};
    std::cout << Widget::get_count() << '\n';  // 2
    std::cout << a << '\n';  // Widget(10)
    {
        Widget c{30};
        std::cout << Widget::get_count() << '\n';  // 3
    }
    std::cout << Widget::get_count() << '\n';  // 2
    return 0;
}

Inizializzazione di Aggregati e Inizializzatori Designati (C++20)

Un aggregato è una classe o una struct senza costruttori dichiarati dall'utente, senza membri dati non statici privati o protetti, senza funzioni virtuali e senza classi base virtuali. Il C++20 aggiunge gli inizializzatori designati, che ti permettono di indicare per nome quali membri stai inizializzando.

designated_init.cpp
#include <iostream>
#include <string>

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool use_tls = false;
    int max_connections = 100;
};

int main() {
    // Traditional aggregate initialization
    Config c1{"example.com", 443, true, 200};

    // C++20 designated initializers — much more readable
    Config c2{
        .host = "api.example.com",
        .port = 443,
        .use_tls = true
        // .max_connections uses default value 100
    };

    std::cout << c2.host << ':' << c2.port << '\n';
    std::cout << "TLS: " << std::boolalpha << c2.use_tls << '\n';
    std::cout << "Max: " << c2.max_connections << '\n';  // 100
    return 0;
}
Buona pratica

Rendi i membri dati privati per impostazione predefinita ed esponi solo ciò che serve attraverso un'interfaccia pubblica minima. Se una classe non ha invarianti da mantenere (è solo un contenitore di dati), usa invece una struct con membri pubblici. Non aggiungere getter e setter per ogni campo — così vanifichi lo scopo dell'incapsulamento. Fornisci invece operazioni significative come deposit() e withdraw() anziché set_balance().

Insidia

Se una funzione membro non modifica lo stato dell'oggetto, marcala come const. Senza const non puoi chiamare la funzione su un oggetto const o attraverso un riferimento const — cosa che succede continuamente quando passi gli oggetti per const&. Un const mancante su un accessore è una fonte comune di errori di compilazione nelle basi di codice più grandi.

Concetti chiave
  • struct e class differiscono solo per l'accesso predefinito: struct è public, class è private
  • L'incapsulamento protegge gli invarianti di classe — usa dati private con un'interfaccia pubblica significativa
  • Marca come const le funzioni membro che non modificano l'oggetto, così funzionano con i riferimenti const
  • Usa inline static (C++17) per i membri statici definiti negli header
  • Gli inizializzatori designati del C++20 rendono l'inizializzazione degli aggregati leggibile e auto-documentante
  • friend va usato con parsimonia — tipicamente per gli overload di operatori che necessitano dell'accesso privato

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Qual è l'UNICA differenza tra `struct` e `class` in C++?

2. Perché dovresti marcare come `const` le funzioni membro di accesso?

3. Cosa ottieni con `inline static int count_ = 0;` in C++17?