Wie funktioniert der Zeigervergleich in C? Ist es in Ordnung, Zeiger zu vergleichen, die nicht auf dasselbe Array verweisen?

33

In Kapitel 5 von K & R (The C Programming Language 2nd Edition) habe ich Folgendes gelesen:

Erstens können Zeiger unter bestimmten Umständen verglichen werden. Wenn pund qPunkt an den Mitgliedern des gleichen Array, dann Beziehungen wie ==, !=, <, >=etc. richtig funktionieren.

Dies scheint zu implizieren, dass nur Zeiger verglichen werden können, die auf dasselbe Array zeigen.

Allerdings, als ich diesen Code ausprobiert habe

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 wird auf den Bildschirm gedruckt.

Zunächst einmal, dachte ich , ich würde oder irgendeine Art oder Fehler nicht definiert werden, weil ptund pxnicht auf das gleiche Array zeigen werden (zumindest in meinem Verständnis).

Liegt auch pt > pxdaran, dass beide Zeiger auf Variablen zeigen, die auf dem Stapel gespeichert sind, und der Stapel nach unten wächst, sodass die Speicheradresse von tgrößer als die von x? Ist. Welches ist, warum pt > pxist wahr?

Ich werde verwirrter, wenn Malloc eingeführt wird. Auch in K & R in Kapitel 8.7 ist Folgendes geschrieben:

Es gibt jedoch immer noch eine Annahme, dass Zeiger auf verschiedene von zurückgegebene Blöcke sbrksinnvoll verglichen werden können. Dies wird durch den Standard nicht garantiert, der Zeigervergleiche nur innerhalb eines Arrays erlaubt. Somit ist diese Version von mallocnur unter Maschinen portierbar, für die der allgemeine Zeigervergleich sinnvoll ist.

Ich hatte kein Problem damit, Zeiger, die auf den auf dem Heap angegebenen Speicherplatz verweisen, mit Zeigern zu vergleichen, die auf Stapelvariablen verweisen.

Der folgende Code hat beispielsweise beim 1Drucken einwandfrei funktioniert :

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Aufgrund meiner Experimente mit meinem Compiler denke ich, dass jeder Zeiger mit jedem anderen Zeiger verglichen werden kann, unabhängig davon, wohin er einzeln zeigt. Darüber hinaus denke ich, dass die Zeigerarithmetik zwischen zwei Zeigern in Ordnung ist, unabhängig davon, wo sie einzeln zeigen, da die Arithmetik nur die Speicheradressen verwendet, die die Zeiger speichern.

Trotzdem bin ich verwirrt von dem, was ich in K & R lese.

Der Grund, den ich frage, ist, dass mein prof. machte es tatsächlich zu einer Prüfungsfrage. Er gab den folgenden Code:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

Was bewerten diese:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

Die Antwort ist 0, 1und 0.

(Mein Professor enthält den Haftungsausschluss für die Prüfung, dass die Fragen für eine 64-Bit-Programmierumgebung mit Ubuntu Linux 16.04 und 64-Bit-Version gelten.)

(Anmerkung des Herausgebers: Wenn SO mehr Tags zulässt, würde dieser letzte Teil , und möglicherweise rechtfertigen . Wenn der Punkt der Frage / Klasse spezifisch Details zur Implementierung des Betriebssystems auf niedriger Ebene und nicht portables C wäre.)

Shisui
quelle
17
Sie sind verwirrend vielleicht was ist gültig in Cdem , was ist sicher in C. Der Vergleich von zwei Zeigern mit demselben Typ kann immer durchgeführt werden (z. B. Überprüfung auf Gleichheit), jedoch mithilfe von Zeigerarithmetik und Vergleich >und <ist nur dann sicher, wenn er innerhalb eines bestimmten Arrays (oder Speicherblocks) verwendet wird.
Adrian Mole
13
Als beiseite, sollten Sie nicht C von K & R lernen. Zunächst einmal hat sich die Sprache seitdem stark verändert. Und um ehrlich zu sein, stammte der Beispielcode aus einer Zeit, in der eher Knappheit als Lesbarkeit geschätzt wurde.
Paxdiablo
5
Nein, es funktioniert nicht garantiert. In der Praxis kann dies auf Maschinen mit segmentierten Speichermodellen fehlschlagen. Siehe Hat C ein Äquivalent von std :: less aus C ++? Auf den meisten modernen Maschinen wird es trotz UB funktionieren.
Peter Cordes
6
@Adam: Schließen, aber dies ist tatsächlich UB (es sei denn, der vom OP verwendete Compiler GCC definiert es. Es könnte sein). Aber UB bedeutet nicht "definitiv explodiert"; Eines der möglichen Verhaltensweisen für UB ist, wie Sie es erwartet haben !! Das macht UB so böse; Es kann direkt in einem Debug-Build funktionieren und bei aktivierter Optimierung fehlschlagen oder umgekehrt oder je nach umgebendem Code unterbrochen werden. Wenn Sie andere Zeiger vergleichen, erhalten Sie immer noch eine Antwort, aber die Sprache definiert nicht, was diese Antwort bedeutet (wenn überhaupt). Nein, Abstürze sind erlaubt. Es ist wirklich UB.
Peter Cordes
3
@Adam: Oh ja, vergiss den ersten Teil meines Kommentars, ich habe deinen falsch verstanden. Sie behaupten jedoch, dass der Vergleich anderer Zeiger Ihnen immer noch eine Antwort gibt . Das ist nicht wahr. Das wäre ein nicht spezifiziertes Ergebnis , keine vollständige UB. UB ist viel schlechter und bedeutet, dass Ihr Programm fehlerhaft oder SIGILL sein könnte, wenn die Ausführung diese Anweisung mit diesen Eingaben erreicht (zu jedem Zeitpunkt davor oder danach). (Nur plausibel auf x86-64, wenn die UB zur Kompilierungszeit sichtbar ist, aber im Allgemeinen kann alles passieren.) Ein Teil der Aufgabe von UB besteht darin, den Compiler beim Generieren von asm "unsichere" Annahmen treffen zu lassen.
Peter Cordes

Antworten:

33

Gemäß der C11 - Standard werden die relationalen Operatoren <, <=, >, und >=nur von Zeigern auf Elemente des gleichen Array oder struct Objekt verwendet werden kann. Dies ist in Abschnitt 6.5.8p5 beschrieben:

Wenn zwei Zeiger verglichen werden, hängt das Ergebnis von den relativen Positionen im Adressraum der Objekte ab, auf die gezeigt wird. Wenn zwei Zeiger auf Objekttypen beide auf dasselbe Objekt zeigen oder beide auf einen nach dem letzten Element desselben Array-Objekts zeigen, werden sie gleich verglichen. Wenn die Objekte, auf die verwiesen wird, Mitglieder desselben Aggregatobjekts sind, werden Zeiger auf später deklarierte Strukturelemente größer als Zeiger auf zuvor in der Struktur deklarierte Elemente und Zeiger auf Array-Elemente mit größeren tiefgestellten Werten verglichen als Zeiger auf Elemente desselben Arrays mit niedrigeren tiefgestellten Werten. Alle Zeiger auf Mitglieder desselben Gewerkschaftsobjekts sind gleich.

