it-swarm.com.de

Inwieweit ist es akzeptabel, sich C++ - Zeiger als Speicheradressen vorzustellen?

Als Sie C++ lernten oder zumindest als ich es durch C++ Primer lernte, wurden Zeiger als "Speicheradressen" der Elemente bezeichnet, auf die sie zeigen. Ich frage mich, inwieweit das stimmt.

Haben beispielsweise zwei Elemente *p1 und *p2 die Eigenschaft p2 = p1 + 1 oder p1 = p2 + 1 genau dann, wenn sie im physischen Speicher benachbart sind?

41
user5648283

Sie sollten sich Zeiger als Adressen von virtual memory vorstellen: Moderne Consumer-Betriebssysteme und Laufzeitumgebungen stellen mindestens eine Abstraktionsebene zwischen physischem Speicher und dem, was Sie als Zeigerwert sehen, ein.

Bei Ihrer abschließenden Aussage können Sie diese Annahme nicht treffen, selbst in einem virtuellen Adressraum. Die Zeigerarithmetik ist nur innerhalb zusammenhängender Speicherblöcke wie Arrays gültig. Und obwohl es zulässig ist (sowohl in C als auch in C++), einem Zeiger einen Punkt hinter einem Array (oder einem Skalar) zuzuweisen, ist das Verhalten bei deferencing einem solchen Zeiger undefiniert. Hypothesen über die Nachbarschaft im physischen Gedächtnis im Zusammenhang mit C und C++ sind sinnlos.

32
Bathsheba

Überhaupt nicht.

C++ ist eine Abstraktion über den Code, den Ihr Computer ausführt. Wir sehen diesen Abstraktionsverlust an einigen Stellen (Klassenmitgliedsreferenzen erfordern beispielsweise Speicherplatz), aber im Allgemeinen sind Sie besser dran, wenn Sie zur Abstraktion codieren und sonst nichts.

Zeiger sind Zeiger. Sie zeigen auf Dinge. Werden sie in der Realität als Speicheradressen implementiert? Könnte sein. Sie könnten auch optimiert werden oder (im Fall von Zeigern auf Mitglieder) etwas komplexer sein als eine einfache numerische Adresse.

Wenn Sie sich Zeiger als Ganzzahlen vorstellen, die auf Adressen im Speicher abgebildet werden, vergessen Sie zum Beispiel, dass es undefined ist, um einen Zeiger auf ein Objekt zu halten, das nicht vorhanden ist Dekrementieren Sie einen Zeiger ohne weiteres auf eine beliebige Speicheradresse.

Wie viele Antworten bereits erwähnt haben, sollten sie nicht als Speicheradressen betrachtet werden. Überprüfen Sie diese Antworten und hier , um sie zu verstehen. Adressieren Sie Ihre letzte Aussage 

* p1 und * p2 haben die Eigenschaft p2 = p1 + 1 oder p1 = p2 + 1, wenn und nur dann, wenn sie im physikalischen Speicher benachbart sind

ist nur korrekt, wenn p1 und p2 vom gleichen Typ sind oder auf Typen derselben Größe verweisen.

11
Aiden Deom

Es ist absolut richtig, Zeiger als Speicheradressen zu betrachten. Das sind sie in ALLEN Compilern, mit denen ich gearbeitet habe - für verschiedene Prozessorarchitekturen, die von verschiedenen Compilerherstellern hergestellt werden. 

Der Compiler ist jedoch etwas interessanter, um Ihnen dabei zu helfen, dass normale Speicheradressen [in allen modernen Mainstream-Prozessoren mindestens] Byte-Adressen sind und das Objekt, auf das Ihr Zeiger verweist, möglicherweise nicht genau ein Byte enthält. Wenn wir T* ptr; haben, wird ptr++((char*)ptr) + sizeof(T); oder ptr + n ist ((char*)ptr) + n*sizeof(T). Dies bedeutet auch, dass Ihr p1 == p2 + 1 erfordert, dass p1 und p2 vom gleichen Typ T sind, da der +1 tatsächlich +sizeof(T)*1 ist.

