ProgrammationDéveloppeur Backend (C++)

Qu'est-ce que les fonctions virtuelles et comment fonctionne le mécanisme de liaison tardive en C++ ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Contexte de la question:

C++ a introduit le support de la programmation orientée objet, fondamental pour les langages modernes. Pour réaliser le polymorphisme, des fonctions virtuelles ont été utilisées. Cela permettait d'appeler la bonne implémentation d'une méthode au moment de l'exécution, et non seulement à la compilation, ce qui est critique pour l'architecture avec héritage.

Problème:

Une erreur courante est la confusion entre l'appel de méthodes statiques et dynamiques, des destructeurs virtuels oubliés, et un mauvais usage de l'héritage (par exemple, le slicing d'objet, l'appel de la version de base au lieu de l'override). On confond souvent quand le polymorphisme fonctionne réellement.

Solution:

Une fonction virtuelle est déclarée à l'aide du mot-clé virtual dans la classe de base et peut être redéfinie (override) dans la classe dérivée. Si on appelle la fonction via un pointeur ou une référence à la classe de base, la version de la classe dérivée sera exécutée.

Exemple de code:

struct Base { virtual void foo() { std::cout << "Base::foo "; } }; struct Derived : Base { void foo() override { std::cout << "Derived::foo "; } }; void call(Base& b) { b.foo(); } int main() { Derived d; call(d); // Affichera Derived::foo }

Caractéristiques clés :

  • Liaison tardive (dynamic dispatch) : le choix de la version de la méthode se fait lors de l'exécution
  • Usage via des pointeurs et des références à la classe de base
  • La bonne redéfinition des fonctions nécessite le mot-clé override (à partir de C++11, c'est possible)

Questions pièges.

Le polymorphisme fonctionne-t-il lors du passage d'un objet par valeur ?

Non. Le passage par valeur entraîne un "slicing" — seule la partie correspondant au type du paramètre est copiée (généralement la classe de base), le polymorphisme est désactivé.

Exemple de code :

void call(Base b) { b.foo(); } // toujours appel de Base::foo

Faut-il déclarer le destructeur virtuel dans la classe de base ?

Oui, si l'on prévoit la suppression d'objets dérivés via un pointeur sur la classe de base. Sinon, cela entraînera une fuite de mémoire ou une non-libération des ressources.

Exemple de code :

struct Base { virtual ~Base() {} };

Que se passera-t-il si on n'utilise pas le mot-clé override dans la classe dérivée ?

Si dans la classe dérivée on ne spécifie pas override, mais qu'on modifie par erreur la signature de la fonction (par exemple, en omettant const ou en se trompant dans les paramètres), la fonction ne redéfinit pas la fonction virtuelle, une nouvelle est créée, et le polymorphisme ne fonctionne pas comme prévu.

Erreurs typiques et anti-patterns

  • Non-déclaration du destructeur virtuel
  • Override mal implémenté (absence d'override, modification de la signature)
  • Utilisation de paramètres par valeur au lieu de références/pointeurs, entraînant un slicing

Exemple de la vie réelle

Cas négatif

Le programmeur n'a pas déclaré le destructeur de la classe de base comme virtuel ; la suppression d'un tableau d'objets via un pointeur de base a entraîné une fuite de mémoire.

Avantages :

  • Fonctionnement correct jusqu'à la suppression des objets

Inconvénients :

  • Fuites, ressources non libérées

Cas positif

Le destructeur virtuel a été déclaré ; seules des références/pointeurs au type de base ont été utilisés. Le polymorphisme a fonctionné correctement.

Avantages :

  • Sécurité de la libération de la mémoire
  • Code propre et évolutif

Inconvénients :

  • Légère augmentation de la mémoire et du temps d'exécution due à la table virtuelle