Betrachten Sie den folgenden Code:
#include <iostream>
struct foo
{
// (a):
void bar() { std::cout << "gman was here" << std::endl; }
// (b):
void baz() { x = 5; }
int x;
};
int main()
{
foo* f = 0;
f->bar(); // (a)
f->baz(); // (b)
}
Wir erwarten (b)
zum Absturz bringen, weil es kein entsprechendes Mitglied gibt x
für den Nullzeiger. In der Praxis, (a)
stürzt nicht ab, weil die this
Zeiger wird nie verwendet.
Weil (b)
dereferenziert die this
Zeiger ((*this).x = 5;
), und this
null ist, tritt das Programm in undefiniertes Verhalten ein, da die Dereferenzierung von null immer als undefiniertes Verhalten bezeichnet wird.
Tut (a)
zu undefiniertem Verhalten führen? Was ist, wenn beide Funktionen (und x
) sind statisch?
Beide (a)
und (b)
zu undefiniertem Verhalten führen. Es ist immer ein undefiniertes Verhalten, eine Memberfunktion über einen Nullzeiger aufzurufen. Wenn die Funktion statisch ist, ist sie technisch ebenfalls undefiniert, aber es gibt einige Streitigkeiten.
Als Erstes müssen Sie verstehen, warum es ein undefiniertes Verhalten ist, einen Nullzeiger zu dereferenzieren. In C++03 gibt es hier tatsächlich ein wenig Mehrdeutigkeit.
Obwohl “Dereferenzieren eines Nullzeigers führt zu undefiniertem Verhalten” sowohl in §1.9/4 als auch in §8.3.2/4 in den Anmerkungen erwähnt wird, wird es nie ausdrücklich erwähnt. (Hinweise sind nicht normativ.)
Man kann jedoch versuchen, es aus §3.10/2 abzuleiten:
Ein lvalue bezieht sich auf ein Objekt oder eine Funktion.
Beim Dereferenzieren ist das Ergebnis ein Lvalue. Ein Nullzeiger nicht beziehen sich auf ein Objekt, daher haben wir ein undefiniertes Verhalten, wenn wir den lvalue verwenden. Das Problem ist, dass der vorherige Satz nie erwähnt wird. Was bedeutet es also, den lvalue zu “verwenden”? Generieren Sie es überhaupt oder verwenden Sie es im formelleren Sinne der Durchführung einer lvalue-to-rvalue-Konvertierung?
Unabhängig davon kann es definitiv nicht in einen rvalue konvertiert werden (§4.1/1):
Wenn das Objekt, auf das sich der lvalue bezieht, kein Objekt des Typs T und kein Objekt eines von T abgeleiteten Typs ist, oder wenn das Objekt nicht initialisiert ist, hat ein Programm, das diese Konvertierung erfordert, ein undefiniertes Verhalten.
Hier ist es definitiv undefiniertes Verhalten.
Die Mehrdeutigkeit ergibt sich daraus, ob es sich um ein undefiniertes Verhalten gegenüber Ehrerbietung handelt oder nicht aber nicht verwenden den Wert von einem ungültigen Zeiger (d. h. einen lvalue abrufen, aber nicht in einen rvalue konvertieren). Wenn nicht, dann int *i = 0; *i; &(*i);
ist wohldefiniert. Das ist ein aktives Thema.
Wir haben also eine strenge Ansicht “Nullzeiger dereferenzieren, undefiniertes Verhalten erhalten” und eine schwache Ansicht “Nullzeiger dereferenzieren, undefiniertes Verhalten erhalten”.
Jetzt betrachten wir die Frage.
Jawohl, (a)
führt zu undefiniertem Verhalten. In der Tat, wenn this
ist dann null unabhängig vom Inhalt der Funktion das Ergebnis ist undefiniert.
Daraus folgt aus §5.2.5/3:
Wenn E1
vom Typ „Zeiger auf Klasse X“ hat, dann ist der Ausdruck E1->E2
wird in die entsprechende Form umgewandelt (*(E1)).E2;
*(E1)
führt bei strenger Auslegung zu undefiniertem Verhalten, und .E2
wandelt es in einen rvalue um, wodurch es für die schwache Interpretation zu einem undefinierten Verhalten wird.
Es folgt auch, dass es sich um ein undefiniertes Verhalten direkt aus (§9.3.1/1) handelt:
Wenn eine nichtstatische Elementfunktion einer Klasse X für ein Objekt aufgerufen wird, das nicht vom Typ X ist oder einen von X abgeleiteten Typ hat, ist das Verhalten undefiniert.
Bei statischen Funktionen macht die strikte versus schwache Interpretation den Unterschied. Genau genommen ist es undefiniert:
Auf ein statisches Mitglied kann unter Verwendung der Zugriffssyntax für Klassenmitglieder verwiesen werden, wobei in diesem Fall der Objektausdruck ausgewertet wird.
Das heißt, es wird so ausgewertet, als wäre es nicht statisch, und wir dereferenzieren erneut einen Nullzeiger mit (*(E1)).E2
.
Allerdings, weil E1
wird nicht in einem statischen Elementfunktionsaufruf verwendet, wenn wir die schwache Interpretation verwenden, ist der Aufruf wohldefiniert. *(E1)
ergibt einen lvalue, die statische Funktion wird aufgelöst, *(E1)
verworfen und die Funktion aufgerufen. Es gibt keine lvalue-zu-rvalue-Konvertierung, also gibt es kein undefiniertes Verhalten.
In C++0x bleibt die Mehrdeutigkeit ab n3126 bestehen. Seien Sie vorerst sicher: Verwenden Sie die strenge Interpretation.
Offensichtlich undefiniert bedeutet, dass es ist nicht definiert, aber manchmal kann es vorhersehbar sein. Die Informationen, die ich gleich bereitstellen werde, sollten sich niemals auf funktionierenden Code verlassen, da dies sicherlich nicht garantiert ist, aber beim Debuggen nützlich sein kann.
Sie könnten denken, dass das Aufrufen einer Funktion für einen Objektzeiger den Zeiger dereferenziert und UB verursacht. Wenn die Funktion nicht virtuell ist, hat der Compiler sie in der Praxis in einen einfachen Funktionsaufruf umgewandelt, der den Zeiger als ersten Parameter übergibt Das, Umgehen der Dereferenzierung und Erstellen einer Zeitbombe für die aufgerufene Member-Funktion. Wenn die Member-Funktion auf keine Member-Variablen oder virtuellen Funktionen verweist, kann sie tatsächlich ohne Fehler erfolgreich ausgeführt werden. Denken Sie daran, dass Erfolg in das Universum des „Undefinierten“ fällt!
MFC-Funktion von Microsoft GetSafeHwnd tatsächlich auf dieses Verhalten angewiesen. Ich weiß nicht, was sie geraucht haben.
Wenn Sie eine virtuelle Funktion aufrufen, muss der Zeiger dereferenziert werden, um zur vtable zu gelangen, und Sie werden mit Sicherheit UB erhalten (wahrscheinlich ein Absturz, aber denken Sie daran, dass es keine Garantien gibt).
Wenn beide Funktionen sind statischwie könnte x nach innen verwiesen werden baz? (x ist eine nicht statische Elementvariable)
– legends2k
29. September 2010 um 16:09 Uhr
@legends2k: Vorgeben
x
wurde auch statisch gemacht. 🙂– GManNickG
29. September 2010 um 21:25 Uhr
Sicherlich, aber für den Fall (a) funktioniert es in allen Fällen gleich, dh die Funktion wird aufgerufen. Wenn Sie jedoch den Wert des Zeigers von 0 auf 1 ersetzen (z. B. durch reinterpret_cast), stürzt es fast immer ab. Stellt die Wertzuweisung von 0 und damit NULL, wie im Fall a, etwas Besonderes für den Compiler dar? Warum stürzt es immer ab, wenn ihm ein anderer Wert zugewiesen wird?
– Siddharth Shankaran
28. Dezember 2010 um 12:18 Uhr
Interessant: Mit der nächsten Überarbeitung von C++ soll es überhaupt keine Dereferenzierung von Zeigern mehr geben. Wir werden jetzt Umleitung durchführen durch Zeiger. Um mehr zu erfahren, führen Sie bitte eine Umleitung über diesen Link durch: N3362
– James McNellis
5. Juni 2012 um 21:08 Uhr
Das Aufrufen einer Member-Funktion für einen Nullzeiger ist immer undefiniertes Verhalten. Wenn ich mir nur Ihren Code ansehe, kann ich bereits das undefinierte Verhalten spüren, das mir langsam den Hals hinaufkriecht!
– fredoverflow
27. Juli 2012 um 9:02 Uhr