LES CLASSES UTILES EN C++ SYSTÈME

Salut ce forum apprends les bases du c++

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

LES CLASSES UTILES EN C++ SYSTÈME

Post by Hydraxx »

LES CLASSES UTILES EN C++ SYSTÈME

Objectif du cours

Ce chapitre explique les classes sans tomber dans l'orienté objet inutile.

Pour du C++ système, une classe est surtout intéressante lorsqu'elle permet de :
  • regrouper un état et les opérations qui lui appartiennent ;
  • protéger des invariants ;
  • gérer automatiquement une ressource ;
  • rendre l'ownership clair ;
  • éviter les fuites et les erreurs de durée de vie ;
  • fournir une interface simple autour d'une API bas niveau.
Le but n'est pas de transformer tout le programme en hiérarchies de classes.

En C++ système, il est parfaitement normal de mélanger :
  • fonctions libres ;
  • structures ;
  • classes ;
  • fichiers .cpp/.h ;
  • API C ;
  • API système.
1. Procédural contre orienté objet

La programmation procédurale organise principalement le programme autour de fonctions et de données.

Exemple conceptuel :

Code: Select all

open_file()
read_file()
parse_header()
close_file()
Ce modèle est simple, direct et très adapté à de nombreux programmes système.

La programmation orientée objet regroupe plutôt :
  • des données ;
  • des opérations associées ;
  • des règles de validité ;
  • éventuellement une durée de vie.
Exemple conceptuel :

Code: Select all

class File
{
public:
    bool open();
    bool read();
    void close();
};
Les deux approches sont valides.

Le C++ permet de choisir selon le problème.

Règle pratique :

Si un problème se résout clairement avec quelques fonctions et une structure simple, inutile d'ajouter une classe complexe.

Si une donnée possède un état, une ressource ou des règles qu'il faut protéger, une classe devient beaucoup plus intéressante.

2. Qu'est-ce qu'une classe ?

Une classe définit un type.

Elle peut contenir :
  • des données membres ;
  • des fonctions membres ;
  • des constructeurs ;
  • un destructeur ;
  • des règles d'accès.
Exemple simple :

Code: Select all

class Counter
{
public:
    void increment()
    {
        ++value_;
    }

    int value() const
    {
        return value_;
    }

private:
    int value_ = 0;
};
Ici :
  • Counter est le type ;
  • value_ représente son état ;
  • increment() modifie cet état ;
  • value() permet de le lire ;
  • le membre privé ne peut pas être modifié directement depuis l'extérieur.
3. Classe contre objet

Une classe est une définition.

Un objet est une instance concrète de cette classe.

Code: Select all

Counter a;
Counter b;
Ici :
  • a possède son propre état ;
  • b possède son propre état ;
  • les deux objets ont le même type mais des données indépendantes.
Cela ressemble à la différence entre un type comme int et plusieurs variables int.

4. Propriétés et comportements

Une classe regroupe généralement :
  • des propriétés : les données ;
  • des comportements : les opérations.
Exemple :

Code: Select all

class Buffer
{
private:
    std::byte* data_;
    std::size_t size_;

public:
    std::size_t size() const;
    void clear();
};
Les données décrivent l'état du buffer.

Les méthodes décrivent ce qu'on peut faire avec lui.

Mais cela ne signifie pas qu'il faut toujours transformer une donnée en objet complexe.

Une structure simple peut parfois suffire :

Code: Select all

struct Header
{
    std::uint32_t magic;
    std::uint16_t machine;
    std::uint16_t sections;
};
Pour une simple structure de données, struct est souvent plus naturel.

5. Quand utiliser une classe

Une classe devient particulièrement utile dans quatre situations.

1. Une ressource doit être possédée

Exemples :
  • HANDLE ;
  • file descriptor ;
  • socket ;
  • mapping ;
  • allocation mémoire ;
  • mutex ;
  • thread ;
  • clé de registre.
2. Un invariant doit être protégé

Exemple :

Code: Select all

size_ <= capacity_
Si n'importe quel code peut modifier size_ ou capacity_ directement, l'invariant peut être cassé.

3. L'implémentation interne doit pouvoir changer

Le reste du programme doit connaître l'interface, pas forcément les détails.

4. Plusieurs opérations appartiennent naturellement au même état