Beachten Sie, dass Vergleiche, die diese Anforderung nicht erfüllen, undefiniertes Verhalten hervorrufen , was (unter anderem) bedeutet, dass Sie sich nicht darauf verlassen können, dass die Ergebnisse wiederholbar sind.

In Ihrem speziellen Fall schien die Operation sowohl für den Vergleich zwischen den Adressen zweier lokaler Variablen als auch zwischen der Adresse einer lokalen und einer dynamischen Adresse "zu funktionieren". Das Ergebnis könnte sich jedoch ändern, indem Sie eine scheinbar nicht verwandte Änderung an Ihrem Code vornehmen oder sogar denselben Code mit unterschiedlichen Optimierungseinstellungen kompilieren. Mit undefiniertem Verhalten, nur weil der Code könnte einen Fehler zum Absturz bringen oder erzeugen bedeutet nicht , es wird .

Beispielsweise verfügt ein x86-Prozessor, der im 8086-Realmodus ausgeführt wird, über ein segmentiertes Speichermodell, das ein 16-Bit-Segment und einen 16-Bit-Offset zum Erstellen einer 20-Bit-Adresse verwendet. In diesem Fall wird eine Adresse also nicht genau in eine Ganzzahl konvertiert.

Die Gleichheitsoperatoren ==und !=jedoch nicht über diese Einschränkung haben. Sie können zwischen zwei beliebigen Zeigern auf kompatible Typen oder NULL-Zeiger verwendet werden. Die Verwendung von ==oder !=in beiden Beispielen würde also einen gültigen C-Code erzeugen.

Selbst mit ==und !=könnten Sie jedoch einige unerwartete, aber immer noch genau definierte Ergebnisse erzielen. Siehe Kann ein Gleichheitsvergleich nicht verwandter Zeiger als wahr bewertet werden? Weitere Details hierzu.

In Bezug auf die Prüfungsfrage Ihres Professors werden einige fehlerhafte Annahmen getroffen:

  • Es gibt ein flaches Speichermodell, bei dem eine 1: 1-Entsprechung zwischen einer Adresse und einem ganzzahligen Wert besteht.
  • Dass die konvertierten Zeigerwerte in einen Integer-Typ passen.
  • Dass die Implementierung Zeiger einfach als Ganzzahlen behandelt, wenn Vergleiche durchgeführt werden, ohne die durch undefiniertes Verhalten gegebene Freiheit auszunutzen.
  • Dass ein Stapel verwendet wird und dass lokale Variablen dort gespeichert sind.
  • Dass ein Heap verwendet wird, um zugewiesenen Speicher abzurufen.
  • Dass der Stapel (und damit lokale Variablen) an einer höheren Adresse als der Heap (und damit zugewiesene Objekte) erscheint.
  • Diese String-Konstanten erscheinen an einer niedrigeren Adresse als der Heap.

Wenn Sie diesen Code auf einer Architektur und / oder mit einem Compiler ausführen, der diese Annahmen nicht erfüllt, können Sie sehr unterschiedliche Ergebnisse erzielen.

Außerdem zeigen beide Beispiele beim Aufrufen auch ein undefiniertes Verhalten strcpy, da der rechte Operand (in einigen Fällen) auf ein einzelnes Zeichen und nicht auf eine nullterminierte Zeichenfolge verweist, was dazu führt, dass die Funktion über die Grenzen der angegebenen Variablen hinaus liest.

dbush
quelle
3
@ Shisui Auch wenn Sie sich nicht auf die Ergebnisse verlassen sollten. Compiler können bei der Optimierung sehr aggressiv werden und nutzen undefiniertes Verhalten als Gelegenheit dazu. Es ist möglich, dass die Verwendung eines anderen Compilers und / oder anderer Optimierungseinstellungen unterschiedliche Ausgaben erzeugen kann.
dbush
2
@ Shisui: Es wird im Allgemeinen auf Computern mit einem flachen Speichermodell wie x86-64 funktionieren. Einige Compiler für solche Systeme definieren möglicherweise sogar das Verhalten in ihrer Dokumentation. Wenn nicht, kann aufgrund der zur Kompilierungszeit sichtbaren UB ein "verrücktes" Verhalten auftreten. (In der Praxis glaube ich nicht, dass irgendjemand das will, also ist es nicht etwas, wonach Mainstream-Compiler suchen und "versuchen zu brechen".)
Peter Cordes
1
Wenn ein Compiler sieht, dass ein Ausführungspfad <zwischen dem mallocErgebnis und einer lokalen Variablen (automatischer Speicher, dh Stapel) führen würde, könnte er davon ausgehen, dass der Ausführungspfad niemals verwendet wird, und nur die gesamte Funktion zu einer ud2Anweisung kompilieren (was einen unzulässigen Wert auslöst) -Anweisung Ausnahme, die der Kernel behandelt, indem er ein SIGILL an den Prozess liefert). GCC / Clang tun dies in der Praxis für andere Arten von UB, z. B. das Abfallen vom Ende einer Nichtfunktion void. godbolt.org ist momentan nicht verfügbar , aber versuchen Sie es mit Kopieren / Einfügen int foo(){int x=2;}und beachten Sie das Fehlen einesret
Peter Cordes
4
@ Shisui: TL: DR: Es ist kein portables C, obwohl es unter x86-64 Linux einwandfrei funktioniert. Annahmen über die Ergebnisse des Vergleichs zu treffen, ist jedoch einfach verrückt. Wenn Sie nicht im Hauptthread sind, wurde Ihr Threadstapel mithilfe desselben Mechanismus dynamisch zugewiesen malloc, um mehr Speicher vom Betriebssystem abzurufen. Es besteht also kein Grund anzunehmen, dass Ihre lokalen Variablen (Threadstapel) über der mallocdynamischen Zuweisung liegen Lager.
Peter Cordes
2
@PeterCordes: Es ist erforderlich, verschiedene Aspekte des Verhaltens als "optional definiert" zu erkennen, sodass Implementierungen sie nach Belieben definieren können oder nicht, aber auf testbare Weise angeben müssen (z. B. vordefiniertes Makro), wenn sie dies nicht tun. Anstatt zu charakterisieren, dass jede Situation, in der die Auswirkungen einer Optimierung als "undefiniertes Verhalten" beobachtbar wären, wäre es weitaus nützlicher zu sagen, dass Optimierer bestimmte Aspekte des Verhaltens als "nicht beobachtbar" betrachten könnten, wenn sie dies anzeigen tun Sie dies. Zum Beispiel int x,y;eine Implementierung gegeben ...
Supercat
12

