Warum Inkrement-Zeiger?

25

Ich habe gerade erst angefangen, C ++ zu lernen, und wie die meisten Leute (nach dem, was ich gelesen habe) habe ich mit Zeigern zu kämpfen.

Nicht im herkömmlichen Sinne verstehe ich, was sie sind und warum sie verwendet werden und wie sie nützlich sein können. Ich kann jedoch nicht verstehen, wie nützlich das Inkrementieren von Zeigern ist. Kann jemand eine Erklärung liefern, wie das Inkrementieren eines Zeigers a ist? sinnvolles Konzept und idiomatisches C ++?

Diese Frage kam, nachdem ich angefangen hatte, das Buch A Tour of C ++ von Bjarne Stroustrup zu lesen. Mir wurde dieses Buch empfohlen, weil ich mit Java ziemlich vertraut bin, und die Jungs von Reddit sagten mir, dass es ein gutes "Umstellungsbuch" wäre .

INdek
quelle
11
Ein Zeiger ist nur ein Iterator
Charles Salvia
1
Es ist eines der beliebtesten Tools zum Schreiben von Computerviren, die lesen, was sie nicht lesen sollten. Dies ist auch einer der häufigsten Fälle von Sicherheitslücken in Apps (wenn man einen Zeiger über den Bereich erhöht, in dem er sich befinden soll, und ihn dann liest oder schreibt).> Siehe den HeartBleed-Fehler.
Sam
1
@vasile Das ist schlecht an Zeigern.
Cruncher
4
Das Schöne / Schlechte an C ++ ist, dass Sie viel mehr tun können, bevor Sie einen Segfault aufrufen. Normalerweise erhalten Sie einen Segfault, wenn Sie versuchen, auf den Speicher, den Systemspeicher oder den geschützten Anwendungsspeicher eines anderen Prozesses zuzugreifen. Jeder Zugriff auf die üblichen Anwendungsseiten wird vom System zugelassen, und es ist Sache des Programmierers / Compilers / der Sprache, angemessene Grenzen zu setzen. Mit C ++ können Sie so ziemlich alles tun, was Sie wollen. Wie für OpenSSL mit einem eigenen Speichermanager - das ist nicht wahr. Es verfügt nur über die Standardmechanismen für den C ++ - Speicherzugriff.
Sam
1
@INdek: Sie erhalten nur dann einen Segfault, wenn der Speicher, auf den Sie zugreifen möchten, geschützt ist. Die meisten Betriebssysteme weisen den Schutz auf Seitenebene zu, sodass Sie normalerweise auf alle Elemente zugreifen können, die sich auf der Seite befinden, auf der Ihr Zeiger beginnt. Wenn das Betriebssystem eine Seitengröße von 4 KB verwendet, ist das eine angemessene Datenmenge. Wenn Ihr Zeiger irgendwo auf dem Haufen beginnt, kann jeder erraten, auf wie viele Daten Sie zugreifen können.
TMN

Antworten:

46

Wenn Sie über ein Array verfügen, können Sie einen Zeiger einrichten, der auf ein Element des Arrays verweist:

int a[10];
int *p = &a[0];

Hier pzeigt auf das erste Element von a, welches ist a[0]. Jetzt können Sie den Zeiger inkrementieren, um auf das nächste Element zu zeigen:

p++;

Zeigt nun pauf das zweite Element a[1]. Hier können Sie mit auf das Element zugreifen *p. Dies unterscheidet sich von Java, bei dem Sie eine ganzzahlige Indexvariable verwenden müssten, um auf Elemente eines Arrays zuzugreifen.

Das Inkrementieren eines Zeigers in C ++, bei dem dieser Zeiger nicht auf ein Element eines Arrays zeigt, ist undefiniertes Verhalten .

Greg Hewgill
quelle
23
Ja, mit C ++ sind Sie dafür verantwortlich, Programmierfehler wie den Zugriff außerhalb der Grenzen eines Arrays zu vermeiden.
Greg Hewgill
9
Nein, das Inkrementieren eines Zeigers, der auf etwas anderes als ein Array-Element zeigt, ist undefiniertes Verhalten. Wenn Sie jedoch etwas Niedriges tun und nicht portabel sind, ist das Inkrementieren eines Zeigers normalerweise nichts anderes als der Zugriff auf das nächste Objekt im Speicher, was auch immer das sein mag.
Greg Hewgill
4
Es gibt einige Dinge, die ein Array sind oder als solches behandelt werden können. Eine Textfolge ist in der Tat ein Array von Zeichen. In einigen Fällen wird ein langes int als Array von Bytes behandelt, obwohl dies Sie leicht in Schwierigkeiten bringen kann.
AMADANON Inc.
6
Das sagt Ihnen den Typ , aber das Verhalten ist in 5.7 Additive Operatoren [expr.add] beschrieben. Insbesondere sagt 5.7 / 5, dass es UB ist, irgendwohin außerhalb des Arrays zu gehen, mit Ausnahme von One-Past-The-End.
Useless
4
Der letzte Absatz lautet: Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Array-Objekts verweisen, darf die Auswertung keinen Überlauf verursachen. Andernfalls ist das Verhalten undefiniert . Wenn das Ergebnis weder im Array noch hinter dem Ende liegt, erhalten Sie UB.
Useless
37

Das Inkrementieren von Zeigern ist idiomatisch in C ++, da die Zeigersemantik einen grundlegenden Aspekt der Designphilosophie hinter der C ++ - Standardbibliothek widerspiegelt (basierend auf der STL von Alexander Stepanov ).

Das wichtige Konzept hierbei ist, dass die STL auf Containern, Algorithmen und Iteratoren basiert. Zeiger sind einfach Iteratoren .