Exemple : ouvrir, lire, écrire et fermer une même ressource.

6. Quand ne pas utiliser une classe

Une classe est inutile si elle ajoute uniquement du bruit.

Par exemple, pour une petite opération sans état :

Code: Select all

std::uint32_t checksum(const std::byte* data, std::size_t size);
une fonction libre est souvent parfaite.

Créer :

Code: Select all

class ChecksumCalculator
{
public:
    std::uint32_t calculate(...);
};
n'apporte pas forcément quelque chose.

Même chose pour un simple groupe de données sans invariant complexe :

Code: Select all

struct ProcessInfo
{
    std::uint32_t pid;
    std::uint64_t memory;
};
Le C++ professionnel n'est pas "tout en classes".

7. Encapsulation

L'encapsulation consiste à contrôler ce que le code extérieur peut modifier.

Les mots clés principaux sont :

Code: Select all

public
private
protected
Pour ton profil, les deux plus importants sont surtout public et private.

Exemple :

Code: Select all

class Buffer
{
public:
    std::size_t size() const
    {
        return size_;
    }

private:
    std::size_t size_ = 0;
};
Le code extérieur ne peut pas modifier directement size_.

Cela protège l'état interne.

8. Pourquoi private est utile en bas niveau

Dans du code système, une valeur incorrecte peut provoquer :
  • lecture hors limites ;
  • écriture hors limites ;
  • double libération ;
  • handle invalide ;
  • use-after-free ;
  • corruption mémoire.
Si une classe garantit :

Code: Select all

size_ <= capacity_
et que tous les changements passent par des fonctions contrôlées, l'invariant est beaucoup plus simple à maintenir.

9. L'usage le plus important : RAII

Pour du C++ système, c'est probablement la partie la plus importante de ce chapitre.

RAII signifie que la durée de vie d'une ressource est liée à la durée de vie d'un objet.

L'idée est :

Code: Select all

construction
    |
    v
acquisition de ressource
    |
    v
utilisation
    |
    v
destruction objet
    |
    v
libération automatique
Cela évite d'oublier les opérations de nettoyage.

10. Exemple conceptuel avec un HANDLE

Sans RAII :

Code: Select all

HANDLE h = ...;

if (erreur1)
{
    CloseHandle(h);
    return;
}

if (erreur2)
{
    CloseHandle(h);
    return;
}

CloseHandle(h);
Plus le nombre de chemins d'erreur augmente, plus il est facile d'oublier un nettoyage.

Avec une classe RAII :

Code: Select all

class UniqueHandle
{
public:
    explicit UniqueHandle(HANDLE handle)
        : handle_(handle)
    {
    }

    ~UniqueHandle()
    {
        if (handle_ != nullptr &&
            handle_ != INVALID_HANDLE_VALUE)
        {
            CloseHandle(handle_);
        }
    }

private:
    HANDLE handle_;
};
Quand l'objet sort de sa portée, le destructeur libère automatiquement la ressource.

C'est ici qu'une classe apporte une vraie valeur bas niveau.

11. RAII avec les ressources Linux

Le même principe s'applique aux file descriptors.

Code: Select all

class FileDescriptor
{
public:
    explicit FileDescriptor(int fd)
        : fd_(fd)
    {
    }

    ~FileDescriptor()
    {
        if (fd_ >= 0)
        {
            close(fd_);
        }
    }

private:
    int fd_;
};
Même logique pour :
  • socket() / close() ;
  • mmap() / munmap() ;
  • pthread_mutex_init() / pthread_mutex_destroy() ;
  • fopen() / fclose().
12. Ownership

Ownership signifie : qui est responsable de libérer une ressource ?

Une bonne classe peut rendre cette question évidente.

Une ressource doit avoir un propriétaire clair.

Sinon on obtient facilement :
  • fuite ;
  • double fermeture ;
  • double free ;
  • adresse utilisée après destruction.
13. Classes propriétaires et non propriétaires

Une classe propriétaire contrôle la durée de vie d'une ressource.

Exemple conceptuel :

Code: Select all

class Mapping
{
private:
    void* address_;
    std::size_t size_;
};
Elle peut être responsable de :

Code: Select all

munmap(address_, size_);
Une vue peut au contraire utiliser une mémoire sans la posséder :