Das Hauptproblem beim Vergleichen von Zeigern mit zwei unterschiedlichen Arrays desselben Typs besteht darin, dass die Arrays selbst nicht an einer bestimmten relativen Position platziert werden müssen - eines könnte vor und nach dem anderen enden.

Zunächst dachte ich, ich würde undefiniert oder einen Typ oder Fehler bekommen, weil pt und px nicht auf dasselbe Array zeigen (zumindest nach meinem Verständnis).

Nein, das Ergebnis hängt von der Implementierung und anderen unvorhersehbaren Faktoren ab.

Ist auch pt> px, weil beide Zeiger auf Variablen zeigen, die auf dem Stapel gespeichert sind, und der Stapel nach unten wächst, so dass die Speicheradresse von t größer als die von x ist? Welches ist, warum pt> px wahr ist?

Es gibt nicht unbedingt einen Stapel . Wenn es existiert, muss es nicht nachwachsen. Es könnte erwachsen werden. Es könnte auf bizarre Weise nicht zusammenhängend sein.

Darüber hinaus denke ich, dass die Zeigerarithmetik zwischen zwei Zeigern in Ordnung ist, unabhängig davon, wo sie einzeln zeigen, da die Arithmetik nur die Speicheradressen verwendet, die die Zeiger speichern.

Schauen wir uns die C-Spezifikation an , §6.5.8 auf Seite 85, in der relationale Operatoren (dh die von Ihnen verwendeten Vergleichsoperatoren) erläutert werden. Beachten Sie, dass dies nicht für direkte !=oder ==Vergleich gilt.

Wenn zwei Zeiger verglichen werden, hängt das Ergebnis von den relativen Positionen im Adressraum der Objekte ab, auf die gezeigt wird. ... Wenn die Objekte, auf die verwiesen wird, Mitglieder desselben Aggregatobjekts sind, ... vergleichen Zeiger auf Array-Elemente mit größeren Indexwerten mehr als Zeiger auf Elemente desselben Arrays mit niedrigeren Indexwerten.

In allen anderen Fällen ist das Verhalten undefiniert.

Der letzte Satz ist wichtig. Während ich einige nicht verwandte Fälle reduziert habe, um Platz zu sparen, ist uns ein Fall wichtig: zwei Arrays, die nicht Teil desselben Struktur- / Aggregatobjekts 1 sind , und wir vergleichen Zeiger mit diesen beiden Arrays. Dies ist undefiniertes Verhalten .

Während Ihr Compiler gerade eine Art CMP-Maschinenanweisung (Vergleichsanweisung) eingefügt hat, die die Zeiger numerisch vergleicht, und Sie hier Glück hatten, ist UB ein ziemlich gefährliches Tier. Es kann buchstäblich alles passieren - Ihr Compiler könnte die gesamte Funktion einschließlich sichtbarer Nebenwirkungen optimieren. Es könnte Nasendämonen hervorbringen.

1 Zeiger auf zwei verschiedene Arrays, die Teil derselben Struktur sind, können verglichen werden, da dies unter die Klausel fällt, in der die beiden Arrays Teil desselben Aggregatobjekts (der Struktur) sind.

Nanofarad
quelle
1
Noch wichtiger ist , mit tund xin der gleichen Funktion definiert ist, gibt es null Grund , etwas darüber , wie ein Compiler Targeting x86-64 Einheimischer für diese Funktion in dem Stapelrahmen wird das Layout zu übernehmen. Der nach unten wachsende Stapel hat nichts mit der Deklarationsreihenfolge von Variablen in einer Funktion zu tun. Selbst in getrennten Funktionen könnten sich die Einheimischen der "Kind" -Funktion mit den Eltern vermischen, wenn sich eines in das andere einfügen könnte.
Peter Cordes
1
Ihr Compiler könnte die gesamte Funktion einschließlich sichtbarer Nebenwirkungen optimieren. Keine Übertreibung: Für andere Arten von UB (wie das Abfallen vom Ende einer Nichtfunktion void) tun g ++ und clang ++ dies in der Praxis wirklich: godbolt.org/z/g5vesB sie Nehmen Sie an, dass der Ausführungspfad nicht genommen wird, weil er zu UB führt, und kompilieren Sie solche Basisblöcke zu einer unzulässigen Anweisung. Oder zu keinerlei Anweisungen, nur stillschweigend zu dem nächsten Asm durchzufallen, falls diese Funktion jemals aufgerufen wurde. (Aus irgendeinem Grund gccnicht nur g++).
Peter Cordes
6

Dann fragte was

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Bewerten zu. Die Antwort lautet 0, 1 und 0.

Diese Fragen reduzieren sich auf:

  1. Befindet sich der Haufen über oder unter dem Stapel?
  2. Befindet sich der Heap über oder unter dem Zeichenfolgenliteralabschnitt des Programms?
  3. das gleiche wie [1].

Und die Antwort auf alle drei lautet "Implementierung definiert". Die Fragen Ihres Profis sind falsch. Sie haben es in traditionellem Unix-Layout basiert:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

Einige moderne Einheiten (und alternative Systeme) entsprechen jedoch nicht diesen Traditionen. Es sei denn, sie haben der Frage "ab 1992" vorangestellt; Stellen Sie sicher, dass Sie bei der Bewertung eine -1 angeben.

mevets
quelle
3
Keine Implementierung definiert, undefiniert! Stellen Sie sich das so vor: Ersteres kann zwischen den Implementierungen variieren, aber die Implementierungen sollten dokumentieren, wie über das Verhalten entschieden wird. Letzteres bedeutet, dass das Verhalten in irgendeiner Weise variieren kann und die Implementierung Ihnen nicht sagen muss, dass Sie in die Hocke gehen :-)
paxdiablo
1
@paxdiablo: Laut Begründung der Autoren des Standards "identifiziert undefiniertes Verhalten ... auch Bereiche möglicher konformer Spracherweiterung: Der Implementierer kann die Sprache erweitern, indem er eine Definition des offiziell undefinierten Verhaltens bereitstellt." In der Begründung heißt es weiter: "Das Ziel ist es, dem Programmierer die Chance zu geben, leistungsstarke C-Programme zu erstellen, die auch sehr portabel sind, ohne die vollkommen nützlichen C-Programme zu beeinträchtigen, die zufällig nicht portabel sind, daher das Adverb streng." Kommerzielle Compiler-Autoren verstehen dies, einige andere Compiler-Autoren jedoch nicht.
Supercat
Es gibt einen anderen implementierungsdefinierten Aspekt. Der Zeigervergleich ist signiert . Abhängig von der Maschine / dem Betriebssystem / dem Compiler können einige Adressen als negativ interpretiert werden. Beispielsweise würde eine 32-Bit-Maschine, die den Stapel auf 0xc << 28 platziert, die automatischen Variablen wahrscheinlich an einer Leasinggeberadresse als dem Heap oder den Rodata anzeigen.
Mevets
1
@mevets: Gibt der Standard eine Situation an, in der die Signatur von Zeigern in Vergleichen beobachtbar wäre? Ich würde erwarten, dass, wenn eine 16-Bit-Plattform Objekte mit mehr als 32768 Bytes zulässt und arr[]ein solches Objekt ist, der Standard einen Vergleich vorschreibt , der arr+32768größer ist als arrselbst wenn ein Vergleich mit signierten Zeigern etwas anderes melden würde.
Supercat
Ich weiß es nicht; Der C-Standard umkreist Dantes neunten Kreis und betet für Sterbehilfe. Das OP verwies speziell auf K & R und eine Prüfungsfrage. #UB ist Trümmer einer faulen Arbeitsgruppe.
Mevets
1