Natürlich geht die Fähigkeit, Zeiger zu inkrementieren (oder zu subtrahieren), auf C zurück. Viele C-String-Manipulationsalgorithmen können einfach unter Verwendung von Zeigerarithmetik geschrieben werden. Betrachten Sie den folgenden Code:

char string1[4] = "abc";
char string2[4];
char* src = string1;
char* dest = string2;
while ((*dest++ = *src++));

Dieser Code verwendet Zeigerarithmetik, um eine nullterminierte C-Zeichenfolge zu kopieren. Die Schleife wird automatisch beendet, wenn sie auf die Null stößt.

In C ++ wird die Zeigersemantik auf das Konzept der Iteratoren verallgemeinert . Die meisten Standard-C ++ - Container bieten Iteratoren, auf die über die Funktionen beginund endmember zugegriffen werden kann. Iteratoren verhalten sich wie Zeiger, da sie inkrementiert, dereferenziert und manchmal dekrementiert oder erweitert werden können.

Um über eine zu iterieren std::string, würden wir sagen:

std::string s = "abcdef";
std::string::iterator it = s.begin();
for (; it != s.end(); ++it) std::cout << *it;

Wir erhöhen den Iterator genauso, wie wir einen Zeiger auf einen einfachen C-String erhöhen würden. Der Grund für die Leistungsfähigkeit dieses Konzepts liegt darin, dass Sie Vorlagen zum Schreiben von Funktionen verwenden können, die für jeden Iteratortyp geeignet sind, der die erforderlichen Konzeptanforderungen erfüllt. Und das ist die Kraft der STL:

std::string s1 = "abcdef";
std::vector<char> buf;
std::copy(s1.begin(), s1.end(), std::back_inserter(buf));

Dieser Code kopiert eine Zeichenfolge in einen Vektor. Die copyFunktion ist eine Vorlage, die mit jedem Iterator funktioniert , der Inkrementierung unterstützt (einschließlich einfacher Zeiger). Wir könnten die gleiche copyFunktion für einen einfachen C-String verwenden:

   const char* s1 = "abcdef";
   std::vector<char> buf;
   std::copy(s1, s1 + std::strlen(s1), std::back_inserter(buf));

Wir könnten copyeinen std::mapoder einen std::setoder einen beliebigen benutzerdefinierten Container verwenden, der Iteratoren unterstützt.

Beachten Sie, dass Zeiger eine bestimmte Art von Iterator sind: Iterator mit wahlfreiem Zugriff. Dies bedeutet, dass sie das Inkrementieren, Dekrementieren und Vorrücken mit dem Operator +und unterstützen -. Andere Iteratortypen unterstützen nur eine Teilmenge der Zeigersemantik: Ein bidirektionaler Iterator unterstützt mindestens das Inkrementieren und Dekrementieren. Ein Forward-Iterator unterstützt mindestens das Inkrementieren. (Alle Iteratortypen unterstützen die Dereferenzierung.) Für die copyFunktion ist ein Iterator erforderlich, der zumindest das Inkrementieren unterstützt.

Sie können über verschiedene Iterator Konzepte lesen hier .

Das Inkrementieren von Zeigern ist also eine idiomatische C ++ - Methode, um über ein C-Array zu iterieren oder auf Elemente / Offsets in einem C-Array zuzugreifen.

Charles Salvia
quelle
3
Obwohl ich Zeiger wie im ersten Beispiel verwende, habe ich nie über einen Iterator nachgedacht, es macht jetzt sehr viel Sinn.
Farbstoffe
1
Msgstr "Die Schleife wird automatisch beendet, wenn sie auf die Null stößt." Dies ist eine erschreckende Redewendung.
Charles Wood
9
@CharlesWood, dann denke ich, dass Sie C ziemlich erschreckend finden müssen
Siler
7
@CharlesWood: Die Alternative besteht darin, die Länge der Zeichenfolge als Regelungsvariable zu verwenden. Dies bedeutet, dass die Zeichenfolge zweimal durchlaufen wird (einmal, um die Länge zu bestimmen, und einmal, um die Zeichen zu kopieren). Wenn Sie mit einem 1-MHz-PDP-7 arbeiten, kann sich das durchaus auszahlen.
TMN
3
@INdek: Zunächst einmal versuchen C und C ++ um jeden Preis zu vermeiden, Änderungen einzuführen - und ich würde sagen, dass das Ändern des Standardverhaltens von String-Literalen eine ziemliche Modifikation wäre. Am wichtigsten ist jedoch, dass nullterminierte Zeichenfolgen nur eine Konvention sind (was einfach zu befolgen ist, da Zeichenfolgenliterale standardmäßig nullterminiert sind und von den Bibliotheksfunktionen erwartet werden). Niemand hindert Sie daran, gezählte Zeichenfolgen in C zu verwenden. mehrere C-Bibliotheken verwenden sie (siehe zB OLE's BSTR).
Matteo Italia
16

Zeigerarithmetik ist in C ++, weil es in C war. Zeigerarithmetik ist in C, weil es eine normale Redewendung in Assembler ist .

Es gibt viele Systeme, in denen "Inkrementregister" schneller ist als "Konstantenwert 1 laden und zum Register hinzufügen". Darüber hinaus können Sie in einigen Systemen mit einem einzigen Befehl "DWORD von der in Register B angegebenen Adresse in A laden und anschließend sizeof (DWORD) zu B hinzufügen". Heutzutage könnte man erwarten, dass ein optimierender Compiler dies für Sie regelt, aber das war 1973 eigentlich keine Option.

Dies ist im Grunde der gleiche Grund, warum C-Arrays nicht gebunden sind und in C-Strings keine Größe eingebettet ist: Die Sprache wurde auf einem System entwickelt, auf dem jedes Byte und jeder Befehl gezählt wurde.

pjc50
quelle