Code: Select all

class BufferView
{
private:
    const std::byte* data_;
    std::size_t size_;
};
Cette distinction entre posséder et emprunter est fondamentale.

14. Relation has-a

Une relation has-a signifie qu'un objet contient ou utilise un autre objet.

Exemple :

Code: Select all

class ProcessManager
{
private:
    Logger logger_;
};
On peut dire :

Code: Select all

ProcessManager has a Logger
Cette relation est extrêmement fréquente et souvent préférable à l'héritage.

15. Composition

La composition est une relation has-a forte.

Code: Select all

class Server
{
private:
    Socket socket_;
};
Lorsque Server est détruit, socket_ est également détruit.

Si Socket utilise RAII, la socket native est automatiquement fermée.

La composition permet :
  • de découper les responsabilités ;
  • de réutiliser des composants ;
  • de contrôler les durées de vie ;
  • d'éviter de grosses hiérarchies ;
  • de rendre l'ownership naturel.
16. Agrégation et association

L'agrégation représente une relation plus faible : un composant utilise un autre objet sans contrôler nécessairement sa durée de vie.

Code: Select all

class Parser
{
public:
    Parser(Logger& logger)
        : logger_(logger)
    {
    }

private:
    Logger& logger_;
};
Parser utilise Logger, mais ne le détruit pas.

Une association peut être encore plus ponctuelle :

Code: Select all

class Worker
{
public:
    void execute(Job& job);
};
Le point important est de distinguer :

Code: Select all

possède
utilise
emprunte
référence temporairement
17. Relation is-a

Une relation is-a signifie qu'un type est une spécialisation d'un autre.

Exemple :

Code: Select all

NetworkDevice is a Device
C'est la logique de l'héritage.

Code: Select all

class Device
{
};

class NetworkDevice : public Device
{
};
18. Héritage

L'héritage peut être utile pour :
  • partager un contrat ;
  • utiliser du polymorphisme ;
  • spécialiser un comportement.
Mais il augmente aussi le couplage.

Une classe dérivée dépend fortement de la classe de base.

Modifier la base peut affecter toutes les classes dérivées.

19. Ne pas utiliser l'héritage uniquement pour réutiliser du code

Si une classe veut simplement utiliser une fonctionnalité d'une autre classe, la composition est souvent meilleure.

Mauvais raisonnement :

Code: Select all

"Je veux réutiliser Logger,
donc ProcessManager hérite de Logger."
Un ProcessManager n'est pas un Logger.

Il doit plutôt contenir ou utiliser un Logger.

20. Composition ou héritage ?

Règle mentale :

Code: Select all

A est un B
    -> héritage éventuellement

A possède/utilise un B
    -> composition / agrégation / association
Dans le doute, la composition est souvent plus simple.

21. Polymorphisme

Le polymorphisme permet d'utiliser différentes implémentations derrière une interface commune.

Code: Select all

class Device
{
public:
    virtual ~Device() = default;
    virtual void start() = 0;
};

class DiskDevice : public Device
{
public:
    void start() override
    {
    }
};

class NetworkDevice : public Device
{
public:
    void start() override
    {
    }
};
On peut manipuler les deux via Device.

22. virtual et override

Une fonction virtuelle permet de sélectionner l'implémentation au moment de l'exécution.

Code: Select all

device->start();
Le mot clé override indique qu'une fonction doit réellement remplacer une fonction virtuelle de la classe de base.

Code: Select all

void start() override;
Il aide le compilateur à détecter les erreurs de signature.

23. Classe abstraite

Une classe contenant une fonction virtuelle pure peut servir d'interface.

Code: Select all

class Reader
{
public:
    virtual ~Reader() = default;

    virtual bool read(
        std::byte* buffer,
        std::size_t size
    ) = 0;
};
On peut ensuite avoir :
  • FileReader ;
  • MemoryReader ;
  • NetworkReader.
24. Coût du polymorphisme dynamique

Le polymorphisme dynamique implique généralement :
  • une table virtuelle ;
  • un pointeur virtuel dans les objets concernés ;
  • une indirection lors de l'appel ;
  • moins de possibilités d'optimisation dans certains cas.
