Ich lese gerade ein Buch mit dem Titel "Numerical Recipes in C". In diesem Buch beschreibt der Autor, wie bestimmte Algorithmen von Natur aus besser funktionieren, wenn Indizes mit 1 beginnen (ich folge seinem Argument nicht vollständig und das ist nicht der Sinn dieses Beitrags), aber C indiziert seine Arrays immer beginnend mit 0 Um dies zu umgehen, schlägt er vor, den Zeiger nach der Zuweisung einfach zu dekrementieren, z.
float *a = malloc(size);
a--;
Dies, sagt er, gibt Ihnen effektiv einen Zeiger, dessen Index mit 1 beginnt und der dann frei wird mit:
free(a + 1);
Soweit mir bekannt ist, ist dies jedoch ein undefiniertes Verhalten des C-Standards. Dies ist anscheinend ein sehr seriöses Buch innerhalb der HPC-Community, daher möchte ich nicht einfach ignorieren, was er sagt, sondern nur einen Zeiger außerhalb des zugewiesenen Bereichs zu dekrementieren, scheint mir sehr lückenhaft. Ist dieses "erlaubte" Verhalten in C? Ich habe es sowohl mit gcc als auch mit icc getestet und beide Ergebnisse scheinen darauf hinzudeuten, dass ich mir um nichts Sorgen mache, aber ich möchte absolut positiv sein.
Antworten:
Sie haben Recht, dass Code wie
ergibt undefiniertes Verhalten gemäß ANSI C-Standard, Abschnitt 3.3.6:
Für Code wie diesen wurde die Qualität des C-Codes im Buch (damals, als ich ihn Ende der 90er Jahre verwendete) nicht als sehr hoch angesehen.
Das Problem mit undefiniertem Verhalten ist, dass unabhängig vom Ergebnis, das der Compiler erzeugt, dieses Ergebnis per Definition korrekt ist (auch wenn es sehr destruktiv und unvorhersehbar ist).
Glücklicherweise bemühen sich nur sehr wenige Compiler, in solchen Fällen tatsächlich unerwartetes Verhalten zu verursachen, und die typische
malloc
Implementierung auf für HPC verwendeten Computern enthält einige Buchhaltungsdaten unmittelbar vor der zurückgegebenen Adresse, sodass Sie durch die Dekrementierung normalerweise einen Zeiger auf diese Buchhaltungsdaten erhalten. Es ist keine gute Idee, dort zu schreiben, aber nur das Erstellen des Zeigers ist auf diesen Systemen harmlos.Seien Sie sich bewusst sein , dass der Code könnte brechen , wenn die Laufzeitumgebung geändert wird oder wenn der Code in eine andere Umgebung portiert.
quelle
Offiziell ist es undefiniertes Verhalten, einen Zeigerpunkt außerhalb des Arrays zu haben (mit Ausnahme eines nach dem Ende), auch wenn er nie dereferenziert wird .
In der Praxis , wenn Ihr Prozessor eine flache Speichermodell (im Gegensatz zu seltsam diejenigen wie hat x86-16 ), und wenn die Compiler Ihnen nicht einen Laufzeitfehler oder falsche Optimierung nicht geben , wenn Sie einen ungültigen Zeiger erstellen, dann Arbeit des Code Alles gut.
quelle
Erstens ist es undefiniertes Verhalten. Einige optimierende Compiler werden heutzutage sehr aggressiv gegenüber undefiniertem Verhalten. Da a-- in diesem Fall ein undefiniertes Verhalten ist, könnte der Compiler beispielsweise entscheiden, einen Befehl und einen Prozessorzyklus zu speichern und a nicht zu dekrementieren. Welches ist offiziell korrekt und legal.
Wenn Sie dies ignorieren, können Sie 1, 2 oder 1980 subtrahieren. Wenn ich beispielsweise Finanzdaten für die Jahre 1980 bis 2013 habe, kann ich 1980 subtrahieren. Wenn wir nun float * a = malloc (Größe) nehmen; es gibt sicherlich eine große Konstante k, so dass a - k ein Nullzeiger ist. In diesem Fall erwarten wir wirklich, dass etwas schief geht.
Nehmen Sie jetzt eine große Struktur, sagen wir ein Megabyte groß. Ordnen Sie einen Zeiger p zu, der auf zwei Strukturen zeigt. p - 1 kann ein Nullzeiger sein. p - 1 wird möglicherweise umbrochen (wenn eine Struktur ein Megabyte ist und der Malloc-Block 900 KB vom Beginn des Adressraums entfernt ist). So könnte es ohne Böswilligkeit des Compilers sein, dass p - 1> p. Dinge können interessant werden.
quelle
Dürfen? Ja. Gute Idee? Nicht gewöhnlich.
C ist eine Abkürzung für Assemblersprache, und in Assemblersprache gibt es keine Zeiger, nur Speicheradressen. Cs Zeiger sind Speicheradressen, die ein Nebenverhalten aufweisen, bei dem sie um die Größe dessen, worauf sie zeigen, inkrementiert oder dekrementiert werden, wenn sie einer Arithmetik unterzogen werden. Dies macht aus Syntaxperspektive Folgendes gut:
Arrays sind in C nicht wirklich eine Sache; Sie sind nur Zeiger auf zusammenhängende Speicherbereiche, die sich wie Arrays verhalten. Der
[]
Operator ist eine Abkürzung für Zeigerarithmetik und Dereferenzierung,a[x]
bedeutet also eigentlich*(a + x)
.Es gibt gute Gründe , die oben, wie einige E / A - Gerät mit ein paar zu tun
double
s in die kartierte0xdeadbee7
und0xdeadbeef
. Sehr wenige Programme müssten das tun.Wenn Sie die Adresse von etwas erstellen, z. B. mithilfe des
&
Operators oder durch Aufrufenmalloc()
, möchten Sie den ursprünglichen Zeiger intakt halten, damit Sie wissen, dass das, worauf er verweist, tatsächlich etwas Gültiges ist. Das Dekrementieren des Zeigers bedeutet, dass ein Teil des fehlerhaften Codes versuchen könnte, ihn zu dereferenzieren, fehlerhafte Ergebnisse zu erhalten, etwas zu blockieren oder, abhängig von Ihrer Umgebung, eine Segmentierungsverletzung zu begehen. Dies gilt insbesondere fürmalloc()
, weil Sie die Last auffree()
denjenigen legen, der anruft , sich daran zu erinnern, den ursprünglichen Wert zu übergeben, und nicht auf eine geänderte Version, die dazu führt, dass alles zum Teufel losbricht.Wenn Sie in C 1-basierte Arrays benötigen, können Sie dies sicher tun, indem Sie ein zusätzliches Element zuweisen, das niemals verwendet wird:
Beachten Sie, dass dies keinen Schutz gegen das Überschreiten der Obergrenze bietet, aber das ist einfach genug zu handhaben.
Nachtrag:
Einige Kapitel und Verse aus dem C99-Entwurf (Entschuldigung, das ist alles, worauf ich verlinken kann):
§6.5.2.1.1 besagt, dass der zweite ("andere") Ausdruck, der mit dem Indexoperator verwendet wird, vom Typ Integer ist.
-1
ist eine ganze Zahl, und das machtp[-1]
gültig und macht daher auch den Zeiger&(p[-1])
gültig. Dies bedeutet nicht, dass der Zugriff auf Speicher an dieser Stelle ein definiertes Verhalten erzeugen würde, aber der Zeiger ist immer noch ein gültiger Zeiger.§6.5.2.2 besagt, dass der Array-Indexoperator das Äquivalent zum Hinzufügen der Elementnummer zum Zeiger
p[-1]
ergibt und daher äquivalent zu ist*(p + (-1))
. Immer noch gültig, aber möglicherweise nicht erwünscht.§6.5.6.8 sagt (Hervorhebung von mir):
Dies bedeutet, dass die Ergebnisse der Zeigerarithmetik auf ein Element in einem Array zeigen müssen. Es heißt nicht, dass die Arithmetik auf einmal durchgeführt werden muss. Deshalb:
Empfehle ich, die Dinge so zu machen? Ich nicht, und meine Antwort erklärt warum.
quelle