wake-up-neo.com

Ist das Inkrementieren eines Nullzeigers genau definiert?

Es gibt viele Beispiele für undefiniertes/nicht angegebenes Verhalten beim Ausführen von Zeigerarithmetik: Zeiger müssen innerhalb desselben Arrays (oder eines nach dem Ende) oder innerhalb desselben Objekts verweisen. Dies schränkt ein, wann Sie Vergleiche/Operationen auf der Grundlage der obigen Angaben durchführen können , usw.

Ist die folgende Operation gut definiert?

int* p = 0;
p++;
48
Luchian Grigore

§ 5.2.6/1:

Der Wert des Operandenobjekts wird durch Hinzufügen von 1 geändert, sofern das Objekt nicht vom Typ bool [..] ist.

Additive Ausdrücke mit Zeigern sind in § 5.7/5 definiert:

Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente des Gleichen Arrayobjekts oder auf ein Element hinter dem letzten Element des Arrayobjekts zeigen, wird Die Auswertung nicht zu einem Überlauf führen. Andernfalls ist das Verhalten undefiniert.

37
Columbo

Es scheint recht wenig zu verstehen, was "undefiniertes Verhalten" bedeutet. 

In C, C++ und verwandten Sprachen wie Objective-C gibt es vier Arten von Verhalten: Es gibt Verhalten, das durch den Sprachstandard definiert wird. Es gibt ein implementierungsdefiniertes Verhalten, dh der Sprachstandard besagt explizit, dass die Implementierung das Verhalten definieren muss. Es gibt ein nicht festgelegtes Verhalten, bei dem der Sprachstandard angibt, dass mehrere Verhaltensweisen möglich sind. Und es gibt undefiniertes Verhalten, bei dem der Sprachstandard nichts über das Ergebnis aussagt . Da der Sprachstandard nichts über das Ergebnis aussagt, kann mit undefiniertem Verhalten überhaupt etwas passieren. 

Einige Leute hier gehen davon aus, dass "undefiniertes Verhalten" "etwas Schlimmes" bedeutet. Das ist falsch. Es bedeutet "alles kann passieren", und dazu gehört, dass "etwas Schlimmes passieren kann" und nicht "Es muss etwas Schlimmes passieren". In der Praxis bedeutet dies, dass "nichts Schlimmes passiert, wenn Sie Ihr Programm testen, aber sobald es an einen Kunden verschickt wird, bricht die Hölle los". Da alles passieren kann, kann der Compiler tatsächlich davon ausgehen, dass in Ihrem Code kein undefiniertes Verhalten vorhanden ist. Entweder ist dies wahr oder falsch. In diesem Fall kann alles passieren, was bedeutet, dass das, was aufgrund der falschen Annahme des Compilers geschieht, immer noch besteht richtig. 

Jemand behauptete, dass, wenn p auf ein Array von 3 Elementen zeigt und p + 4 berechnet wird, nichts Schlimmes passiert. Falsch. Hier kommt dein optimierender Compiler. Sagen Sie, das ist Ihr Code: 

int f (int x)
{
    int a [3], b [4];
    int* p = (x == 0 ? &a [0] : &b [0]);
    p + 4;
    return x == 0 ? 0 : 1000000 / x;
}

Das Auswerten von p + 4 ist undefiniertes Verhalten, wenn p auf a [0] zeigt, aber nicht, wenn es auf b [0] zeigt. Der Compiler darf daher annehmen, dass p auf b [0] zeigt. Der Compiler darf daher von x! = 0 ausgehen, da x == 0 zu undefiniertem Verhalten führt. Der Compiler darf daher die Prüfung x == 0 in der return-Anweisung entfernen und nur 1000000/x zurückgeben. Was bedeutet, dass Ihr Programm abstürzt, wenn Sie f (0) aufrufen, anstatt 0 zurückzusetzen. 

Eine andere Annahme war, dass, wenn Sie einen Nullzeiger inkrementieren und ihn dann erneut dekrementieren, das Ergebnis wieder ein Nullzeiger ist. Wieder falsch. Abgesehen von der Möglichkeit, dass das Inkrementieren eines Nullzeigers auf einigen Hardwareprodukten nur zum Absturz führt, können Sie Folgendes beachten: Da das Inkrementieren eines Nullzeigers nicht undefiniert ist, prüft der Compiler, ob ein Zeiger Null ist, und erhöht den Zeiger nur, wenn es kein Nullzeiger ist also ist p + 1 wieder ein Nullzeiger. Normalerweise macht es das Gleiche für das Dekrementieren. Als cleverer Compiler stellt er jedoch fest, dass p + 1 immer undefiniertes Verhalten ist, wenn das Ergebnis ein Nullzeiger ist. Daher kann davon ausgegangen werden, dass p + 1 kein Nullzeiger ist. Daher kann die Überprüfung des Nullzeigers ausgelassen werden. Was bedeutet (p + 1) - 1 ist kein Nullzeiger, wenn p ein Nullzeiger war. 

15
gnasher729

Vorgänge an einem Zeiger (wie Inkrementieren, Hinzufügen usw.) sind im Allgemeinen nur gültig, wenn sowohl der Anfangswert des Zeigers als auch das Ergebnis auf Elemente desselben Arrays (oder auf einen über dem letzten Element) zeigen. Ansonsten ist das Ergebnis undefiniert. Es gibt verschiedene Klauseln im Standard für die verschiedenen Operatoren, die dies sagen, einschließlich für das Inkrementieren und Hinzufügen.