Mais ce coût est souvent faible par rapport à :
  • I/O ;
  • réseau ;
  • accès disque ;
  • syscalls.
Il ne faut donc ni l'utiliser partout, ni le fuir systématiquement.

25. Hiérarchies de classes

Une hiérarchie représente plusieurs niveaux d'héritage.

Code: Select all

Device
   |
   +-- StorageDevice
   |      |
   |      +-- SSD
   |
   +-- NetworkDevice
Les hiérarchies trop profondes deviennent difficiles à suivre.

Problèmes possibles :
  • comportement hérité difficile à comprendre ;
  • fort couplage ;
  • modifications risquées ;
  • méthodes virtuelles réparties sur plusieurs classes ;
  • architecture rigide.
Pour du code système, garde les hiérarchies courtes.

26. Sur-classification

La sur-classification consiste à créer des classes pour tout.

Exemple inutile :

Code: Select all

class IntegerValue
class FileNameString
class ProcessIdObject
class CounterManager
class CounterManagerFactory
alors que de simples types ou fonctions suffisent.

Une abstraction doit résoudre un problème réel.

27. Classes trop générales

Une classe qui essaie de tout faire devient difficile à maintenir.

Code: Select all

class SystemManager
{
    // fichiers
    // mémoire
    // sockets
    // threads
    // processus
    // logs
    // configuration
};
Cette classe possède trop de responsabilités.

Mieux vaut séparer les responsabilités ou utiliser simplement des fonctions/modules lorsque cela suffit.

28. Héritage multiple

C++ permet :

Code: Select all

class C : public A, public B
{
};
Cela peut être utile dans certains designs, notamment avec de petites interfaces.

Mais cela peut créer :
  • ambiguïtés ;
  • hiérarchies difficiles à comprendre ;
  • problèmes de construction ;
  • diamond problem ;
  • couplage supplémentaire.
Pour du C++ système classique, ce n'est pas une technique prioritaire.

29. Mixins

Un mixin ajoute une capacité particulière à un type.

Conceptuellement :

Code: Select all

class LoggingCapability
{
};

class NetworkService
    : public LoggingCapability
{
};
C'est utile dans certaines architectures génériques, mais secondaire ici.

Composition, fonctions et RAII couvrent déjà énormément de besoins.

30. Classes et fichiers .cpp/.h

Une classe et un fichier ne répondent pas au même problème.

Le fichier organise physiquement le code :

Code: Select all

memory.h
memory.cpp
network.h
network.cpp
Une classe définit une abstraction et éventuellement un état.

On peut parfaitement avoir :

Code: Select all

memory.cpp
    allocate_region()
    protect_region()
    free_region()
sans classe.

Ou une classe Mapping si une ressource et sa durée de vie doivent être encapsulées.

31. Quand préférer fonctions + struct

Cette approche est excellente quand :
  • les données sont simples ;
  • il n'y a pas de ressource à posséder ;
  • les invariants sont simples ;
  • les opérations n'ont pas besoin d'état persistant ;
  • une API procédurale est plus naturelle.
Exemple :

Code: Select all

struct ModuleInfo
{
    void* base;
    std::size_t size;
};

bool inspect_module(
    const ModuleInfo& info
);
C'est parfaitement professionnel.

32. Quand préférer une classe

Une classe devient intéressante lorsque tu veux écrire quelque chose comme :

Code: Select all

Mapping mapping;
Socket socket;
Thread thread;
UniqueHandle handle;
MappedFile file;
et que chaque objet possède automatiquement la ressource correspondante.

Dans ce cas, la classe apporte :
  • ownership ;
  • RAII ;
  • invariants ;
  • interface ;
  • durée de vie claire.
33. Exemple : wrapper de HANDLE

Code: Select all

class UniqueHandle
{
public:
    UniqueHandle() = default;

    explicit UniqueHandle(HANDLE handle)
        : handle_(handle)
    {
    }

    ~UniqueHandle()
    {
        reset();
    }

    HANDLE get() const
    {
        return handle_;
    }

    void reset()
    {
        if (handle_ != nullptr &&
            handle_ != INVALID_HANDLE_VALUE)
        {
            CloseHandle(handle_);
            handle_ = nullptr;
        }
    }

private:
    HANDLE handle_ = nullptr;
};
Le but est simple :

