Zugreifen auf Klassenmitglieder auf einen NULL-Zeiger

Lesezeit: 7 Minuten

Benutzer-Avatar
Navaneeth KN

Ich habe mit C++ experimentiert und fand den folgenden Code sehr seltsam.

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

Ich weiß, dass der virtuelle Methodenaufruf abstürzt, weil er eine vtable-Suche erfordert und nur mit gültigen Objekten arbeiten kann.

Ich habe folgende Fragen

  1. Wie funktioniert die nicht virtuelle Methode say_hi Arbeit an einem NULL-Zeiger?
  2. Woher kommt das Objekt foo zugeteilt werden?

Irgendwelche Gedanken?

  • Siehe dies für das, was die Sprache darüber sagt. Beides sind undefinierte Verhaltensweisen.

    – GManNickG

    29. September 2010 um 21:26 Uhr

Das Objekt foo ist eine lokale Variable vom Typ Foo*. Diese Variable wird wahrscheinlich auf dem Stack für die zugewiesen main Funktion, genau wie jede andere lokale Variable. Aber die Wert darin gespeichert foo ist ein Nullzeiger. Es zeigt nirgendwo hin. Es gibt keine Instanz des Typs Foo überall vertreten.

Um eine virtuelle Funktion aufzurufen, muss der Aufrufer wissen, auf welchem ​​Objekt die Funktion aufgerufen wird. Das liegt daran, dass das Objekt selbst angibt, welche Funktion wirklich aufgerufen werden soll. (Das wird häufig implementiert, indem dem Objekt ein Zeiger auf eine vtable, eine Liste von Funktionszeigern, gegeben wird und der Aufrufer einfach weiß, dass er die erste Funktion auf der Liste aufrufen soll, ohne im Voraus zu wissen, wohin dieser Zeiger zeigt.)

Aber um eine nicht-virtuelle Funktion aufzurufen, muss der Aufrufer das alles nicht wissen. Der Compiler weiß genau, welche Funktion aufgerufen wird, also kann er eine generieren CALL Maschinencode-Anweisung, um direkt zur gewünschten Funktion zu gelangen. Es übergibt einfach einen Zeiger auf das Objekt, für das die Funktion aufgerufen wurde, als versteckten Parameter an die Funktion. Mit anderen Worten, der Compiler übersetzt Ihren Funktionsaufruf in Folgendes:

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

Da die Implementierung dieser Funktion nun niemals auf irgendwelche Mitglieder des Objekts verweist, auf die sie zeigt this -Argument weichen Sie effektiv der Kugel aus, einen Nullzeiger zu dereferenzieren, da Sie niemals einen dereferenzieren.

Formell anrufen irgendein Funktion – auch eine nicht virtuelle – auf einem Nullzeiger ist ein undefiniertes Verhalten. Eines der zulässigen Ergebnisse von undefiniertem Verhalten ist, dass Ihr Code anscheinend genau so ausgeführt wird, wie Sie es beabsichtigt haben. Du Sie sollten sich nicht darauf verlassen, obwohl Sie manchmal Bibliotheken von Ihrem Compiler-Anbieter finden werden tun darauf verlassen. Aber der Compiler-Anbieter hat den Vorteil, dass er weitere Definitionen zu ansonsten undefiniertem Verhalten hinzufügen kann. Mach es nicht selbst.

  • Es scheint auch Verwirrung darüber zu geben, dass der Funktionscode und die Objektdaten zwei verschiedene Dinge sind. Werfen Sie einen Blick auf diese stackoverflow.com/questions/1966920/…. Die Objektdaten sind in diesem Fall wegen des Nullzeigers nach der Initialisierung nicht verfügbar, aber der Code war immer an anderer Stelle im Speicher verfügbar.

    – Loki

    5. Januar 2012 um 19:59 Uhr


  • FYI dies ist abgeleitet von [C++11: 9.3.1/2]: “Wenn eine nicht statische Mitgliedsfunktion einer Klasse X wird für ein Objekt aufgerufen, das nicht vom Typ ist Xoder eines Typs, der von abgeleitet ist Xdas Verhalten ist undefiniert.” Eindeutig *foo ist nicht typ Foo (da es ihn nicht gibt).

    – Leichtigkeitsrennen im Orbit

    5. Januar 2012 um 20:17 Uhr

  • Tatsächlich ist es im Nachhinein direkter davon abgeleitet [C++11: 5.2.5/2]: “Der Ausdruck E1->E2 wird in die entsprechende Form umgewandelt (*(E1)).E2” und dann das offensichtliche UB der Dereferenzierung E1 wenn es kein gültiger Zeiger ist (inkl. [C++11: 3.8/2]).

    – Leichtigkeitsrennen im Orbit

    5. Januar 2012 um 20:36 Uhr


  • Können Sie mir sagen, wo Sie auf diese Frage verwiesen haben, @Lightness? Ich habe am letzten Tag über 20 Stimmen dafür bekommen und ich würde gerne sehen, warum es plötzlich so viel Aufmerksamkeit erregt.

    – Rob Kennedy

    6. Januar 2012 um 17:48 Uhr

  • @RobKennedy: Jemand hat gestern auf freenode##c++ und wahrscheinlich auch anderswo darauf verlinkt. Meine Kommentare haben es vielleicht auch kurz auf die Titelseiten gebracht.

    – Leichtigkeitsrennen im Orbit

    6. Januar 2012 um 17:53 Uhr

Benutzer-Avatar
Pontus-Gagge

Das say_hi() Die Member-Funktion wird normalerweise vom Compiler als implementiert

void say_hi(Foo *this);

Da Sie auf keine Mitglieder zugreifen, ist Ihr Aufruf erfolgreich (obwohl Sie gemäß dem Standard undefiniertes Verhalten eingeben).