Es gibt EINE Ausnahme zu den obigen "Zeigern sind Speicheradressen", und dies sind Mitgliedsfunktionszeiger. Sie sind "besonders", und ignorieren Sie vorerst einfach, wie sie tatsächlich implementiert werden. Dies reicht aus, um zu sagen, dass sie nicht "nur Speicheradressen" sind.

5
Mats Petersson

Das Betriebssystem stellt Ihrem Programm eine Abstraktion der physischen Maschine zur Verfügung (d. H. Ihr Programm läuft in einer virtuellen Maschine). Daher hat Ihr Programm keinen Zugriff auf eine physische Ressource Ihres Computers, z. B. CPU-Zeit, Arbeitsspeicher usw.; Es muss lediglich das Betriebssystem nach diesen Ressourcen fragen.

Bei Speicher arbeitet Ihr Programm in einem virtuellen Adressraum, der vom Betriebssystem definiert wird. Dieser Adressraum hat mehrere Bereiche, z. B. Stack, Heap, Code usw. Der Wert Ihrer Zeiger repräsentiert Adressen in diesem virtuellen Adressraum. Tatsächlich zeigen 2 Zeiger auf aufeinanderfolgende Adressen auf aufeinanderfolgende Stellen in diesem Adressraum.

Dieser Adressraum wird jedoch vom Betriebssystem in Seiten und Segmente aufgeteilt, die je nach Bedarf in den Speicher ein- und ausgeblendet werden, sodass Ihre Zeiger möglicherweise auf aufeinanderfolgende physische Speicherpositionen verweisen oder nicht. Dies ist zur Laufzeit unmöglich, wenn dies der Fall ist wahr oder nicht. Dies hängt auch von der Richtlinie ab, die das Betriebssystem für Paging und Segmentierung verwendet.

Im Endeffekt sind Zeiger Speicheradressen. Es handelt sich jedoch um Adressen in einem virtuellen Speicherbereich, und es ist Sache des Betriebssystems, zu entscheiden, wie dies dem physischen Speicherplatz zugeordnet wird.

Für Ihr Programm ist dies kein Problem. Ein Grund für diese Abstraktion ist, dass Programme glauben gemacht werden, dass sie die einzigen Benutzer der Maschine sind. Stellen Sie sich den Albtraum vor, den Sie durchmachen müssten, wenn Sie beim Schreiben Ihres Programms den von anderen Prozessen zugewiesenen Speicher berücksichtigen müssten. Sie wissen nicht einmal, welche Prozesse gleichzeitig mit Ihrem ausgeführt werden. Dies ist auch eine gute Technik zur Durchsetzung der Sicherheit: Ihr Prozess kann nicht (zumindest sollte er nicht in der Lage sein) auf den Speicherplatz eines anderen Prozesses zuzugreifen, da er in zwei verschiedenen (virtuellen) Speicherbereichen ausgeführt wird.

5
Paul92

Wie andere Variablen speichert der Zeiger Daten, die eine Speicheradresse sein können, an der andere Daten gespeichert sind. 

Zeiger ist also eine Variable, die eine Adresse hat und eine Adresse enthalten kann. 

Beachten Sie, dass es nicht notwendig ist, dass ein Zeiger immer eine Adresse enthält. Es kann eine Nicht-Adressen-ID/ein Handle usw. enthalten. Daher ist es nicht ratsam, den Zeiger als Adresse zu sagen.


Zu Ihrer zweiten Frage: 

Zeigerarithmetik ist für einen zusammenhängenden Speicherblock gültig. Wenn p2 = p1 + 1 und beide Zeiger vom gleichen Typ sind, zeigen p1 und p2 auf ein zusammenhängendes Stück Speicher. Die Adressen p1 und p2 liegen also nebeneinander.

4
haccks

Ich denke diese Antwort hat die richtige Idee, aber schlechte Terminologie. Was C-Zeiger liefern, ist das genaue Gegenteil von Abstraktion.

Eine Abstraktion bietet ein mentales Modell, das relativ leicht zu verstehen ist und eine Begründung dafür bietet, selbst wenn die Hardware komplexer und schwer verständlich oder schwieriger zu verstehen ist.