Auf fast jeder fernmodernen Plattform haben Zeiger und Ganzzahlen eine isomorphe Ordnungsbeziehung, und Zeiger auf disjunkte Objekte werden nicht verschachtelt. Die meisten Compiler stellen diese Reihenfolge Programmierern zur Verfügung, wenn Optimierungen deaktiviert sind. Der Standard unterscheidet jedoch nicht zwischen Plattformen mit einer solchen Reihenfolge und solchen, die nicht erfordern , dass Implementierungen dem Programmierer eine solche Reihenfolge auch auf Plattformen offenlegen, die dies tun würden definiere es. Folglich führen einige Compiler-Writer verschiedene Arten von Optimierungen und "Optimierungen" durch, basierend auf der Annahme, dass Code niemals relationale Operatoren für Zeiger auf verschiedene Objekte vergleicht.

Gemäß der veröffentlichten Begründung beabsichtigten die Autoren des Standards, dass Implementierungen die Sprache erweitern, indem sie angeben, wie sie sich in Situationen verhalten, die der Standard als "undefiniertes Verhalten" charakterisiert (dh wenn der Standard keine Anforderungen stellt ), wenn dies nützlich und praktisch wäre Einige Compiler-Autoren gehen jedoch eher davon aus, dass Programme niemals versuchen werden, von etwas zu profitieren, das über die Standardmandate hinausgeht, als es Programmen zu ermöglichen, Verhaltensweisen, die die Plattformen unterstützen könnten, ohne zusätzliche Kosten sinnvoll auszunutzen.

Mir sind keine kommerziell entworfenen Compiler bekannt, die mit Zeigervergleichen etwas Seltsames anfangen, aber wenn Compiler für ihr Back-End auf das nichtkommerzielle LLVM umsteigen, verarbeiten sie zunehmend unsinnigen Code, dessen Verhalten zuvor angegeben wurde Compiler für ihre Plattformen. Ein solches Verhalten ist nicht auf relationale Operatoren beschränkt, sondern kann sogar die Gleichheit / Ungleichheit beeinflussen. Obwohl der Standard festlegt, dass ein Vergleich zwischen einem Zeiger auf ein Objekt und einem "gerade vergangenen" Zeiger auf ein unmittelbar vorhergehendes Objekt gleich ist, neigen gcc- und LLVM-basierte Compiler dazu, unsinnigen Code zu generieren, wenn Programme einen solchen ausführen Vergleiche.

Betrachten Sie als Beispiel für eine Situation, in der sich selbst ein Gleichheitsvergleich in gcc und clang unsinnig verhält, Folgendes:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Sowohl clang als auch gcc generieren Code, der immer 4 zurückgibt, selbst wenn xes sich um zehn Elemente handelt, yunmittelbar darauf folgt und iNull ist, was dazu führt, dass der Vergleich wahr ist und p[0]mit dem Wert 1 geschrieben wird. Ich denke, was passiert, ist, dass ein Durchgang der Optimierung neu geschrieben wird die Funktion als ob *p = 1;durch ersetzt worden wäre x[10] = 1;. Der letztere Code wäre äquivalent, wenn der Compiler *(x+10)als äquivalent zu interpretiert würde *(y+i), aber leider erkennt eine nachgeschaltete Optimierungsstufe, dass ein Zugriff auf x[10]nur definiert werden würde, wenn xmindestens 11 Elemente vorhanden wären, was es unmöglich machen würde, dass dieser Zugriff Auswirkungen hat y.

Wenn Compiler dieses "Kreativ" mit Zeigergleichheitsszenario erhalten können, das vom Standard beschrieben wird, würde ich ihnen nicht vertrauen, dass sie nicht noch kreativer werden, wenn der Standard keine Anforderungen stellt.

Superkatze
quelle
0

Es ist ganz einfach: Der Vergleich von Zeigern ist nicht sinnvoll, da die Speicherorte für Objekte niemals in der Reihenfolge garantiert werden, in der Sie sie deklariert haben. Die Ausnahme sind Arrays. & array [0] ist niedriger als & array [1]. Darauf weist K & R hin. In der Praxis sind die Adressen der Strukturmitglieder auch in der Reihenfolge, in der Sie sie meiner Erfahrung nach deklarieren. Keine Garantie dafür .... Eine weitere Ausnahme ist, wenn Sie einen Zeiger für gleich vergleichen. Wenn ein Zeiger einem anderen entspricht, wissen Sie, dass er auf dasselbe Objekt zeigt. Was auch immer es ist. Schlechte Prüfungsfrage, wenn Sie mich fragen. Abhängig von Ubuntu Linux 16.04, 64-Bit-Version Programmierumgebung für eine Prüfungsfrage? Ja wirklich ?

Hans Lepoeter
quelle
Technisch - Arrays ist nicht wirklich eine Ausnahme , da Sie erklären nicht arr[0], arr[1]usw. getrennt. Sie deklarieren arrals Ganzes, sodass die Reihenfolge der einzelnen Array-Elemente ein anderes Problem darstellt als in dieser Frage beschrieben.
Paxdiablo
1
Es wird garantiert, dass Strukturelemente in Ordnung sind, was garantiert, dass man memcpyeinen zusammenhängenden Teil einer Struktur kopieren und alle darin enthaltenen Elemente und nichts anderes beeinflussen kann. Der Standard ist schlampig in Bezug auf die Terminologie, welche Arten von Zeigerarithmetik mit Strukturen oder malloc()zugewiesenem Speicher durchgeführt werden können. Das offsetofMakro wäre ziemlich nutzlos, wenn man nicht die gleiche Art von Zeigerarithmetik mit den Bytes einer Struktur wie mit a verwenden könnte char[], aber der Standard sagt nicht ausdrücklich, dass die Bytes einer Struktur sind (oder verwendet werden können als) ein Array-Objekt.
Supercat
-4

Was für eine provokative Frage!

Selbst das flüchtige Scannen der Antworten und Kommentare in diesem Thread zeigt, wie emotional Ihre scheinbar einfache und unkomplizierte Abfrage ist.

Es sollte nicht überraschen.

Inarguably, Missverständnisse rund um das Konzept und die Verwendung von Zeigern stellen eine vorherrschende Ursache von schweren Fehlern in allgemein Programmierung.