(Es gibt einige Ausnahmen wie das Hinzufügen von Null zu NULL oder das Subtrahieren von Null von NULL, die gültig sind, aber das trifft hier nicht zu.).

Ein NULL-Zeiger zeigt auf nichts, daher führt das Inkrementieren zu undefiniertem Verhalten (es gilt die Klausel "else").

13
Peter

Es stellt sich heraus, dass es tatsächlich undefiniert ist. Es gibt Systeme, für die dies zutrifft

int *p = NULL;
if (*(int *)&p == 0xFFFF)

Daher würde ++ p die undefinierte Überlaufregel auslösen (stellt die Größe von (int *) == 2) dar). Es ist nicht garantiert, dass Zeiger vorzeichenlose Ganzzahlen sind, daher gilt die Regel für nicht signierte Zeilenumbrüche nicht.

1
Joshua

Wie von Columbo gesagt, ist es UB. Aus sprachlicher Sicht ist dies die endgültige Antwort.

Alle mir bekannten C++ - Compiler-Implementierungen werden jedoch zu demselben Ergebnis führen:

int *p = 0;
intptr_t ip = (intptr_t) p + 1;

cout << ip - sizeof(int) << endl;

gibt 0, was bedeutet, dass p bei einer 32-Bit-Implementierung den Wert 4 und bei einer 64-Bit-Implementierung 8 hat

Anders gesagt:

int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour 
int *p2 = (int *) ip;      // formally UB
p++;               // formally UB
assert ( p2 == p) ;  // works on all major implementation
0
Serge Ballesta

Aus ISO IEC 14882-2011 §5.2.6 :

Der Wert eines postfix ++ - Ausdrucks ist der Wert seines Operanden. [Hinweis: Der erhaltene Wert ist eine Kopie des ursprünglichen Werts - Hinweis]. Der Operand muss ein veränderbarer Wert sein. Der Typ des Operanden muss ein arithmetischer Typ Oder ein Zeiger auf einen vollständigen Objekttyp sein.

Da ein Nullptr ein Zeiger auf einen vollständigen Objekttyp ist. Ich würde also nicht verstehen, warum dies undefiniertes Verhalten wäre.

Wie bereits erwähnt, heißt es auch in §5.2.6/1 :

Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Arrayobjekts oder ein hinter Hinter dem letzten Element des Arrayobjekts verweisen, darf die Auswertung keinen Überlauf erzeugen. Andernfalls ist das Verhalten undefiniert.

Dieser Ausdruck erscheint etwas mehrdeutig. In meiner Interpretation könnte der undefinierte Teil sehr wohl die Bewertung des Objekts sein. Und ich denke, niemand würde dem widersprechen. Zeigerarithmetik scheint jedoch nur ein vollständiges Objekt zu erfordern. 

Natürlich sind Postfix [] - Operatoren und Subtraktionen oder Multiplikationen auf Zeiger auf Array-Objekte nur dann gut definiert, wenn sie tatsächlich auf dasselbe Array zeigen. Dies ist vor allem deshalb wichtig, weil man versucht sein könnte zu glauben, dass 2 Arrays, die in einem Objekt nacheinander definiert werden, wie ein einzelnes Array wiederholt werden können. 

Meine Schlussfolgerung wäre also, dass die Operation genau definiert ist, die Bewertung jedoch nicht. 

0
laurisvr

Der C-Standard verlangt, dass kein Objekt, das über standarddefinierte Mittel erstellt wurde, eine Adresse haben kann, die einem Nullzeiger entspricht. Implementierungen können das Vorhandensein von Objekten zulassen, die nicht über standarddefinierte Mittel erstellt werden. Der Standard sagt jedoch nichts darüber aus, ob ein solches Objekt eine Adresse haben könnte, die (wahrscheinlich aufgrund von Hardwareentwurfsproblemen) einem Nullzeiger entspricht .

Wenn eine Implementierung das Vorhandensein eines Multibyte-Objekts dokumentiert, dessen Adresse mit null verglichen werden würde, würde bei dieser Implementierung char *p = (char*)0; sagen, dass p einen Zeiger auf das erste Byte dieses Objekts hält (das einem Nullzeiger entspricht). und p++ würde es auf das zweite Byte verweisen lassen. Wenn eine Implementierung jedoch nicht die Existenz eines solchen Objekts dokumentiert oder angibt, dass sie eine Zeigerarithmetik ausführt, als ob ein solches Objekt vorhanden wäre, besteht kein Grund, ein bestimmtes Verhalten zu erwarten. Wenn die Implementierung von Versuchen absichtlich abgefangen wird, um beliebige Arten von Arithmetik für Nullzeiger auszuführen, mit Ausnahme des Hinzufügens oder Entfernens von Null- oder anderen Nullzeigern, kann dies eine nützliche Sicherheitsmaßnahme sein, und Code, der Nullzeiger für einen beabsichtigten nützlichen Zweck erhöhen würde, wäre damit nicht kompatibel. Schlimmer noch, einige "clevere" Compiler können entscheiden, dass sie Null-Checks in Fällen von Zeigern auslassen können, die selbst dann erhöht werden würden, wenn sie den Wert null haben, wodurch alle möglichen Verwüstungen entstehen können.

0
supercat