C-Zeiger sind das Gegenteil davon. Sie berücksichtigen mögliche Schwierigkeiten der Hardware, auch wenn die reale Hardware oft einfacher und einfacher zu verstehen ist. Sie beschränken Ihre Argumentation auf das, was eine Vereinigung der komplexesten Teile der komplexesten Hardware erlaubt, unabhängig davon, wie einfach die zur Verfügung stehende Hardware tatsächlich ist.

C++ - Zeiger fügen etwas hinzu, das C nicht enthält. Es ermöglicht den Vergleich aller Zeiger desselben Typs auf Reihenfolge, auch wenn sie sich nicht im selben Array befinden. Dies erlaubt ein bisschen mehr ein mentales Modell, auch wenn es nicht perfekt zur Hardware passt. 

4
Jerry Coffin

Wenn der Compiler keine Zeiger herausoptimiert, sind dies Ganzzahlen, die Speicheradressen speichern. Ihre Länge hängt von der Maschine ab, für die der Code erstellt wird, aber sie können normalerweise als Ints behandelt werden.

Sie können das überprüfen, indem Sie die tatsächlich gespeicherte Nummer mit printf() drucken.

Beachten Sie jedoch, dass type *-Zeiger-Inkrementierungs-/Dekrementierungsoperationen von der sizeof(type) ausgeführt werden. Überzeugen Sie sich mit diesem Code (online getestet auf Repl.it):

#include <stdio.h>

int main() {
    volatile int i1 = 1337;
    volatile int i2 = 31337;
    volatile double d1 = 1.337;
    volatile double d2 = 31.337;
    volatile int* pi = &i1;
    volatile double* pd = &d1;
    printf("ints: %d, %d\ndoubles: %f, %f\n", i1, i2, d1, d2);
    printf("0x%X = %d\n", pi, *pi);
    printf("0x%X = %d\n", pi-1, *(pi-1));
    printf("Difference: %d\n",(long)(pi)-(long)(pi-1));
    printf("0x%X = %f\n", pd, *pd);
    printf("0x%X = %f\n", pd-1, *(pd-1));
    printf("Difference: %d\n",(long)(pd)-(long)(pd-1));
}

Alle Variablen und Zeiger wurden für unbeständig erklärt, da der Compiler sie nicht optimieren würde. Beachten Sie auch, dass ich dekrementiert habe, da die Variablen im Funktionsstack abgelegt werden.

Die Ausgabe war:

ints: 1337, 31337
doubles: 1.337000, 31.337000
0xFAFF465C = 1337
0xFAFF4658 = 31337
Difference: 4
0xFAFF4650 = 1.337000
0xFAFF4648 = 31.337000
Difference: 8

Beachten Sie, dass dieser Code möglicherweise nicht auf allen Compilern funktioniert, insbesondere wenn sie keine Variablen in derselben Reihenfolge speichern. Wichtig ist jedoch, dass die Zeigerwerte tatsächlich gelesen und gedruckt werden können und dass Dekrement von einem je nach Größe der Variablen, auf die der Zeiger verweist, dekrementieren kann/wird.

Beachten Sie auch, dass & und * tatsächliche Operatoren für reference ("Speicheradresse dieser Variablen abrufen") und dereference ("Inhalt dieser Speicheradresse abrufen").

Dies kann auch für coole Tricks wie das Abrufen der IEEE 754-Binärwerte für Floats verwendet werden, indem der float* als int* umgewandelt wird:

#include <iostream>

int main() {
    float f = -9.5;
    int* p = (int*)&f;

    std::cout << "Binary contents:\n";
    int i = sizeof(f)*8;
    while(i) {
        i--;
        std::cout << ((*p & (1 << i))?1:0);
   } 
}

Ergebnis ist:

Binary contents:
11000001000110000000000000000000 

Beispiel aus https://pt.wikipedia.org/wiki/IEEE_754 . Schauen Sie sich einen Konverter an.

1
Ronan Paixão

