52 votes

Accès aux membres d'une classe sur un pointeur NULL

Je faisais des expériences avec C++ et j'ai trouvé le code ci-dessous très étrange.

class Foo{
public:
    virtual void say_virtual_hi(){
        std::cout << "Virtual Hi";
    }

    void say_hi()
    {
        std::cout << "Hi";
    }
};

int main(int argc, char** argv)
{
    Foo* foo = 0;
    foo->say_hi(); // works well
    foo->say_virtual_hi(); // will crash the app
    return 0;
}

Je sais que l'appel à une méthode virtuelle se plante parce qu'il nécessite une consultation de la table virtuelle et ne peut fonctionner qu'avec des objets valides.

J'ai les questions suivantes

  1. Comment la méthode non virtuelle *say_hi* fonctionne-t-elle sur un pointeur NULL ?
  2. Où l'objet foo est alloué ?

Des idées ?

86voto

Rob Kennedy Points 107381

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.

18voto

Pontus Gagge Points 12950

La fonction membre say_hi() est généralement implémentée par le compilateur sous la forme suivante

void say_hi(Foo *this);

Comme vous n'accédez à aucun membre, votre appel aboutit (même si vous entrez dans un comportement non défini selon la norme).

Foo n'est pas du tout alloué.

7voto

Le déréférencement d'un pointeur NULL provoque un "comportement indéfini", ce qui signifie que tout peut arriver - votre code peut même sembler fonctionner correctement. Vous ne devez cependant pas vous fier à cela - si vous exécutez le même code sur une autre plate-forme (ou même éventuellement sur la même plate-forme), il se plantera probablement.

Dans votre code il n'y a pas d'objet Foo, seulement un pointeur qui est initialisé avec la valeur NULL.

6voto

bayda Points 7454

C'est un comportement indéfini. Mais la plupart des compilateurs ont créé des instructions qui gèrent correctement cette situation si vous n'accédez pas aux variables membres et à la table virtuelle.

Voyons le désassemblage dans Visual Studio pour comprendre ce qui se passe.

   Foo* foo = 0;
004114BE  mov         dword ptr [foo],0 
    foo->say_hi(); // works well
004114C5  mov         ecx,dword ptr [foo] 
004114C8  call        Foo::say_hi (411091h) 
    foo->say_virtual_hi(); // will crash the app
004114CD  mov         eax,dword ptr [foo] 
004114D0  mov         edx,dword ptr [eax] 
004114D2  mov         esi,esp 
004114D4  mov         ecx,dword ptr [foo] 
004114D7  mov         eax,dword ptr [edx] 
004114D9  call        eax

comme vous pouvez le voir, Foo:say_hi est appelé comme une fonction habituelle mais avec ce dans le registre ecx. Pour simplifier, vous pouvez supposer que ce passé comme paramètre implicite que nous n'utilisons jamais dans votre exemple.
Mais dans le deuxième cas, nous calculons l'adresse de la fonction due à la table virtuelle - due à l'adresse de foo et obtient le noyau.

2voto

Pasi Savolainen Points 1489

A) Il fonctionne parce qu'il ne déréférence rien à travers le pointeur implicite "this". Dès que vous faites cela, boom. Je ne suis pas sûr à 100%, mais je pense que les déréférencements de pointeurs nuls sont effectués par RW en protégeant les premiers 1K de l'espace mémoire, donc il y a une petite chance que le déréférencement nul ne soit pas attrapé si vous ne déréférencez qu'au-delà de la ligne 1K (par exemple, une variable d'instance qui serait allouée très loin, comme :

 class A {
     char foo[2048];
     int i;
 }

alors a->i pourrait ne pas être attrapé lorsque A est nul.

b) Nulle part, vous n'avez déclaré un pointeur, qui est alloué sur la pile de main().

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X