Foo wird gar nicht zugeteilt.

  • Vielen Dank. Ob Foo wird nicht zugeteilt, wie kommt der Anruf zustande? ich bin etwas verwirrt..

    – Navaneeth KN

    21. März 2009 um 18:55 Uhr

  • Prozessor bzw. Assembly hat keine Ahnung von HLL-Details des Codes. Nicht virtuelle C++-Funktionen sind lediglich normale Funktionen mit einem Vertrag, dass sich der ‘this’-Zeiger an einer bestimmten Stelle befindet (Register oder Stack, abhängig vom Compiler). Solange Sie nicht auf den ‘this’-Zeiger zugreifen, ist alles in Ordnung.

    – Arul

    21. März 2009 um 19:01 Uhr

  • Ich hatte eine Situation, in der null point de reference nicht abstürzte, selbst wenn auf ein Datenfeld zugegriffen wurde. Ich denke, der Absturz sollte standardisiert werden.

    – Charlie

    14. September 2019 um 11:24 Uhr

  • Die Implementierungen variieren, aber das Erfordernis von Nullprüfungen überall würde Zeigerreferenzen für die zentralen C++-Entwurfsziele auf den meisten Plattformen zu teuer machen.

    – Pontus-Gagge

    16. September 2019 um 6:52 Uhr

Das Dereferenzieren eines NULL-Zeigers verursacht “undefiniertes Verhalten”. Dies bedeutet, dass alles passieren kann – Ihr Code kann sogar scheinbar richtig funktionieren. Sie dürfen sich jedoch nicht darauf verlassen – wenn Sie denselben Code auf einer anderen Plattform (oder möglicherweise sogar auf derselben Plattform) ausführen, wird er wahrscheinlich abstürzen.

In Ihrem Code gibt es kein Foo-Objekt, nur einen Zeiger, der mit dem Wert NULL initialisiert wird.

  • Vielen Dank. Was denkst du über die zweite Frage? Woher Foo wird zugeteilt?

    – Navaneeth KN

    21. März 2009 um 18:51 Uhr

  • foo ist kein Objekt, sondern ein Zeiger. Dieser Zeiger wird auf dem Stapel zugewiesen (wie jede Variable, die nicht als „statisch“ markiert oder mit „neu“ zugewiesen ist. Und er zeigt niemals auf ein gültiges Objekt.

    – jalf

    21. März 2009 um 18:53 Uhr

Benutzer-Avatar
bayda

Es ist ein undefiniertes Verhalten, aber die meisten Compiler generieren Anweisungen, die diese Situation korrekt handhaben, wenn Sie weder auf Member-Variablen noch auf die virtuelle Tabelle zugreifen.

Sehen wir uns die von Visual Studio generierte Disassemblierung an, um zu verstehen, was passiert:

   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  

Wie du sehen kannst Foo:say_hi wird als normale Funktion aufgerufen, aber mit this in dem ecx registrieren. Vereinfacht kann man davon ausgehen this wird als impliziter Parameter übergeben, der in Ihrem Beispiel nie verwendet wird.
Aber im zweiten Fall muss aufgrund der virtuellen Tabelle die Adresse der Funktion berechnet werden – dazu muss die Adresse von foo gültig ist und einen Absturz verursacht.

Es ist wichtig, das zu erkennen beide Aufrufe erzeugen ein undefiniertes Verhalten, und dieses Verhalten kann sich auf unerwartete Weise manifestieren. Auch wenn der Anruf erscheint zu funktionieren, kann es sein, dass es ein Minenfeld legt.

Betrachten Sie diese kleine Änderung an Ihrem Beispiel:

Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
    foo->say_virtual_hi(); // why does it still crash?

Seit dem ersten Anruf an foo aktiviert undefiniertes Verhalten, wenn foo null ist, kann der Compiler jetzt davon ausgehen foo ist nicht Null. Das macht die if (foo != 0) überflüssig, und der Compiler kann es optimieren! Sie könnten denken, dass dies eine sehr sinnlose Optimierung ist, aber die Compiler-Autoren sind sehr aggressiv geworden, und so etwas ist im tatsächlichen Code passiert.

Benutzer-Avatar
Pasi Savolainen

a) Es funktioniert, weil es nichts durch den impliziten “this”-Zeiger dereferenziert. Sobald Sie das tun, boom. Ich bin mir nicht 100% sicher, aber ich denke, dass Nullzeiger-Dereferenzierungen von RW durchgeführt werden, das die ersten 1 KB Speicherplatz schützt. Daher besteht eine geringe Chance, dass die Nullreferenzierung nicht abgefangen wird, wenn Sie sie nur über die 1-KB-Zeile hinaus dereferenzieren (dh eine Instanzvariable das würde sehr weit verteilt werden, wie:

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

dann wäre a->i möglicherweise nicht erfasst, wenn A null ist.

b) Nirgendwo haben Sie nur einen Zeiger deklariert, der auf dem Stack von main(): liegt.

Benutzer-Avatar
Uri

Der Aufruf von say_hi ist statisch gebunden. Der Computer führt also eigentlich nur einen Standardaufruf einer Funktion aus. Die Funktion verwendet keine Felder, daher gibt es kein Problem.

Der Aufruf von virtual_say_hi ist dynamisch gebunden, also geht der Prozessor zum virtuellen Tisch, und da es dort keinen virtuellen Tisch gibt, springt er irgendwo hin und bringt das Programm zum Absturz.

  • Das macht absolut Sinn. Vielen Dank

    – Navaneeth KN

    21. März 2009 um 18:58 Uhr

1013600cookie-checkZugreifen auf Klassenmitglieder auf einen NULL-Zeiger

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy