Ist das Verhalten beim Subtrahieren von zwei NULL-Zeigern definiert?

77

Ist die Differenz zweier nicht ungültiger Zeigervariablen definiert (gemäß C99 und / oder C ++ 98), wenn beide NULLbewertet werden?

Angenommen, ich habe eine Pufferstruktur, die folgendermaßen aussieht:

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

Sprich ex.bufauf ein Array oder einen malloc'ed Speicher. Wenn mein Code dies immer sicherstellt pwriteund preadinnerhalb dieses Arrays oder eines nach ihm zeigt, bin ich ziemlich sicher, dass dies ex.pwrite - ex.preadimmer definiert wird. Was ist jedoch, wenn pwriteund preadbeide NULL sind? Kann ich nur erwarten, dass das Subtrahieren der beiden definiert ist (ptrdiff_t)0oder dass streng konformer Code erforderlich ist, um die Zeiger auf NULL zu testen? Beachten Sie, dass der einzige Fall, an dem ich interessiert bin, der Fall ist, wenn beide Zeiger NULL sind (was einen nicht initialisierten Puffer darstellt). Der Grund hat mit einer vollständig kompatiblen "verfügbaren" Funktion zu tun, sofern die vorhergehenden Annahmen erfüllt sind:

size_t buf_avail(const struct s_buf *b)
{     
    return b->pwrite - b->pread;
}
John Luebs
quelle
1
Haben Sie die Operation mehr als einmal versucht?
Hunter McMillen
10
Was meinst du? Ich weiß, dass das Ergebnis dieser Operation bei 95% (sagen wir, die 5% sind AS / 400) der Implementierungen 0 ist und nichts Schlimmes passieren wird. Die Implementierungsspezifikationen interessieren mich nicht. Meine Frage bezieht sich auf einige spezifische Standarddefinitionen.
John Luebs
8
Hunter McMillen: Das ist ein schlechter Ansatz - "Ich habe den Zeiger in int gespeichert und nichts ist passiert. Ich überprüfe verschiedene Computer und Compilatoren und nichts ist passiert. Dann kamen 64-Bit-Computer." Wenn etwas jetzt funktioniert, aber auf undefiniertem Verhalten beruht, funktioniert es möglicherweise in Zukunft nicht mehr.
Maciej Piechotka
3
Ich empfehle Ihnen , um sicherzustellen , Ihr Code wird garantiert durch die einschlägigen Normen zu arbeiten , anstatt nur zu bemerken , dass es an die Arbeit auf den Plattformen geschehen Sie getestet.
David Schwartz
1
@TobySpeight: Ein 8086-Compiler hat eine andere Darstellung für einen nearqualifizierten Nullzeiger als ein qualifizierter Nullzeiger. farWürde er jedoch mehrere Darstellungen für farNullzeiger verwenden? Wenn ein Nullzeiger nearin einen farZeiger konvertiert wird, der wiederum mit einem Nullzeiger verglichen wird, farwenn z. B. DS gleich 0x1234 ist, passiert Folgendes: (1) 0x0000 wird zu 0x0000: 0x0000 zusammengeführt; (2) 0x0000 wird in 0x1234: 0x0000 konvertiert, aber der Vergleichsoperator prüft, ob beide Segmente Null sind, oder (3) 0x0000 wird in 0x1234: 0x0000 konvertiert, was ungleich 0x0000: 0x0000 ist.
Supercat

Antworten:

98

In C99 ist es technisch undefiniertes Verhalten. C99 §6.5.6 sagt:

7) Für die Zwecke dieser Operatoren verhält sich ein Zeiger auf ein Objekt, das kein Element eines Arrays ist, genauso wie ein Zeiger auf das erste Element eines Arrays der Länge eins mit dem Objekttyp als Elementtyp.

[...]

9) Wenn zwei Zeiger subtrahiert werden, zeigen beide auf Elemente desselben Array-Objekts oder auf eines nach dem letzten Element des Array-Objekts. Das Ergebnis ist die Differenz der Indizes der beiden Array-Elemente. [...]

Und §6.3.2.3 / 3 sagt:

Ein ganzzahliger Konstantenausdruck mit dem Wert 0 oder ein solcher Ausdruck, der in einen Typ umgewandelt wird void *, wird als Nullzeigerkonstante bezeichnet. 55) Wenn eine Nullzeigerkonstante in einen Zeigertyp konvertiert wird, wird der resultierende Zeiger, der als Nullzeiger bezeichnet wird, garantiert ungleich einem Zeiger mit einem Objekt oder einer Funktion verglichen.

Da ein Nullzeiger für kein Objekt ungleich ist, verletzt er die Voraussetzungen von 6.5.6 / 9, sodass es sich um ein undefiniertes Verhalten handelt. In der Praxis wäre ich jedoch bereit zu wetten, dass so ziemlich jeder Compiler ein Ergebnis von 0 ohne negative Nebenwirkungen zurückgibt.

In C89 ist es auch undefiniertes Verhalten, obwohl der Wortlaut des Standards etwas anders ist.

C ++ 03 hingegen hat in dieser Instanz ein definiertes Verhalten. Der Standard macht eine besondere Ausnahme für das Subtrahieren von zwei Nullzeigern. C ++ 03 §5.7 / 7 sagt:

Wenn der Wert 0 zu einem Zeigerwert addiert oder von diesem subtrahiert wird, wird das Ergebnis mit dem ursprünglichen Zeigerwert verglichen. Wenn zwei Zeiger auf dasselbe Objekt zeigen oder beide auf eins nach dem Ende desselben Arrays zeigen oder beide null sind und die beiden Zeiger subtrahiert werden, wird das Ergebnis mit dem in den Typ konvertierten Wert 0 verglichen ptrdiff_t.

C ++ 11 (sowie der neueste Entwurf von C ++ 14, n3690) haben den gleichen Wortlaut wie C ++ 03, nur mit der geringfügigen Änderung von std::ptrdiff_tanstelle von ptrdiff_t.

Adam Rosenfield
quelle
14
Aus Gründen der Vollständigkeit ist dies derzeit die beste Antwort.
John Dibling
Dies scheint ein Versehen im Standard zu sein, das korrigiert werden sollte durch "9) Wenn zwei Zeiger subtrahiert werden, wenn sie gleich sind, ist das Ergebnis Null. Andernfalls sollen beide auf Elemente desselben Array-Objekts zeigen ..."
R. .. GitHub STOP HELPING ICE
@R .., um es zu disambiguieren, sollte "vergleiche gleich" nein sagen? Da zwei Nullzeiger möglicherweise nicht denselben Wert enthalten, sind sie nicht gleich.
Jens Gustedt
Es sieht auch so aus, als ob der neueste Entwurf des kommenden C1X-Standards dieselbe Sprache hat. Ich hoffe, dass dies tatsächlich ein Versehen ist und dass das Sprachkomitee es behebt.
Adam Rosenfield
Zwei Null - Zeiger sind der gleiche Wert durch den Vergleich gleich. Natürlich haben sie möglicherweise nicht die gleiche Darstellung .
R .. GitHub STOP HELPING ICE
36

Ich fand dies im C ++ - Standard (5.7 [expr.add] / 7):

Wenn zwei Zeiger [...] beide null sind und die beiden Zeiger subtrahiert werden, wird das Ergebnis mit dem Wert 0 verglichen, der in den Typ std :: ptrdiff_t konvertiert wurde

Wie andere gesagt haben, erfordert C99 das Addieren / Subtrahieren zwischen 2 Zeigern desselben Array-Objekts. NULL zeigt nicht auf ein gültiges Objekt, weshalb Sie es nicht für die Subtraktion verwenden können.

Pubby
quelle
4
+1: Interessant, daher definiert C ++ dieses Verhalten explizit, während C dies nicht tut.
Oliver Charlesworth
23

Bearbeiten : Diese Antwort gilt nur für C, ich habe das C ++ - Tag nicht gesehen, als ich geantwortet habe.

Nein, Zeigerarithmetik ist nur für Zeiger zulässig, die auf dasselbe Objekt zeigen. Da per Definition des C-Standards Nullzeiger auf kein Objekt zeigen, ist dies ein undefiniertes Verhalten.

(Ich würde zwar vermuten, dass jeder vernünftige Compiler nur darauf zurückkommt 0, aber wer weiß.)

Jens Gustedt
quelle
5
Das ist falsch. Siehe 5.7 [expr.add] / 7: "Wenn zwei Zeiger auf dasselbe Objekt zeigen oder beide auf eins nach dem Ende desselben Arrays zeigen oder beide null sind und die beiden Zeiger subtrahiert werden, wird das Ergebnis mit dem Wert 0 verglichen in den Typ konvertiert std::ptrdiff_t. "
CB Bailey
1
Dies ist im spezifischen Kontext von C ++ nicht korrekt. Was ist mit den anderen getaggten Sprachen?
John Dibling
8
Wow, hätte nie gedacht, dass diese lahme Frage einen Unterschied in der C / C ++ - Spezifikation berühren würde.
John Luebs
3
@Jens: Die FAQ- und Site-Administratoren ermutigen uns, Antworten zu bearbeiten, wenn wir sie verbessern können. Ihre Antwort ist jetzt besser als früher. Ich hatte nicht vor zu beleidigen und, um ehrlich zu sein, angesichts der Richtlinien der Website denke ich, dass Sie nicht in der Reihe sind, beleidigt zu werden . Siehe: stackoverflow.com/privileges/edit
John Dibling
2
Jens, normalerweise würde ich dir zustimmen. Ich versuche, niemals die Antwort von jemandem zu bearbeiten, sondern meine Meinungsverschiedenheiten mit einem Kommentar hervorzuheben - weniger Federn werden gekräuselt und sie könnten etwas lernen. Oder vielleicht irre ich mich stattdessen und meine Bearbeitung wäre kontraproduktiv. Aber in diesem Fall denke ich, dass Johns Bearbeitung gerechtfertigt war, weil Ihre Antwort am besten bewertet, aber eindeutig nicht 100% korrekt war. Es war notwendig, die Leute davon abzuhalten, sich auf eine richtig aussehende Antwort zu "stapeln", ohne die Alternativen in Betracht zu ziehen.
Mark Ransom
0

Der C-Standard stellt in diesem Fall keine Anforderungen an das Verhalten, aber viele Implementierungen spezifizieren das Verhalten der Zeigerarithmetik in vielen Fällen über die vom Standard geforderten Mindestanforderungen hinaus, einschließlich dieses.

Bei jeder konformen C-Implementierung und bei fast allen (wenn nicht allen) Implementierungen von C-ähnlichen Dialekten gelten die folgenden Garantien für jeden Zeiger p, der entweder ein Objekt identifiziert *poder *(p-1)identifiziert:

  • Für jeden ganzzahligen Wert z, der gleich Null ist, sind die Zeigerwerte (p+z)und (p-z)in jeder Hinsicht gleichwertig p, mit der Ausnahme, dass sie nur dann konstant sind, wenn beide pund zkonstant sind.
  • Für alle, qdie äquivalent sind p, ergeben die Ausdrücke p-qund q-pbeide Null.

Wenn solche Garantien für alle Zeigerwerte, einschließlich Null, gelten, müssen möglicherweise keine Nullprüfungen im Benutzercode durchgeführt werden. Darüber hinaus wäre es auf den meisten Plattformen einfacher und billiger, Code zu generieren, der solche Garantien für alle Zeigerwerte ohne Rücksicht darauf, ob sie null sind, einhält, als speziell Nullen zu behandeln. Einige Plattformen können jedoch bei Versuchen, eine Zeigerarithmetik mit Nullzeigern durchzuführen, abfangen, selbst wenn Null addiert oder subtrahiert wird. Auf solchen Plattformen würde die Anzahl der vom Compiler generierten Nullprüfungen, die zu Zeigeroperationen hinzugefügt werden müssten, um die Garantie aufrechtzuerhalten, in vielen Fällen die Anzahl der vom Benutzer generierten Nullprüfungen, die als Ergebnis weggelassen werden könnten, erheblich überschreiten.

Wenn es eine Implementierung gäbe, bei der die Kosten für die Aufrechterhaltung der Garantien hoch wären, aber nur wenige, wenn Programme davon profitieren würden, wäre es sinnvoll, zuzulassen, dass "Null + Null" -Berechnungen abgefangen werden und dieser Benutzercode für erforderlich ist Eine solche Implementierung umfasst die manuelle Nullprüfung, die die Garantien möglicherweise unnötig gemacht haben. Es wurde nicht erwartet, dass eine solche Wertberichtigung die anderen 99,44% der Implementierungen betrifft, bei denen der Wert der Aufrechterhaltung der Garantien die Kosten übersteigen würde. Solche Implementierungen sollten solche Garantien aufrechterhalten, aber ihre Autoren sollten die Autoren des Standards nicht brauchen, um ihnen dies mitzuteilen.

Die Autoren von C ++ haben entschieden, dass konforme Implementierungen die oben genannten Garantien um jeden Preis einhalten müssen, selbst auf Plattformen, auf denen sie die Leistung der Zeigerarithmetik erheblich beeinträchtigen könnten. Sie urteilten, dass der Wert der Garantien selbst auf Plattformen, deren Aufrechterhaltung teuer wäre, die Kosten übersteigen würde. Eine solche Einstellung könnte durch den Wunsch beeinflusst worden sein, C ++ als eine höhere Sprache als C zu behandeln. Von AC-Programmierern könnte erwartet werden, dass sie wissen, wann eine bestimmte Zielplattform Fälle wie (null + null) auf ungewöhnliche Weise behandeln würde, aber C ++ - Programmierer Es wurde nicht erwartet, dass sie sich mit solchen Dingen befassen. Die Gewährleistung eines konsistenten Verhaltensmodells wurde daher als die Kosten wert beurteilt.

Fragen, was "definiert" ist, haben heutzutage natürlich selten etwas damit zu tun, welche Verhaltensweisen eine Plattform unterstützen kann. Stattdessen ist es jetzt für Compiler in Mode, im Namen der "Optimierung" zu verlangen, dass Programmierer manuell Code schreiben, um Eckfälle zu behandeln, die Plattformen zuvor korrekt behandelt hätten. Wenn beispielsweise Code, der nZeichen ab der Adresse ausgeben psoll, wie folgt geschrieben wird:

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

ältere Compiler würden Code generieren, der zuverlässig nichts ohne Nebenwirkungen ausgeben würde, wenn p == NULL und n == 0, ohne dass ein Sonderfall n == 0 erforderlich wäre. Bei neueren Compilern müsste man jedoch zusätzlichen Code hinzufügen:

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

was ein Optimierer möglicherweise loswerden kann oder nicht. Wenn der zusätzliche Code nicht eingeschlossen wird, stellen einige Compiler möglicherweise fest, dass, da p "möglicherweise nicht null sein kann", alle nachfolgenden Nullzeigerprüfungen weggelassen werden können, wodurch der Code an einer Stelle unterbrochen wird, die nicht mit dem tatsächlichen "Problem" zusammenhängt.

Superkatze
quelle