Ist es legal, baumelnde Zeiger zu vergleichen?
int *p, *q;
{
int a;
p = &a;
}
{
int b;
q = &b;
}
std::cout << (p == q) << '\n';
Beachten Sie, wie beide p
und q
auf Objekte zeigen, die bereits verschwunden sind. Ist das legal?
Ist es legal, baumelnde Zeiger zu vergleichen?
int *p, *q;
{
int a;
p = &a;
}
{
int b;
q = &b;
}
std::cout << (p == q) << '\n';
Beachten Sie, wie beide p
und q
auf Objekte zeigen, die bereits verschwunden sind. Ist das legal?
int*f(){int a;return &a;}
auf return 0;
.
Antworten:
Einleitung: Die erste Frage ist, ob es legal ist, den Wert von p
überhaupt zu verwenden.
Nach a
zerstört worden ist , p
erwirbt , was als bekannt ist ungültig Zeigerwert . Zitat aus N4430 (zur Diskussion des Status von N4430 siehe "Hinweis" unten):
Wenn das Ende der Dauer eines Speicherbereichs erreicht ist, werden die Werte aller Zeiger, die die Adresse eines Teils des freigegebenen Speichers darstellen, zu ungültigen Zeigerwerten .
Das Verhalten bei Verwendung eines ungültigen Zeigerwerts wird ebenfalls im selben Abschnitt von N4430 behandelt (und nahezu identischer Text wird in C ++ 14 [basic.stc.dynamic.deallocation] / 4 angezeigt):
Die Indirektion durch einen ungültigen Zeigerwert und die Übergabe eines ungültigen Zeigerwerts an eine Freigabefunktion haben ein undefiniertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten .
[ Fußnote: Einige Implementierungen definieren möglicherweise, dass das Kopieren eines ungültigen Zeigerwerts einen vom System generierten Laufzeitfehler verursacht. - Fußnote beenden]
Sie müssen daher die Dokumentation Ihrer Implementierung konsultieren, um herauszufinden, was hier geschehen soll (seit C ++ 14).
Der Begriff wird in den obigen Anführungszeichen verwendet Mitteln erforderlich lvalue-to-rvalue Umwandlung, wie in C ++ 14 [conv.lval / 2]:
Wenn eine lWert-zu-r-Wert-Konvertierung auf einen Ausdruck e angewendet wird und [...] das Objekt, auf das sich der gl-Wert bezieht, einen ungültigen Zeigerwert enthält, ist das Verhalten implementierungsdefiniert.
Verlauf: In C ++ 11 wurde dies eher als undefiniert als als implementierungsdefiniert bezeichnet . es wurde von DR1438 geändert . Die vollständigen Anführungszeichen finden Sie im Bearbeitungsverlauf dieses Beitrags.
Anwendung auf p == q
: Angenommen, wir haben in C ++ 14 + N4430 akzeptiert, dass das Ergebnis der Bewertung p
undq
Implementierung definiert ist und dass die Implementierung nicht definiert, dass ein Hardware-Trap auftritt. [expr.eq] / 2 sagt:
Zwei Zeiger werden gleich verglichen, wenn beide null sind, beide auf dieselbe Funktion zeigen oder beide dieselbe Adresse darstellen (3.9.2), andernfalls werden sie ungleich verglichen.
Da es implementierungsdefiniert ist, welche Werte wann erhalten p
und q
ausgewertet werden, können wir nicht sicher sagen, was hier passieren wird. Es muss jedoch entweder implementierungsdefiniert oder nicht spezifiziert sein.
g ++ scheint in diesem Fall ein nicht spezifiziertes Verhalten aufzuweisen; Abhängig vom -O
Schalter konnte ich entweder 1
oder sagen lassen 0
, ob dieselbe Speicheradresse b
nach a
der Zerstörung wiederverwendet wurde oder nicht .
Hinweis zu N4430: Dies ist eine vorgeschlagene Fehlerbehebung für C ++ 14, die noch nicht akzeptiert wurde. Es bereinigt viele Formulierungen in Bezug auf die Objektlebensdauer, ungültige Zeiger, Unterobjekte, Gewerkschaften und den Zugriff auf Array-Grenzen.
Im C ++ 14-Text wird unter [basic.stc.dynamic.deallocation] / 4 und den folgenden Absätzen definiert, dass ein ungültiger Zeigerwert vorliegt ergibt sich , wenn delete
verwendet. Es ist jedoch nicht klar angegeben, ob das gleiche Prinzip für die statische oder automatische Speicherung gilt oder nicht.
Es gibt eine Definition "gültiger Zeiger" in [basic.compound] / 3, aber sie ist zu vage, um sie sinnvoll zu verwenden. Die [basic.life] / 5 (Fußnote) bezieht sich auf denselben Text, um das Verhalten von Zeigern auf Objekte von zu definieren statische Speicherdauer, was darauf hindeutet, dass sie für alle Speichertypen gelten sollte.
In N4430 wird der Text von diesem Abschnitt um eine Ebene nach oben verschoben, sodass er eindeutig für alle Speicherdauern gilt. Es ist ein Hinweis beigefügt:
Entwurfsnotiz: Dies sollte für alle Speicherdauern gelten, die enden können, nicht nur für die dynamische Speicherdauer. In einer Implementierung, die Threads oder segmentierte Stapel unterstützt, verhalten sich Thread und automatischer Speicher möglicherweise genauso wie dynamischer Speicher.
Meine Meinung: Ich sehe keine konsistente Möglichkeit, den Standard (vor N4430) zu interpretieren, außer zu sagen, dass er p
einen ungültigen Zeigerwert erhält. Das Verhalten scheint von keinem anderen Abschnitt als dem, was wir uns bereits angesehen haben, abgedeckt zu werden. Daher bin ich froh, den Wortlaut des N4430 so zu behandeln, dass er in diesem Fall die Absicht des Standards darstellt.
In der Vergangenheit gab es einige Systeme, bei denen die Verwendung eines Zeigers als r-Wert dazu führen kann, dass das System einige Informationen abruft, die durch einige Bits in diesem Zeiger gekennzeichnet sind. Wenn ein Zeiger beispielsweise die Adresse des Headers eines Objekts zusammen mit einem Versatz in das Objekt enthalten könnte, könnte das Abrufen eines Zeigers dazu führen, dass das System auch einige Informationen aus diesem Header abruft. Wenn das Objekt nicht mehr existiert, kann der Versuch, Informationen aus seinem Header abzurufen, mit willkürlichen Konsequenzen fehlschlagen.
Allerdings werden in der überwiegenden Mehrheit der C-Implementierungen alle Zeiger, die zu einem bestimmten Zeitpunkt am Leben waren, für immer die gleichen Beziehungen in Bezug auf die relationalen und Subtraktionsoperatoren haben wie zu diesem bestimmten Zeitpunkt. In der Tat kann man in den meisten Implementierungen, wenn man dies hat char *p
, bestimmen, ob es einen Teil eines Objekts identifiziert, char *base; size_t size;
indem man prüft, ob (size_t)(p-base) < size
; Ein solcher Vergleich funktioniert auch nachträglich, wenn sich die Lebensdauer der Objekte überschneidet.
Leider definiert der Standard weder Mittel, mit denen Code anzeigen kann, dass er eine der letzteren Garantien benötigt, noch gibt es ein Standardmittel, mit dem Code fragen kann, ob eine bestimmte Implementierung eines der letzteren Verhaltensweisen versprechen und die Kompilierung ablehnen kann, wenn dies nicht der Fall ist . Ferner betrachten einige hypermoderne Implementierungen jede Verwendung von relationalen oder Subtraktionsoperatoren für zwei Zeiger als ein Versprechen des Programmierers, dass die fraglichen Zeiger immer dasselbe lebende Objekt identifizieren und jeglichen Code weglassen, der nur relevant wäre, wenn diese Annahme hielt nicht. Obwohl viele Hardwareplattformen in der Lage wären, Garantien anzubieten, die für viele Algorithmen nützlich wären, gibt es folglich
Die Zeiger enthalten die Adressen der Variablen, auf die sie verweisen. Die Adressen sind auch dann gültig, wenn die Variablen, die dort gespeichert waren, freigegeben / zerstört / nicht verfügbar sind. Solange Sie nicht versuchen, die Werte an diesen Adressen zu verwenden, sind Sie sicher, was bedeutet, dass * p und * q undefiniert sind.
Offensichtlich ist das Ergebnis eine definierte Implementierung. Daher kann dieses Codebeispiel verwendet werden, um die Funktionen Ihres Compilers zu untersuchen, wenn Sie sich nicht mit Assemblycode befassen möchten.
Ob dies eine sinnvolle Praxis ist, ist eine völlig andere Diskussion.