Aller au contenu

Classes, structs et encapsulation

Comprenez la différence entre struct et class, les spécificateurs d'accès, l'encapsulation, les fonctions membres const, les membres statiques et l'initialisation d'agrégat moderne.

Qu'est-ce qu'une classe ?

Une classe en C++ est un type défini par l'utilisateur qui regroupe des données (variables membres) et des opérations sur ces données (fonctions membres) en une seule unité. Ce regroupement s'appelle l'encapsulation — l'un des quatre piliers de la POO.

L'idée clé est qu'une classe définit un invariant : un ensemble de règles qui doivent toujours être vraies concernant son état interne. Par exemple, une classe Date pourrait exiger que le mois soit toujours entre 1 et 12. L'encapsulation vous permet d'appliquer ces invariants en contrôlant l'accès à l'état interne.

struct vs class

En C++, struct et class sont presque identiques. La seule différence est le spécificateur d'accès par défaut : struct est public par défaut, tandis que class est private par défaut. Convention : utilisez struct pour les agrégats de données simples, class quand vous avez des invariants à maintenir.

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;
}

Spécificateurs d'accès et le pointeur this

Le C++ fournit trois niveaux d'accès : public (tout le monde peut accéder), private (seulement la classe et ses amies), et protected (la classe, ses amies et les classes dérivées). Le pointeur this est un pointeur implicite vers l'objet courant, disponible dans chaque fonction membre non statique.

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;
}

Membres statiques et friend

Les membres statiques appartiennent à la classe, pas à une instance particulière. Utilisez-les pour un état partagé ou des fonctions utilitaires. Le mot-clé friend accorde à une fonction non membre ou à une autre classe l'accès aux membres privés — utilisez-le avec parcimonie, car il brise l'encapsulation.

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;
}

Initialisation d'agrégat et initialiseurs désignés (C++20)

Un agrégat est une classe ou struct sans constructeurs déclarés par l'utilisateur, sans données membres non statiques privées/protégées, sans fonctions virtuelles et sans classes de base virtuelles. C++20 ajoute les initialiseurs désignés, vous permettant de nommer les membres que vous initialisez.

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;
}
Bonne pratique

Rendez les données membres privées par défaut et n'exposez que ce qui est nécessaire via une interface publique minimale. Si une classe n'a pas d'invariant à maintenir (c'est juste un regroupement de données), utilisez un struct avec des membres publics à la place. N'ajoutez pas de getters et setters pour chaque champ — cela va à l'encontre de l'objectif de l'encapsulation. À la place, fournissez des opérations significatives comme deposit() et withdraw() plutôt que set_balance().

Piège

Si une fonction membre ne modifie pas l'état de l'objet, marquez-la const. Sans const, vous ne pouvez pas appeler la fonction sur un objet const ou via une référence const — ce qui arrive tout le temps quand on passe des objets par const&. Un const manquant sur un accesseur est une source courante d'erreurs de compilation dans les grandes bases de code.

Points clés
  • struct et class ne diffèrent que par l'accès par défaut : struct est public, class est private
  • L'encapsulation protège les invariants de classe — utilisez des données private avec une interface publique significative
  • Marquez les fonctions membres non modifiantes const pour qu'elles fonctionnent avec les références const
  • Utilisez inline static (C++17) pour les membres statiques définis dans les en-têtes
  • Les initialiseurs désignés C++20 rendent l'initialisation d'agrégat lisible et auto-documentée
  • friend devrait être utilisé avec parcimonie — typiquement pour les surcharges d'opérateurs qui ont besoin d'un accès privé

Quiz — Testez vos connaissances

(15 XP)

1. Quelle est la SEULE différence entre `struct` et `class` en C++ ?

2. Pourquoi devriez-vous marquer les fonctions membres accesseurs comme `const` ?

3. Que réalise `inline static int count_ = 0;` en C++17 ?