Das Erkennen dieser Realität zeigt sich leicht in der Allgegenwart von Sprachen, die speziell dafür entwickelt wurden, die Herausforderungen, die Zeiger insgesamt mit sich bringen , anzugehen und vorzugsweise zu vermeiden . Denken Sie an C ++ und andere Ableitungen von C, Java und seinen Beziehungen, Python und anderen Skripten - lediglich als die bekannteren und am weitesten verbreiteten und mehr oder weniger geordneten Schweregrade bei der Behandlung des Problems.

Die Entwicklung eines tieferen Verständnisses der zugrunde liegenden Prinzipien muss daher für jeden Einzelnen relevant sein , der eine hervorragende Programmierung anstrebt - insbesondere auf Systemebene .

Ich stelle mir vor, genau das will Ihr Lehrer demonstrieren.

Und die Natur von C macht es zu einem bequemen Fahrzeug für diese Erkundung. Weniger klar als Assemblierung - obwohl vielleicht leichter verständlich - und dennoch weitaus expliziter als Sprachen, die auf einer tieferen Abstraktion der Ausführungsumgebung basieren.

C ist eine Sprache auf Systemebene , die die deterministische Übersetzung der Absicht des Programmierers in Anweisungen erleichtert , die Maschinen verstehen können . Obwohl es als hochrangig eingestuft ist, gehört es tatsächlich zu einer „mittleren“ Kategorie. Da es jedoch keine solche gibt, muss die Bezeichnung "System" ausreichen.

Diese Eigenschaft ist maßgeblich dafür verantwortlich, dass es eine bevorzugte Sprache für Gerätetreiber , Betriebssystemcode und eingebettete Implementierungen ist. Darüber hinaus eine zu Recht bevorzugte Alternative bei Anwendungen, bei denen eine optimale Effizienz von größter Bedeutung ist. wo das den Unterschied zwischen Überleben und Aussterben bedeutet und daher eine Notwendigkeit im Gegensatz zu einem Luxus ist. In solchen Fällen verliert der attraktive Komfort der Portabilität seinen Reiz, und die Entscheidung für die mangelhafte Leistung des kleinsten gemeinsamen Nenners wird zu einer undenkbar nachteiligen Option.

Was macht C - und einige seiner Derivate - ganz speziell ist, dass es erlaubt seinen Benutzern vollständige Kontrolle - wenn das ist , was sie sich wünschen - ohne Auferlegung der damit verbundenen Aufgaben auf sie , wenn sie es nicht tun. Dennoch bietet es nie mehr als die dünnste von Isolierungen aus der Maschine , weshalb die ordnungsgemäße Verwendung erfordert anspruchsvolles Verständnis des Begriffs des Zeigers .

Im Wesentlichen ist die Antwort auf Ihre Frage sehr einfach und befriedigend süß - zur Bestätigung Ihres Verdachts. Vorausgesetzt jedoch, man misst jedem Konzept in dieser Aussage die erforderliche Bedeutung bei :

  • Das Untersuchen, Vergleichen und Manipulieren von Zeigern ist immer und notwendigerweise gültig, während die aus dem Ergebnis abgeleiteten Schlussfolgerungen von der Gültigkeit der enthaltenen Werte abhängen und daher nicht gültig sein müssen.

Ersteres ist sowohl stets sichere und potentiell richtige , während die letzteren kann je nur sein richtige , wenn es wurde festgelegt als sicher . Überraschenderweise hängt die Feststellung der Gültigkeit des letzteren von einigen ab und verlangt dies .

Ein Teil der Verwirrung ergibt sich natürlich aus der Auswirkung der Rekursion, die dem Prinzip eines Zeigers innewohnt - und den Herausforderungen bei der Unterscheidung von Inhalten und Adressen.

Sie haben ganz richtig vermutet,

Ich werde zu dem Gedanken gebracht, dass jeder Zeiger mit jedem anderen Zeiger verglichen werden kann, unabhängig davon, wohin er einzeln zeigt. Darüber hinaus denke ich, dass die Zeigerarithmetik zwischen zwei Zeigern in Ordnung ist, unabhängig davon, wo sie einzeln zeigen, da die Arithmetik nur die Speicheradressen verwendet, die die Zeiger speichern.

Und mehrere Mitwirkende haben bestätigt: Zeiger sind nur Zahlen. Manchmal etwas näher an komplexen Zahlen, aber immer noch nicht mehr als Zahlen.

Die amüsante Schärfe, in der diese Behauptung hier aufgenommen wurde, offenbart mehr über die menschliche Natur als über die Programmierung, bleibt jedoch bemerkenswert und ausführlich. Vielleicht machen wir das später ...

Wie ein Kommentar andeutet; All diese Verwirrung und Bestürzung ergibt sich aus der Notwendigkeit, zu unterscheiden, was gültig ist und was sicher ist , aber das ist eine übermäßige Vereinfachung. Wir müssen auch unterscheiden, was funktional und was zuverlässig ist , was praktisch ist und was richtig sein kann und noch weiter: was unter bestimmten Umständen richtig ist und was im allgemeineren Sinne richtig sein kann . Ganz zu schweigen von; der Unterschied zwischen Konformität und Anstand .

Zu diesem Zweck müssen wir zunächst genau wissen , was ein Zeiger ist .

  • Sie haben das Konzept fest im Griff und mögen diese Illustrationen wie einige andere als herablassend simpel empfinden, aber das hier erkennbare Maß an Verwirrung erfordert eine solche Einfachheit bei der Klärung.

Wie mehrere darauf hingewiesen haben: Der Begriff Zeiger ist lediglich ein spezieller Name für einen Index und somit nichts weiter als eine andere Zahl .

Dies sollte angesichts der Tatsache, dass alle modernen Mainstream-Computer Binärmaschinen sind , die notwendigerweise ausschließlich mit und auf Zahlen arbeiten, bereits selbstverständlich sein . Quantum Computing mag das ändern, aber das ist höchst unwahrscheinlich und nicht erwachsen geworden .

Wie Sie bereits bemerkt haben, sind Zeiger technisch gesehen genauere Adressen . Eine offensichtliche Einsicht, die natürlich die lohnende Analogie einführt, sie mit den „Adressen“ von Häusern oder Grundstücken auf einer Straße zu korrelieren.

  • In einem flachen Speichermodell: Der gesamte Systemspeicher ist in einer einzigen linearen Reihenfolge organisiert: Alle Häuser in der Stadt liegen auf derselben Straße, und jedes Haus wird allein durch seine Nummer eindeutig identifiziert. Herrlich einfach.

  • In segmentierten Schemata wird eine hierarchische Organisation von nummerierten Straßen über der von nummerierten Häusern eingeführt, so dass zusammengesetzte Adressen erforderlich sind.

    • Einige Implementierungen sind noch komplizierter, und die Gesamtheit der verschiedenen "Straßen" muss sich nicht zu einer zusammenhängenden Sequenz summieren, aber nichts davon ändert etwas am Basiswert.
    • Wir sind notwendigerweise in der Lage, jede solche hierarchische Verknüpfung wieder in eine flache Organisation zu zerlegen. Je komplexer die Organisation ist, desto mehr Reifen müssen wir durchspringen, um dies zu tun, aber es muss möglich sein. Dies gilt in der Tat auch für den "Real-Modus" auf x86.
    • Ansonsten ist die Zuordnung von Links zu anderen Adressen wäre nicht bijektiv , als zuverlässige Ausführung - auf Systemebene - Anforderungen , dass es muss sein.
      • Mehrere Adressen dürfen nicht einzelnen Speicherorten zugeordnet werden
      • Singularadressen dürfen niemals mehreren Speicherorten zugeordnet werden.