Code: Select all

objet vivant
    =
handle possédé

objet détruit
    =
handle fermé
34. Exemple : wrapper de mapping

Code: Select all

class Mapping
{
public:
    Mapping(void* address, std::size_t size)
        : address_(address),
          size_(size)
    {
    }

    ~Mapping()
    {
        if (address_ != nullptr)
        {
            munmap(address_, size_);
        }
    }

private:
    void* address_ = nullptr;
    std::size_t size_ = 0;
};
Le programme n'a plus besoin de penser à appeler munmap() dans chaque chemin de retour.

35. Exception safety grâce à RAII

RAII reste utile lorsque l'exécution quitte une fonction prématurément :
  • return ;
  • exception ;
  • branche d'erreur.
Les objets locaux sont détruits automatiquement.

Donc leurs destructeurs libèrent les ressources.

C'est l'une des raisons pour lesquelles RAII est central en C++.

36. Destructeur virtuel

Si une classe de base doit être détruite polymorphiquement, son destructeur doit généralement être virtuel.

Code: Select all

class Device
{
public:
    virtual ~Device() = default;
};
Sinon :

Code: Select all

Device* device = new NetworkDevice;
delete device;
peut ne pas détruire correctement la partie dérivée.

Retenir :

Classe polymorphique détruite via la base -> destructeur virtuel.

37. Ne pas cacher les coûts importants

Une abstraction ne doit pas faire croire qu'une opération coûte presque rien si elle réalise en réalité :
  • une allocation ;
  • une copie importante ;
  • un syscall ;
  • une attente ;
  • une opération réseau.
Une bonne classe système simplifie l'utilisation sans masquer les propriétés essentielles.

38. Ne pas modéliser le monde réel pour le plaisir

Les livres d'OOP utilisent souvent Animal, Monkey, Car ou Employee pour expliquer les relations.

Cela aide à comprendre la théorie, mais un programme système n'a pas besoin de transformer chaque concept en objet métier.

Des abstractions plus naturelles pour le système sont :

Code: Select all

UniqueHandle
MappedRegion
Socket
FileDescriptor
ThreadPool
ProcessSnapshot
ModuleView
39. Résumé pratique pour le C++ système

Les classes sont vraiment utiles lorsque tu veux :
  • posséder une ressource ;
  • libérer cette ressource automatiquement ;
  • protéger un état ;
  • imposer un invariant ;
  • regrouper des opérations autour d'un même état ;
  • fournir une interface stable.
Sinon :
  • fonction libre ;
  • struct ;
  • namespace ;
  • fichier .cpp/.h ;
peuvent être plus simples.

40. Les règles à retenir absolument

Règle 1

Ne fais pas de classe uniquement parce que tu programmes en C++.

Règle 2

Pour une ressource, pense RAII.

Règle 3

Un propriétaire doit être clairement identifiable.

Règle 4

Utilise private pour protéger les invariants.

Règle 5

Composition avant héritage dans beaucoup de situations.

Règle 6

Utilise l'héritage seulement lorsqu'il existe une vraie relation is-a.

Règle 7

Utilise override lorsqu'une méthode remplace une méthode virtuelle.

Règle 8

Une classe polymorphique supprimée via sa base doit généralement avoir un destructeur virtuel.

Règle 9

Évite les hiérarchies profondes.

Règle 10

Une abstraction doit réduire la complexité, pas l'augmenter.

Résumé mental

Code: Select all

Fonction
    -> opération simple sans état complexe

struct
    -> données simples

classe
    -> état + invariants
       ou ressource + durée de vie

composition
    -> "possède/utilise un"

héritage
    -> "est un"

RAII
    -> acquisition dans l'objet
       destruction automatique

polymorphisme
    -> plusieurs implémentations
       derrière un même contrat
Pour du C++ bas niveau, le point le plus important est surtout :

Code: Select all

RESSOURCE NATIVE
      |
      v
OBJET RAII
      |
      v
OWNERSHIP CLAIR
      |
      v
DESTRUCTION AUTOMATIQUE
C'est là que les classes deviennent réellement puissantes pour écrire du code système plus sûr sans perdre le contrôle du bas niveau.

Return to “Bases du C++”

Who is online

Users browsing this forum: No registered users and 1 guest