In den Antworten wird nicht erwähnt, dass eine bestimmte Familie von Zeigern, also Zeiger auf Mitglieder, erwähnt wird. Das sind sicherlich nicht Speicheradressen.

1
SergeyA

Zeiger sind Speicheradressen, aber Sie sollten nicht davon ausgehen, dass sie die physische Adresse widerspiegeln. Wenn Sie Adressen wie 0x00ffb500 sehen, handelt es sich um logische Adressen, die von MMU in die entsprechende physische Adresse übersetzt werden. Dies ist das wahrscheinlichste Szenario, da virtueller Speicher das am meisten erweiterte Speicherverwaltungssystem ist. Es kann jedoch auch Systeme geben, die die physische Adresse direkt verwalten

0
Mr. E

Gemäß dem C++ 14-Standard [expr.unary.op]/3:

Das Ergebnis des unären &-Operators ist ein Zeiger auf seinen Operanden. Der Operand muss ein Wert oder eine qualifizierte ID sein. Wenn der Operand eine qualifizierte ID ist, die ein nicht statisches Member m einer Klasse C vom Typ T benennt, hat das Ergebnis den Typ „Zeiger auf Member der Klasse C des Typs T“ und ist ein Wert, der C::m bezeichnet. Andernfalls, wenn der Typ des Ausdrucks T ist, hat das Ergebnis den Typ "Zeiger auf T" und ist ein prvalue das ist die Adresse des angegebenen Objekts oder ein Zeiger auf die angegebene Funktion. [Anmerkung: Insbesondere ist die Adresse eines Objekts vom Typ "cv T" ist "Zeiger auf cv T" mit der gleichen cv-Qualifikation. —Endnotiz]

Das heißt also eindeutig und eindeutig, dass Verweise auf Objekttypen (d. H. Ein T *, wobei T kein Funktionstyp ist) Adressen enthalten.


"Adresse" wird definiert durch [Intro.Memory]/1:

Der für ein C++ - Programm zur Verfügung stehende Speicher besteht aus einer oder mehreren aufeinander folgenden Bytes. Jedes Byte hat eine eindeutige Adresse.

Eine Adresse kann also alles sein, was dazu dient, ein bestimmtes Byte des Speichers eindeutig zu identifizieren. 

Hinweis: In der C++ - Standardterminologie bezieht sich memory nur auf den verwendeten Speicherplatz. Das bedeutet nicht physischen Speicher oder virtuellen Speicher oder ähnliches. Der Speicher ist eine unzusammenhängende Menge von Zuweisungen.


Es ist wichtig zu bedenken, dass, obwohl es eine Möglichkeit gibt, jedes Byte im Speicher eindeutig zu identifizieren, jedem Byte des physischen oder virtuellen Speichers eine eindeutige ganze Zahl zugewiesen wird. Dies ist jedoch nicht die einzige Möglichkeit. 

Um das Schreiben von nicht portablem Code zu vermeiden, sollten Sie nicht davon ausgehen, dass eine Adresse mit einer Ganzzahl identisch ist. Die Arithmetikregeln für Zeiger unterscheiden sich ohnehin von den Regeln der Arithmetik für ganze Zahlen. In ähnlicher Weise würden wir nicht sagen, dass 5.0f dasselbe ist wie 1084227584, obwohl sie identische Bitdarstellungen im Speicher haben (unter IEEE754).

0
M.M

Das bestimmte Beispiel, das Sie geben:

Haben beispielsweise zwei Elemente * p1 und * p2 die Eigenschaft p2 = p1 + 1 oder p1 = p2 + 1, wenn und nur dann, wenn sie im physikalischen Speicher benachbart sind?

würde auf Plattformen fehlschlagen, die keinen flachen Adressraum haben, wie beispielsweise das PIC . Um auf den physischen Speicher des PIC zuzugreifen, benötigen Sie sowohl eine Adresse als auch eine Banknummer. Letztere kann jedoch von extrinsischen Informationen wie der jeweiligen Quelldatei abgeleitet werden. Eine Arithmetik mit Zeigern verschiedener Banken würde also zu unerwarteten Ergebnissen führen.

0
Owen