Der Zeiger
bezieht sich auf eine Speicherzelle, und das
Dereferenzieren eines Zeigers bedeutet das Lesen des Werts der angegebenen Zelle. Der Wert des Zeigers selbst ist die Adresse der Speicherzelle. Der C-Sprachstandard spezifiziert nicht das Formular zur Darstellung von Speicheradressen. Dies ist ein sehr wichtiger Punkt, da unterschiedliche Architekturen unterschiedliche Adressierungsmodelle verwenden können. Die meisten modernen Architekturen verwenden einen linearen Adressraum oder ähnliches. Selbst diese Frage ist jedoch nicht streng spezifiziert, da Adressen physisch oder virtuell sein können. Einige Architekturen verwenden überhaupt eine nicht numerische Darstellung. Symbolics Lisp Machine arbeitet also mit Tupeln der Form
(Objekt, Versatz) als Adressen.
Einige Zeit später, nach der Veröffentlichung der Übersetzung über Habré, nahm der Autor große Änderungen am Text des Artikels vor. Das Aktualisieren einer Übersetzung auf Habré ist keine gute Idee, da einige Kommentare ihre Bedeutung verlieren oder fehl am Platz aussehen. Ich möchte den Text nicht als neuen Artikel veröffentlichen. Deshalb haben wir gerade die Übersetzung des Artikels auf viva64.com aktualisiert und hier alles so gelassen, wie es ist. Wenn Sie ein neuer Leser sind, empfehle ich Ihnen, eine neuere Übersetzung auf unserer Website zu lesen, indem Sie auf den obigen Link klicken. |
Der Standard schreibt nicht die Form der Darstellung von Zeigern vor, sondern legt mehr oder weniger Operationen mit ihnen fest. Nachfolgend betrachten wir diese Operationen und die Merkmale ihrer Definition im Standard. Beginnen wir mit dem folgenden Beispiel:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
Wenn wir diesen GCC-Code mit Optimierungsstufe 1 kompilieren und das Programm unter Linux x86-64 ausführen, wird Folgendes gedruckt:
0x7fff4a35b19c 0x7fff4a35b19c 0
Beachten Sie, dass sich die Zeiger
p und
q auf dieselbe Adresse beziehen. Das Ergebnis des Ausdrucks
p == q ist jedoch
falsch , und dies erscheint auf den ersten Blick seltsam. Sollten nicht zwei Zeiger auf dieselbe Adresse gleich sein?
So definiert der C-Standard das Ergebnis der Überprüfung von zwei Zeigern auf Gleichheit:
C11 § 6.5.9 Absatz 6
Zwei Zeiger sind genau dann gleich, wenn beide Null sind. Entweder zeigen sie auf dasselbe Objekt (einschließlich eines Zeigers auf das Objekt und das erste Unterobjekt in der Struktur des Objekts) oder auf eine Funktion oder geben die Position nach dem letzten Element des Arrays oder einen Zeiger an bezieht sich auf die Position nach dem letzten Element des Arrays, und das andere bezieht sich auf den Beginn eines anderen Arrays unmittelbar nach dem ersten im selben Adressraum. |
Zunächst stellt sich die Frage: Was ist ein „Objekt
“ ? Da es sich um die C-Sprache handelt, ist es offensichtlich, dass Objekte hier nichts mit Objekten in OOP-Sprachen wie C ++ zu tun haben. Im C-Standard ist dieses Konzept nicht vollständig definiert:
C11 § 3.15
Ein Objekt ist ein Laufzeitspeicherbereich, dessen Inhalt zur Darstellung von Werten verwendet werden kann
HINWEIS Wenn erwähnt, kann davon ausgegangen werden, dass ein Objekt einen bestimmten Typ hat. siehe 6.3.2.1. |
Lass es uns richtig machen. Eine 16-Bit-Ganzzahlvariable ist ein Datensatz im Speicher, der 16-Bit-Ganzzahlwerte darstellen kann. Daher ist eine solche Variable ein Objekt. Sind zwei Zeiger gleich, wenn sich einer auf das erste Byte einer bestimmten Ganzzahl und der zweite auf das zweite Byte derselben Zahl bezieht? Das Sprachstandardisierungskomitee meinte das natürlich überhaupt nicht. Aber hier sollte angemerkt werden, dass er in dieser Hinsicht keine klaren Erklärungen hat und wir gezwungen sind zu erraten, was wirklich gemeint war.
Wenn der Compiler im Weg ist
Kehren wir zu unserem ersten Beispiel zurück. Der Zeiger
p wird von Objekt
a erhalten , und der Zeiger
q wird von Objekt
b erhalten . Im zweiten Fall wird die Adressarithmetik verwendet, die für die Plus- und Minusoperatoren wie folgt definiert ist:
C11 § 6.5.6 Ziffer 7
Bei Verwendung mit diesen Operatoren verhält sich ein Zeiger auf ein Objekt, das kein Element des Arrays ist, wie ein Zeiger auf den Anfang eines Arrays mit einer Länge von einem Element, dessen Typ dem Typ des ursprünglichen Objekts entspricht. |
Da jeder Zeiger auf ein Objekt, das kein Array ist,
tatsächlich ein Zeiger auf ein Array mit einer Länge von einem Element wird, definiert der Standard die Adressarithmetik nur für Zeiger auf Arrays - dies ist Punkt 8. Der folgende Teil interessiert uns:
C11 § 6.5.6 Ziffer 8
Wenn dem Zeiger ein ganzzahliger Ausdruck hinzugefügt oder von diesem subtrahiert wird, ist der resultierende Zeiger vom gleichen Typ wie der ursprüngliche Zeiger. Wenn sich der Quellzeiger auf ein Array-Element bezieht und das Array eine ausreichende Länge hat, werden die Quelle und die resultierenden Elemente voneinander getrennt, sodass die Differenz zwischen ihren Indizes gleich dem Wert des ganzzahligen Ausdrucks ist. Mit anderen Worten, wenn der Ausdruck P auf das i-te Element des Arrays zeigt, geben die Ausdrücke (P) + N (oder sein Äquivalent N + (P) ) und (P) -N (wobei N den Wert n hat) jeweils (i + n) an. th und (i - n) th Elemente des Arrays, sofern sie existieren. Wenn der Ausdruck P auf das letzte Element des Arrays zeigt, gibt der Ausdruck (P) +1 die Position nach dem letzten Element des Arrays an, und wenn der Ausdruck Q die Position nach dem letzten Element des Arrays angibt, gibt der Ausdruck (Q) -1 das letzte Element an Array. Wenn sich sowohl die Quelle als auch die resultierenden Zeiger auf Elemente desselben Arrays oder auf die Position nach dem letzten Element des Arrays beziehen, wird ein Überlauf ausgeschlossen. Andernfalls ist das Verhalten undefiniert. Wenn sich der resultierende Zeiger auf die Position nach dem letzten Element des Arrays bezieht, kann der unäre * -Operator nicht darauf angewendet werden. |
Daraus folgt, dass das Ergebnis des Ausdrucks
& b + 1 definitiv eine Adresse sein sollte und daher
p und
q gültige Zeiger sind. Ich möchte Sie daran erinnern, wie die Gleichheit zweier Zeiger im Standard definiert ist: "
Zwei Zeiger sind genau dann gleich, wenn [...] ein Zeiger auf die Position nach dem letzten Element des Arrays und der andere auf den Anfang eines anderen Arrays unmittelbar nach dem ersten im selben verweist Adressraum " (C11 § 6.5.9 Ziffer 6). Genau das beobachten wir in unserem Beispiel. Der Zeiger q bezieht sich auf die Position nach dem Objekt b, unmittelbar gefolgt von dem Objekt a, auf das sich der Zeiger p bezieht. Gibt es einen Fehler in GCC? Dieser Widerspruch wurde 2014 als
Fehler Nr. 61502 beschrieben , aber GCC-Entwickler betrachten ihn nicht als Fehler und werden ihn daher nicht beheben.
Ein ähnliches Problem wurde 2016 von Linux-Programmierern festgestellt. Betrachten Sie den folgenden Code:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
Die Symbole
_start und
_end geben die Grenzen des Speicherbereichs an. Da sie in eine externe Datei übertragen werden, weiß der Compiler nicht, wie sich die Arrays tatsächlich im Speicher befinden. Aus diesem Grund sollte er hier vorsichtig sein und davon ausgehen, dass sie im Adressraum aufeinander folgen. GCC kompiliert jedoch die Schleifenbedingung so, dass sie immer wahr ist, wodurch die Schleife unendlich wird. Dieses Problem wird hier in diesem
Beitrag zu LKML beschrieben - dort wird ein ähnliches Codefragment verwendet. In diesem Fall haben die Autoren von GCC die Kommentare dennoch berücksichtigt und das Verhalten des Compilers geändert. Zumindest konnte ich diesen Fehler in GCC Version 7.3.1 unter Linux x86_64 nicht reproduzieren.
Lösung - im Fehlerbericht # 260?
Unser Fall kann den Fehlerbericht
Nr. 260 klarstellen. Es geht mehr um unsichere Werte, aber Sie können einen merkwürdigen Kommentar des Komitees darin finden:
Compiler-Implementierungen [...] können auch Zeiger unterscheiden, die von verschiedenen Objekten erhalten wurden, selbst wenn diese Zeiger den gleichen Satz von Bits haben.Wenn wir diesen Kommentar wörtlich nehmen, ist es logisch, dass das Ergebnis des Ausdrucks
p == q "falsch" ist, da
p und
q von verschiedenen Objekten erhalten werden, die in keiner Weise verbunden sind. Es scheint, dass wir der Wahrheit näher kommen - oder nicht? Bisher haben wir uns mit Gleichheitsoperatoren befasst, aber was ist mit Beziehungsoperatoren?
Der letzte Hinweis ist in Relation Operatoren?
Die Definition der Beziehungsoperatoren
< ,
<= ,
> und
> = im Kontext von Zeigervergleichen enthält einen merkwürdigen Gedanken:
C11 § 6.5.8 Absatz 5
Das Ergebnis des Vergleichs zweier Zeiger hängt von der relativen Position der angegebenen Objekte im Adressraum ab. Wenn sich zwei Zeiger auf Objekttypen auf dasselbe Objekt beziehen oder beide auf die Position nach dem letzten Element desselben Arrays verweisen, sind diese Zeiger gleich. Wenn die angegebenen Objekte Mitglieder desselben zusammengesetzten Objekts sind, sind Zeiger auf Mitglieder der später deklarierten Struktur mehr als Zeiger auf zuvor deklarierte Mitglieder, und Zeiger auf Elemente eines Arrays mit höheren Indizes sind mehr als Zeiger auf Elemente desselben Arrays mit niedrigeren Indizes. Alle Zeiger auf Mitglieder derselben Vereinigung sind gleich. Wenn der Ausdruck P auf ein Element des Arrays zeigt und der Ausdruck Q auf das letzte Element desselben Arrays zeigt, ist der Wert des Zeigerausdrucks Q + 1 größer als der Wert des Ausdrucks P. In allen anderen Fällen ist das Verhalten nicht definiert. |
Gemäß dieser Definition wird das Ergebnis des Vergleichens von Zeigern nur bestimmt, wenn die Zeiger von
demselben Objekt erhalten werden. Wir zeigen dies anhand von zwei Beispielen.
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
Hier beziehen sich die Zeiger
p und
q auf zwei verschiedene Objekte, die nicht miteinander verbunden sind. Daher ist das Ergebnis ihres Vergleichs nicht definiert. Aber im folgenden Beispiel:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
Die Zeiger
p und
q beziehen sich auf dasselbe Objekt und sind daher miteinander verbunden. Sie können also verglichen werden - es sei denn,
malloc gibt einen Nullwert zurück.
Zusammenfassung
Der C11-Standard beschreibt Zeigervergleiche nicht angemessen. Der problematischste Punkt, auf den wir gestoßen sind, war Absatz 6 § 6.5.9, wo es ausdrücklich erlaubt ist, zwei Zeiger zu vergleichen, die auf zwei verschiedene Arrays verweisen. Dies widerspricht dem Kommentar aus dem Fehlerbericht Nr. 260. Dort sprechen wir jedoch über unbestimmte Bedeutungen, und ich möchte meine Argumentation nicht allein auf der Grundlage dieses Kommentars aufbauen und in einem anderen Kontext interpretieren. Beim Vergleichen von Zeigern werden Beziehungsoperatoren geringfügig anders definiert als Gleichheitsoperatoren - Beziehungsoperatoren werden nur definiert, wenn beide Zeiger von
demselben Objekt erhalten werden.
Wenn wir den Text des Standards ignorieren und fragen, ob es möglich ist, zwei Zeiger zu vergleichen, die von zwei verschiedenen Objekten erhalten wurden, lautet die Antwort auf jeden Fall höchstwahrscheinlich "Nein". Das Beispiel am Anfang des Artikels zeigt ein theoretisches Problem. Da die Variablen
a und
b eine automatische Speicherdauer haben, sind unsere Annahmen über ihre Speicherung im Speicher unzuverlässig. In einigen Fällen können wir raten, aber es ist offensichtlich, dass ein solcher Code nicht sicher portiert werden kann, und Sie können die Bedeutung des Programms nur durch Kompilieren und Ausführen oder Zerlegen des Codes herausfinden, was jedem ernsthaften Programmierparadigma widerspricht.
Im Allgemeinen bin ich jedoch mit dem Wortlaut der C11-Norm nicht zufrieden, und da bereits mehrere Personen auf dieses Problem gestoßen sind, bleibt die Frage: Warum nicht die Regeln klarer formulieren?
Ergänzung
Zeiger auf die Position nach dem letzten Element des Arrays
In Bezug auf die Regel zum Vergleichen und Adressieren der Arithmetik von Zeigern auf die Position nach dem letzten Element des Arrays finden Sie häufig Ausnahmen. Angenommen, der Standard erlaubt nicht den Vergleich von zwei Zeigern, die von
demselben Array erhalten wurden, obwohl sich mindestens einer von ihnen auf die Position hinter dem Ende des Arrays bezieht. Dann würde der folgende Code nicht funktionieren:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
Unter Verwendung einer Schleife gehen wir um das gesamte
x- Array herum, das aus 64 Elementen besteht, d.h. Der Schleifenkörper sollte genau 64 Mal ausgeführt werden. Tatsächlich wird die Bedingung jedoch 65 Mal überprüft - einmal mehr als die Anzahl der Elemente im Array. In den ersten 64 Iterationen bezieht sich der Zeiger
i immer auf das Innere des Arrays
x , während der Ausdruck
& x [num] immer die Position nach dem letzten Element des Arrays angibt. Bei der 65. Iteration bezieht sich der Zeiger
i auch auf die Position hinter dem Ende des Arrays
x , wodurch die Schleifenbedingung falsch wird. Dies ist eine bequeme Möglichkeit, das gesamte Array zu umgehen, und beruht auf einer Ausnahme von der Regel der Unsicherheit des Verhaltens beim Vergleich solcher Zeiger. Beachten Sie, dass der Standard nur das Verhalten beim Vergleichen von Zeigern beschreibt. Dereferenzierung ist ein separates Thema.
Ist es möglich, unser Beispiel so zu ändern, dass sich kein einziger Zeiger auf die Position nach dem letzten Element des Arrays
x bezieht? Es ist möglich, aber es wird schwieriger. Wir müssen die Schleifenbedingung ändern und das Inkrementieren der Variablen
i bei der letzten Iteration verhindern.
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
Dieser Code steckt voller technischer Feinheiten, mit denen sich die Hauptaufgabe ablenkt. Zusätzlich erschien ein zusätzlicher Zweig im Körper der Schleife. Daher finde ich es vernünftig, dass der Standard Ausnahmen beim Vergleichen von Positionszeigern nach dem letzten Element eines Arrays zulässt.
PVS-Studio Team HinweisBei der Entwicklung des PVS-Studio-Code-Analysators müssen wir uns manchmal mit subtilen Problemen befassen, um die Diagnose genauer zu gestalten oder unseren Kunden detaillierte Konsultationen zu geben. Dieser Artikel erschien uns interessant, da er Themen berührt, bei denen wir uns nicht ganz sicher fühlen. Deshalb haben wir die Autorin gebeten, ihre Übersetzung zu veröffentlichen. Wir hoffen, dass mehr C- und C ++ - Programmierer sie kennenlernen und verstehen, dass dies nicht so einfach ist und dass Sie sich nicht beeilen sollten, wenn der Analysator plötzlich eine seltsame Meldung anzeigt, diese als falsch positiv zu betrachten :).Der Artikel wurde erstmals in englischer Sprache bei stefansf.de veröffentlicht. Übersetzungen werden mit Genehmigung des Autors veröffentlicht.