Bringen Sie uns zu der weiteren Wendung , die das Rätsel in ein so faszinierend kompliziertes Gewirr verwandelt . Oben war es zweckmäßig, der Einfachheit und Klarheit halber vorzuschlagen, dass Zeiger Adressen sind . Das ist natürlich nicht richtig. Ein Zeiger ist keine Adresse; Ein Zeiger ist eine Referenz auf eine Adresse , er enthält eine Adresse . Wie der Umschlag trägt ein Hinweis auf das Haus. Wenn Sie darüber nachdenken, können Sie einen Blick darauf werfen, was mit dem im Konzept enthaltenen Vorschlag der Rekursion gemeint war. Immer noch; Wir haben nur so viele Wörter und sprechen über die Adressen von Verweisen auf Adressenund so blockiert bald die meisten Gehirne bei einer ungültigen Op-Code-Ausnahme . Und zum größten Teil wird die Absicht leicht aus dem Kontext gewonnen, also kehren wir auf die Straße zurück.

Postangestellte in unserer imaginären Stadt ähneln denen, die wir in der "realen" Welt finden. Es ist wahrscheinlich, dass niemand einen Schlaganfall erleidet, wenn Sie über eine ungültige Adresse sprechen oder sich erkundigen , aber jeder letzte wird zurückschrecken, wenn Sie ihn bitten , auf diese Informationen zu reagieren .

Angenommen, es gibt nur 20 Häuser in unserer einzigartigen Straße. Stellen Sie sich weiter vor, eine fehlgeleitete oder legasthene Seele habe einen sehr wichtigen Brief an Nummer 71 gerichtet. Jetzt können wir unseren Spediteur Frank fragen, ob es eine solche Adresse gibt, und er wird einfach und ruhig berichten: Nein . Wir können auch erwarten , dass er , wie weit außerhalb der Straße schätzen diese Stelle würde lügen , wenn es tat exist: etwa 2,5 - mal weiter als das Ende. Nichts davon wird ihn ärgern. Allerdings , wenn wir ihn fragen würden , zu liefern , diesen Brief, oder holen von diesem Ort ein Element, ist er wahrscheinlich ganz offen über seine sein Unmut und Ablehnung zu erfüllen.

Zeiger sind nur Adressen und Adressen sind nur Zahlen.

Überprüfen Sie die Ausgabe von Folgendem:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

Rufen Sie so viele Zeiger auf, wie Sie möchten, gültig oder nicht. Bitte veröffentlichen Sie Ihre Ergebnisse, wenn dies auf Ihrer Plattform fehlschlägt oder Ihr (zeitgemäßer) Compiler sich beschwert.

Nun, da Zeiger sind nur Zahlen, ist es zwangsläufig gültig , sie zu vergleichen. In gewisser Hinsicht ist es genau das, was Ihr Lehrer demonstriert. Alle folgenden Aussagen sind absolut gültig - und richtig! - C und wird beim Kompilieren ohne Probleme ausgeführt , obwohl keiner der Zeiger initialisiert werden muss und die darin enthaltenen Werte möglicherweise undefiniert sind :

  • Wir berechnen nur aus Gründen der Klarheit result explizit und drucken es aus, um den Compiler zu zwingen, den ansonsten redundanten, toten Code zu berechnen.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Natürlich ist das Programm schlecht geformt, wenn entweder a oder b zum Zeitpunkt des Tests undefiniert (sprich: nicht richtig initialisiert ) sind, aber das ist für diesen Teil unserer Diskussion völlig irrelevant . Diese Schnipsel, wie auch die folgenden Aussagen sind garantiert - von der ‚Standard‘ - kompilieren und laufen einwandfrei, trotz der IN -validity jeder Zeiger beteiligt.

Probleme treten nur auf, wenn ein ungültiger Zeiger dereferenziert wird . Wenn wir Frank bitten, an der ungültigen, nicht vorhandenen Adresse abzuholen oder zu liefern.

Bei einem beliebigen Zeiger:

int *p;

Während diese Anweisung kompiliert und ausgeführt werden muss:

printf(“%p”, p);

... wie muss das:

size_t foo( int *p ) { return (size_t)p; }

... die folgenden beiden werden im krassen Gegensatz dazu immer noch leicht kompiliert, schlagen jedoch bei der Ausführung fehl, es sei denn, der Zeiger ist gültig - womit wir hier lediglich meinen, dass er auf eine Adresse verweist, auf die der vorliegenden Anmeldung Zugriff gewährt wurde :

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Wie subtil die Veränderung? Die Unterscheidung liegt in der Differenz zwischen dem Wert des Zeigers - das ist die Adresse - und dem Wert des Inhalts: des Hauses unter dieser Nummer. Es tritt kein Problem auf, bis der Zeiger dereferenziert wird . bis versucht wird, auf die Adresse zuzugreifen, mit der es verknüpft ist. Beim Versuch, das Paket über den Straßenabschnitt hinaus zu liefern oder abzuholen ...

Im weiteren Sinne gilt das gleiche Prinzip notwendigerweise für komplexere Beispiele, einschließlich der oben genannten Notwendigkeit , die erforderliche Gültigkeit festzustellen :

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

Relationaler Vergleich und Arithmetik bieten den gleichen Nutzen wie das Testen der Äquivalenz und sind im Prinzip gleichwertig. Allerdings , was die Ergebnisse dieser Berechnung würde bedeuten , ist eine andere Sache ganz - und genau das Problem behoben , indem die Notierungen Sie enthalten.

In C ist ein Array ein zusammenhängender Puffer, eine ununterbrochene lineare Reihe von Speicherstellen. Vergleich und Arithmetik, die auf Zeiger angewendet werden, deren Referenzorte innerhalb einer solchen singulären Reihe natürlich und offensichtlich sowohl in Bezug aufeinander als auch auf dieses 'Array' (das einfach durch die Basis identifiziert wird) von Bedeutung sind. Genau das Gleiche gilt für jeden Block, der durch mallocoder zugewiesen wird sbrk. Da diese Beziehungen implizit sind , kann der Compiler gültige Beziehungen zwischen ihnen herstellen und daher sicher sein, dass Berechnungen die erwarteten Antworten liefern.

