L'objet foo
est une variable locale de type Foo*
. Cette variable est vraisemblablement allouée sur la pile pour le programme main
comme toute autre variable locale. Mais le valeur stocké dans foo
est un pointeur nul. Il ne pointe nulle part. Il n'y a pas d'instance de type Foo
représenté partout.
Pour appeler une fonction virtuelle, l'appelant doit savoir sur quel objet la fonction est appelée. En effet, c'est l'objet lui-même qui indique quelle fonction doit réellement être appelée. (Cela est fréquemment mis en œuvre en donnant à l'objet un pointeur vers une vtable, une liste de pointeurs de fonctions, et l'appelant sait simplement qu'il est censé appeler la première fonction de la liste, sans savoir à l'avance où pointe ce pointeur).
Mais pour appeler une fonction non virtuelle, l'appelant n'a pas besoin de savoir tout cela. Le compilateur sait exactement quelle fonction sera appelée, il peut donc générer un fichier CALL
instruction de code machine pour aller directement à la fonction désirée. Il transmet simplement un pointeur vers l'objet sur lequel la fonction a été appelée comme paramètre caché de la fonction. En d'autres termes, le compilateur traduit votre appel de fonction en ceci :
void Foo_say_hi(Foo* this);
Foo_say_hi(foo);
Maintenant, puisque l'implémentation de cette fonction ne fait jamais référence à aucun membre de l'objet pointé par sa fonction this
vous évitez effectivement le déréférencement d'un pointeur nul, car vous n'en déréférencez jamais.
Formellement, appeler tout même non virtuelle - sur un pointeur nul est un comportement non défini. L'un des résultats autorisés du comportement non défini est que votre code semble s'exécuter exactement comme vous l'aviez prévu. Vous ne devrait pas compter sur cela, bien que vous trouverez parfois des bibliothèques de votre fournisseur de compilateur qui faire s'y fier. Mais le fournisseur du compilateur a l'avantage de pouvoir ajouter une définition supplémentaire à ce qui serait autrement un comportement non défini. Ne le faites pas vous-même.