Eine ähnliche Gymnastik mit Zeigern durchzuführen, die auf bestimmte Blöcke oder Arrays verweisen, bietet keinen solchen inhärenten und offensichtlichen Nutzen. Dies gilt umso mehr, als jede Beziehung, die zu einem bestimmten Zeitpunkt besteht, durch eine nachfolgende Neuzuweisung ungültig werden kann, bei der sich diese höchstwahrscheinlich ändern oder sogar invertiert werden. In solchen Fällen kann der Compiler nicht die erforderlichen Informationen abrufen, um das Vertrauen herzustellen, das er in der vorherigen Situation hatte.

Sie als Programmierer können jedoch über solche Kenntnisse verfügen! Und in einigen Fällen sind sie verpflichtet, dies auszunutzen.

Es IST daher Umstände , unter denen selbst diese ganz ist VALID und vollkommen richtig.

In der Tat ist, dass genau das, was mallocselbst hat intern zu tun , wenn die Zeit zurückgewonnen Blöcke versuchen kommt Verschmelzung - auf der großen Mehrheit der Architekturen. Gleiches gilt für den Betriebssystem-Allokator, wie er dahinter steht sbrk. wenn offensichtlicher , häufig , auf unterschiedlicheren Einheiten, kritischer - und relevant auch auf Plattformen, auf denen dies mallocmöglicherweise nicht der Fall ist. Und wie viele davon sind nicht in C geschrieben?

Die Gültigkeit, Sicherheit und der Erfolg einer Handlung sind unweigerlich die Folge des Einsichtsniveaus, auf dem sie beruht und angewendet wird.

In den von Ihnen angebotenen Zitaten sprechen Kernighan und Ritchie ein eng verwandtes, aber dennoch getrenntes Problem an. Sie definieren die Einschränkungen der Sprache und erläutern, wie Sie die Funktionen des Compilers nutzen können, um Sie zu schützen, indem Sie zumindest potenziell fehlerhafte Konstrukte erkennen. Sie beschreiben die Längen der Mechanismus in der Lage ist - ausgelegt ist - zu gehen , um Sie in Ihrer Programmieraufgabe zu unterstützen. Der Compiler ist dein Diener, du bist der Meister. Ein weiser Meister ist jedoch einer, der mit den Fähigkeiten seiner verschiedenen Diener bestens vertraut ist.

In diesem Zusammenhang dient undefiniertes Verhalten dazu, auf eine potenzielle Gefahr und die Möglichkeit eines Schadens hinzuweisen. nicht das bevorstehende, irreversible Schicksal oder das Ende der Welt, wie wir sie kennen, zu implizieren. Es bedeutet einfach, dass wir - was den Compiler bedeutet - keine Vermutungen darüber anstellen können, was dieses Ding sein oder darstellen könnte und aus diesem Grund entscheiden wir uns, unsere Hände von der Sache zu waschen. Wir werden nicht für Missgeschicke verantwortlich gemacht, die sich aus der Nutzung oder dem Missbrauch dieser Einrichtung ergeben können .

Tatsächlich heißt es einfach: „Über diesen Punkt hinaus, Cowboy : Sie sind auf sich allein gestellt ..."

Ihr Professor möchte Ihnen die feineren Nuancen demonstrieren .

Beachten Sie, wie sorgfältig sie ihr Beispiel ausgearbeitet haben. und wie spröde es noch ist ist. Indem Sie die Adresse von a, in

p[0].p0 = &a;

Der Compiler wird gezwungen, den tatsächlichen Speicher für die Variable zuzuweisen, anstatt ihn in ein Register zu stellen. Da es sich um eine automatische Variable handelt, hat der Programmierer jedoch keine Kontrolle darüber, wo diese zugewiesen ist, und kann daher keine gültigen Vermutungen darüber anstellen, was darauf folgen würde. Aus diesem Grund a muss der Wert auf Null gesetzt werden, damit der Code wie erwartet funktioniert.

Nur diese Zeile ändern:

char a = 0;

dazu:

char a = 1;  // or ANY other value than 0

bewirkt, dass das Verhalten des Programms undefiniert wird . Zumindest ist die erste Antwort jetzt 1; aber das Problem ist weitaus unheimlicher.

Jetzt lädt der Code zur Katastrophe ein.

Obwohl es immer noch vollkommen gültig ist und sogar dem Standard entspricht , ist es jetzt schlecht geformt und kann, obwohl es sicher kompiliert werden kann, aus verschiedenen Gründen fehlschlagen. Denn jetzt gibt es mehrere Probleme - keine von denen der Compiler ist die Lage , zu erkennen.

strcpybeginnt an der Adresse von aund geht darüber hinaus, um Byte für Byte zu verbrauchen und zu übertragen, bis eine Null auftritt.

Der p1Zeiger wurde auf einen Block von genau 10 Bytes initialisiert .

  • Wenn es azufällig am Ende eines Blocks platziert wird und der Prozess keinen Zugriff auf das Folgende hat, löst der nächste Lesevorgang - von p0 [1] - einen Segfault aus. Dieses Szenario ist auf der x86-Architektur unwahrscheinlich , aber möglich.

  • Wenn das Gebiet jenseits der Adresse a ist zugänglich, wird kein Lesefehler auftreten, aber das Programm noch nicht vor Unglück gerettet.

  • Wenn ein Null - Byte geschieht innerhalb der zehn an der Adresse des Startens auftreten a, es kann noch überleben dann strcpyaufhören wird und zumindest werden wir nicht in eine Schreib Verletzung leiden.

  • Wenn es nicht fehlerhaft ist , falsch zu lesen, aber in dieser Zeitspanne von 10 kein Null-Byte auftritt, strcpywird fortgesetzt und versucht , über den durch zugewiesenen Block hinaus zu schreibenmalloc .

    • Wenn dieser Bereich nicht dem Prozess gehört, sollte der Segfault sofort ausgelöst werden.

    • Die noch katastrophal - und subtile --- Situation entsteht , wenn der folgende Block wird durch das Verfahren im Besitz, denn dann der Fehler nicht erkannt wird, kann kein Signal angehoben werden, und so kann es zu ‚Arbeit‘ ‚erscheint‘ noch , Während andere Daten, die Verwaltungsstrukturen Ihres Allokators oder sogar Code (in bestimmten Betriebsumgebungen) tatsächlich überschrieben werden .

Aus diesem Grund können zeigerbezogene Fehler so schwer zu verfolgen sein . Stellen Sie sich diese Zeilen vor, die tief in Tausenden von Zeilen kompliziert verwandten Codes vergraben sind, den jemand anderes geschrieben hat, und Sie werden angewiesen, sich damit zu beschäftigen.

Dennoch , das Programm muss noch kompilieren, denn es bleibt vollkommen gültig und Standard - konforme C.

Diese Art von Fehlern, kein Standard und kein Compiler können die Unvorsichtigen davor schützen. Ich stelle mir vor, genau das wollen sie dir beibringen.

Paranoide Menschen versuchen ständig, die Natur von C zu ändern , um diese problematischen Möglichkeiten zu beseitigen und uns so vor uns selbst zu retten. aber das ist unaufrichtig . Dies ist die Verantwortung, die wir übernehmen müssen , wenn wir uns dafür entscheiden, die Macht zu verfolgen und die Freiheit zu erlangen, die uns eine direktere und umfassendere Steuerung der Maschine bietet. Promotoren und Verfolger von Perfektion in der Leistung werden niemals weniger akzeptieren.

Die Portabilität und die Allgemeinheit, die sie darstellt, sind eine grundsätzlich getrennte Überlegung und alles , was der Standard ansprechen möchte:

Dieses Dokument legt die Form und stellt die Interpretation von Programmen in der Programmiersprache C. Sein ausgedrückt Zweck ist auf Portabilität zu fördern , Zuverlässigkeit, Wartbarkeit und effiziente Ausführung von C - Sprachprogramme auf einer Vielzahl von Rechensystemen .

Deshalb ist es völlig in Ordnung ist , es zu halten verschieden von der Definition und technischen Spezifikation der Sprache selbst. Im Gegensatz zuwas viele zu glauben scheinen Allgemeinheit ist gegensätzlich zu außergewöhnlichen und beispielhaft .

Schlussfolgern:

  • Das Untersuchen und Manipulieren von Zeigern selbst ist ausnahmslos gültig und oft fruchtbar . Die Interpretation der Ergebnisse kann sinnvoll sein oder auch nicht, aber Unglück wird niemals eingeladen, bis der Zeiger dereferenziert wird . bis versucht wird, auf die mit verknüpfte Adresse zuzugreifen .

Wäre dies nicht wahr, wäre eine Programmierung, wie wir sie kennen - und lieben - nicht möglich gewesen.

Ghii Velte
quelle
3
Diese Antwort ist leider von Natur aus ungültig. Sie können nichts über undefiniertes Verhalten begründen. Der Vergleich muss nicht auf Maschinenebene durchgeführt werden.
Antti Haapala
6
Ghii, eigentlich nein. Wenn Sie sich C11 Anhang J und 6.5.8 ansehen, ist der Vergleich selbst UB. Dereferenzierung ist ein separates Problem.
Paxdiablo
6
Nein, UB kann immer noch schädlich sein, noch bevor ein Zeiger dereferenziert wird. Ein Compiler kann eine Funktion mit UB vollständig in einem einzigen NOP optimieren, obwohl dies offensichtlich das sichtbare Verhalten ändert.
Nanofarad
2
@Ghii, Anhang J (das Bit, das ich erwähnt habe) ist die Liste der Dinge, die undefiniertes Verhalten sind, daher bin ich mir nicht sicher, wie dies Ihr Argument stützt :-) 6.5.8 ruft den Vergleich explizit als UB auf. Für Ihren Kommentar zu Supercat gibt es keinen Vergleich, wenn Sie einen Zeiger drucken. Sie haben also wahrscheinlich Recht, dass er nicht abstürzt. Aber darum hat das OP nicht gebeten. 3.4.3Dies ist auch ein Abschnitt, den Sie sich ansehen sollten: Er definiert UB als Verhalten, "für das diese Internationale Norm keine Anforderungen stellt".
Paxdiablo
3
@GhiiVelte, du sagst immer wieder Dinge, die einfach falsch sind, obwohl dir darauf hingewiesen wird. Ja, das von Ihnen gepostete Snippet muss kompiliert werden, aber Ihre Behauptung, dass es reibungslos funktioniert, ist falsch. Ich schlage vor, dass Sie den Standard tatsächlich lesen , insbesondere (in diesem Fall) C11 6.5.6/9, wobei zu beachten ist, dass das Wort "soll" eine Anforderung anzeigt. L "Wenn zwei Zeiger subtrahiert werden, zeigen beide auf Elemente desselben Array-Objekts oder einen nach dem letzten Element des Array-Objekts ".
Paxdiablo
-5

Zeiger sind nur ganze Zahlen, wie alles andere in einem Computer. Sie können sie absolut mit vergleichen< und> und Ergebnisse erzielen, ohne dass ein Programm abstürzt. Der Standard garantiert jedoch nicht, dass diese Ergebnisse außerhalb von Array-Vergleichen eine Bedeutung haben .

In Ihrem Beispiel für stapelzugewiesene Variablen kann der Compiler diese Variablen Registern oder Stapelspeicheradressen zuweisen und in beliebiger Reihenfolge auswählen. Vergleiche wie <und werden >daher nicht über Compiler oder Architekturen hinweg konsistent sein. Allerdings ==und !=sind nicht so beschränkt, Zeiger Vergleich Gleichheit ist ein gültiger und nützlicher Betrieb.

Nickelpro
quelle
2
Das Wort Stapel erscheint genau null mal in dem C11 - Standard. Und undefiniertes Verhalten bedeutet, dass alles passieren kann (einschließlich Programmabsturz).
Paxdiablo
1
@paxdiablo Habe ich gesagt, dass es so ist?
Nickelpro
2
Sie haben stapelzugeordnete Variablen erwähnt. Der Standard enthält keinen Stapel, sondern nur ein Implementierungsdetail. Das schwerwiegendere Problem bei dieser Antwort ist die Behauptung, dass Sie Zeiger ohne Absturzwahrscheinlichkeit vergleichen können - das ist einfach falsch.
Paxdiablo
1
@nickelpro: Wenn man Code schreiben möchte, der mit den Optimierern in gcc und clang kompatibel ist, muss man durch viele dumme Reifen springen. Beide Optimierer suchen aggressiv nach Möglichkeiten, um Rückschlüsse darauf zu ziehen, auf welche Dinge Zeiger zugreifen, wenn der Standard auf irgendeine Weise verdreht werden kann, um sie zu rechtfertigen (und manchmal auch, wenn dies nicht der Fall ist). Gegeben int x[10],y[10],*p;, wenn Code ausgewertet y[0], dann ausgewertet p>(x+5)und geschrieben, *pohne pin der Zwischenzeit y[0]
Supercat
1
nickelpro, stimme zu, nicht zuzustimmen, aber deine Antwort ist immer noch grundlegend falsch. Ich vergleiche Ihre Herangehensweise mit der der Leute, die (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')stattdessen verwenden, isalpha()weil bei welcher vernünftigen Implementierung diese Charaktere diskontinuierlich wären? Das Fazit ist, dass Sie, selbst wenn keine Implementierung, von der Sie wissen, ein Problem hat, so weit wie möglich nach dem Standard codieren sollten, wenn Sie Wert auf Portabilität legen. Ich schätze das Label "Standards Maven", danke dafür. Ich kann in meinen Lebenslauf
eintragen