Was sind die Hürden beim Verständnis von Hinweisen und was kann getan werden, um sie zu überwinden?[geschlossen]

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Frage

Warum sind Zeiger für viele neue und sogar alte College-Studenten in C oder C++ ein so großer Verwirrungsfaktor?Gibt es Tools oder Denkprozesse, die Ihnen geholfen haben zu verstehen, wie Zeiger auf Variablen-, Funktions- und darüber hinausgehender Ebene funktionieren?

Welche guten Übungsmaßnahmen können durchgeführt werden, um jemanden auf die Ebene von „Ah-hah, ich habe es verstanden“ zu bringen, ohne dass er sich im Gesamtkonzept verzettelt?Grundsätzlich handelt es sich um Drill-ähnliche Szenarios.

War es hilfreich?

Lösung

Zeiger sind ein Konzept, das für viele zunächst verwirrend sein kann, insbesondere wenn es darum geht, Zeigerwerte zu kopieren und trotzdem auf denselben Speicherblock zu verweisen.

Ich habe herausgefunden, dass die beste Analogie darin besteht, den Zeiger als ein Stück Papier mit einer Hausadresse darauf und den Speicherblock, auf den er verweist, als das tatsächliche Haus zu betrachten.So können alle möglichen Vorgänge einfach erklärt werden.

Ich habe unten etwas Delphi-Code und gegebenenfalls einige Kommentare hinzugefügt.Ich habe mich für Delphi entschieden, da meine andere Hauptprogrammiersprache, C#, nicht in der gleichen Weise Speicherverluste aufweist.

Wenn Sie nur das übergeordnete Konzept von Zeigern erlernen möchten, sollten Sie die Teile mit der Bezeichnung „Speicherlayout“ in der folgenden Erklärung ignorieren.Sie sollen Beispiele dafür geben, wie das Gedächtnis nach Operationen aussehen könnte, sie sind jedoch eher untergeordneter Natur.Um jedoch genau zu erklären, wie Pufferüberläufe wirklich funktionieren, war es wichtig, dass ich diese Diagramme hinzugefügt habe.

Haftungsausschluss:In jeder Hinsicht sind diese Erklärung und die Beispiel -Speicherlayouts erheblich vereinfacht.Es gibt mehr Overhead und viel mehr Details, die Sie wissen müssen, wenn Sie auf niedriger Ebene mit Speicher zu tun haben.Für die Absicht, Gedächtnis und Zeiger zu erklären, ist es jedoch genau genug.


Nehmen wir an, die unten verwendete THouse-Klasse sieht folgendermaßen aus:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

Wenn Sie das Hausobjekt initialisieren, wird der dem Konstruktor gegebene Name in das private Feld FName kopiert.Es gibt einen Grund, warum es als Array fester Größe definiert ist.

Denken Sie daran, dass mit der Hauszuteilung ein gewisser Mehraufwand verbunden sein wird. Ich werde dies unten wie folgt veranschaulichen:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

Der „tttt“-Bereich ist Overhead. Für verschiedene Arten von Laufzeiten und Sprachen gibt es normalerweise mehr davon, z. B. 8 oder 12 Byte.Es ist unbedingt erforderlich, dass die in diesem Bereich gespeicherten Werte niemals durch etwas anderes als die Speicherzuweisung oder die Kernsystemroutinen geändert werden, da sonst die Gefahr besteht, dass das Programm abstürzt.


Speicher zuweisen

Beauftragen Sie einen Unternehmer mit dem Bau Ihres Hauses und nennen Sie ihm die Adresse des Hauses.Im Gegensatz zur realen Welt kann der Speicherzuteilung nicht gesagt werden, wo sie zugewiesen werden soll, sondern es wird ein geeigneter Ort mit ausreichend Platz gefunden und die Adresse an den zugewiesenen Speicher zurückgemeldet.

Mit anderen Worten: Der Unternehmer wählt den Ort aus.

THouse.Create('My house');

Speicherlayout:

---[ttttNNNNNNNNNN]---
    1234My house

Behalten Sie eine Variable mit der Adresse

Schreiben Sie die Adresse Ihres neuen Hauses auf ein Blatt Papier.Dieses Papier dient als Referenz für Ihr Haus.Ohne dieses Stück Papier sind Sie verloren und können das Haus nicht finden, es sei denn, Sie befinden sich bereits darin.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Speicherlayout:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Zeigerwert kopieren

Schreiben Sie einfach die Adresse auf ein neues Blatt Papier.Sie haben jetzt zwei Zettel, die Sie zum selben Haus führen, nicht zu zwei separaten Häusern.Jeder Versuch, der Adresse aus einem Papier zu folgen und die Möbel in diesem Haus neu anzuordnen, wird den Eindruck erwecken, dass dies der Fall ist das andere Haus wurde auf die gleiche Weise verändert, es sei denn, Sie können eindeutig erkennen, dass es sich tatsächlich nur um ein Haus handelt.

Notiz Dies ist normalerweise das Konzept, das ich den Leuten am meisten erklären kann. Zwei Zeiger bedeuten nicht zwei Objekte oder Speicherblöcke.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

Den Speicher freimachen

Reiß das Haus ab.Sie können das Papier dann später bei Bedarf für eine neue Adresse wiederverwenden oder es löschen, um die Adresse des Hauses zu vergessen, die nicht mehr existiert.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

Hier baue ich zuerst das Haus und besorge mir die Adresse.Dann mache ich etwas mit dem Haus (benutze es, das ...Code, als Übung für den Leser hinterlassen), und dann gebe ich ihn frei.Zuletzt lösche ich die Adresse aus meiner Variablen.

Speicherlayout:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

Baumelnde Zeiger

Sie sagen Ihrem Unternehmer, er solle das Haus zerstören, vergessen aber, die Adresse von Ihrem Zettel zu löschen.Wenn Sie sich später den Zettel ansehen, haben Sie vergessen, dass das Haus nicht mehr da ist, und machen sich auf den Weg, es zu besichtigen – mit fehlgeschlagenem Ergebnis (siehe auch den Abschnitt über eine ungültige Referenz weiter unten).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Benutzen h nach dem Anruf an .Free könnte Arbeit, aber das ist reines Glück.Höchstwahrscheinlich wird es beim Kunden mitten in einem kritischen Vorgang ausfallen.

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Wie Sie sehen können, verweist H immer noch auf die Überreste der Daten im Speicher, aber da dies möglicherweise nicht vollständig ist, kann es möglicherweise fehlschlagen.


Speicherleck

Sie verlieren den Zettel und können das Haus nicht finden.Das Haus steht jedoch immer noch irgendwo, und wenn Sie später ein neues Haus bauen möchten, können Sie diesen Platz nicht wiederverwenden.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Hier haben wir den Inhalt des überschrieben h Variable mit der Adresse eines neuen Hauses, aber das alte steht noch...irgendwo.Nach diesem Code gibt es keine Möglichkeit mehr, dieses Haus zu erreichen, und es bleibt stehen.Mit anderen Worten: Der zugewiesene Speicher bleibt zugewiesen, bis die Anwendung geschlossen wird. Zu diesem Zeitpunkt wird er vom Betriebssystem abgebaut.

Speicherlayout nach der ersten Zuweisung:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Speicherlayout nach zweiter Zuweisung:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Ein häufigerer Weg, diese Methode zu erhalten, besteht darin, einfach zu vergessen, etwas freizugeben, anstatt es wie oben beschrieben zu überschreiben.In Delphi-Begriffen geschieht dies mit der folgenden Methode:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

Nachdem diese Methode ausgeführt wurde, gibt es in unseren Variablen keinen Platz dafür, dass die Adresse des Hauses existiert, aber das Haus ist immer noch da draußen.

Speicherlayout:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

Wie Sie sehen können, bleiben die alten Daten im Speicher intakt und werden vom Speicher Allocator nicht wiederverwendet.Der Allocator verfolgt, welche Speicherbereiche verwendet wurden, und wird sie nicht wiederverwenden, es sei denn, Sie befreien ihn.


Geben Sie den Speicher frei, behalten Sie aber eine (jetzt ungültige) Referenz bei

Reißen Sie das Haus ab, löschen Sie einen der Zettel, aber Sie haben auch einen anderen Zettel mit der alten Adresse darauf. Wenn Sie zur Adresse gehen, werden Sie kein Haus finden, aber vielleicht etwas, das den Ruinen ähnelt von einem.

Vielleicht finden Sie sogar ein Haus, aber es ist nicht das Haus, dessen Adresse Sie ursprünglich erhalten haben, und daher könnten alle Versuche, es so zu nutzen, als ob es Ihnen gehörte, schrecklich scheitern.

Manchmal stellen Sie möglicherweise sogar fest, dass auf einer Nachbaradresse ein ziemlich großes Haus steht, das drei Adressen einnimmt (Hauptstraße 1-3), und Ihre Adresse befindet sich in der Mitte des Hauses.Jeder Versuch, diesen Teil des großen Hauses mit drei Adressen als ein einzelnes kleines Haus zu behandeln, könnte ebenfalls schrecklich scheitern.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Hier wurde das Haus abgerissen, durch den Hinweis in h1, und während h1 wurde auch geklärt, h2 hat immer noch die alte, veraltete Adresse.Der Zugang zum Haus, das nicht mehr steht, könnte funktionieren oder auch nicht.

Dies ist eine Variation des baumelnden Zeigers oben.Sehen Sie sich das Speicherlayout an.


Pufferüberlauf

Sie bringen mehr Dinge in das Haus, als Sie überhaupt unterbringen können, und ergießen sich in das Haus oder den Garten des Nachbarn.Wenn der Besitzer des Nachbarhauses später nach Hause kommt, wird er allerlei Dinge vorfinden, die er als sein Eigentum betrachten wird.

Aus diesem Grund habe ich mich für ein Array mit fester Größe entschieden.Um die Bühne zu setzen, nehmen Sie an, dass das zweite Haus, das wir aus irgendeinem Grund zuordnen, vor dem ersten in Erinnerung gestellt werden wird.Mit anderen Worten, das zweite Haus hat eine niedrigere Adresse als die erste.Außerdem sind sie direkt nebeneinander angeordnet.

Somit ist dieser Code:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Speicherlayout nach der ersten Zuweisung:

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

Speicherlayout nach zweiter Zuweisung:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

Der Teil, der am häufigsten zum Absturz führt, ist, wenn Sie wichtige Teile der von Ihnen gespeicherten Daten überschreiben, die wirklich nicht zufällig geändert werden sollten.Zum Beispiel ist es möglicherweise kein Problem, dass Teile des Namens des H1-House im Hinblick auf das Absturz des Programms geändert wurden. Das Überziehen des Overhead des Objekts wird jedoch höchstwahrscheinlich zum Absturz gebracht, wenn Sie versuchen, das kaputte Objekt zu verwenden, wie Überschreiben von Links, die auf andere Objekte im Objekt gespeichert sind.


Verknüpfte Listen

Wenn Sie einer Adresse auf einem Blatt Papier folgen, gelangen Sie zu einem Haus, und bei diesem Haus liegt ein weiteres Blatt Papier mit einer neuen Adresse darauf, für das nächste Haus in der Kette und so weiter.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Hier erstellen wir eine Verbindung von unserem Zuhause zu unserer Hütte.Wir können der Kette folgen, bis ein Haus keine mehr hat NextHouse Referenz, was bedeutet, dass es die letzte ist.Um alle unsere Häuser zu besuchen, könnten wir den folgenden Code verwenden:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Speicherlayout (Nexhouse als Link im Objekt hinzugefügt, das mit den vier LLLLs im folgenden Diagramm festgestellt wurde):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

Was ist im Grunde eine Speicheradresse?

Eine Speicheradresse ist im Grunde nur eine Zahl.Wenn Sie das Gedächtnis als eine große Reihe von Bytes betrachten, hat das allererste Byte die Adresse 0, die nächste die Adresse 1 und so weiter nach oben.Das ist vereinfacht, aber gut genug.

Also dieses Speicherlayout:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Könnte diese beiden Adressen haben (die Adresse ganz links ist Adresse 0):

  • h1 = 4
  • h2 = 23

Das bedeutet, dass unsere oben verlinkte Liste tatsächlich so aussehen könnte:

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

Typischerweise wird eine Adresse, die „in nichts zeigt“, als Nulladresse gespeichert.


Was ist im Grunde ein Zeiger?

Ein Zeiger ist einfach eine Variable, die eine Speicheradresse enthält.In der Regel können Sie die Programmiersprache bitten, Ihnen ihre Nummer zu geben, aber die meisten Programmiersprachen und Laufzeiten versuchen, die Tatsache zu verbergen, dass es eine Nummer darunter gibt, nur weil die Zahl selbst für Sie keine Bedeutung hat.Stellen Sie sich einen Zeiger am besten als Blackbox vor, d. h.Sie wissen oder kümmern sich nicht wirklich darum, wie es tatsächlich implementiert wird, solange es funktioniert.

Andere Tipps

In meinem ersten Comp Sci-Kurs haben wir die folgende Übung gemacht.Zugegeben, das war ein Hörsaal mit etwa 200 Studenten ...

Professor schreibt an die Tafel: int john;

John steht auf

Professor schreibt: int *sally = &john;

Sally steht auf und zeigt auf John

Professor: int *bill = sally;

Bill steht auf und zeigt auf John

Professor: int sam;

Sam steht auf

Professor: bill = &sam;

Bill zeigt nun auf Sam.

Ich denke, Sie haben die Idee verstanden.Ich glaube, wir haben ungefähr eine Stunde damit verbracht, bis wir uns mit den Grundlagen der Zeigerzuweisung befasst haben.

Eine Analogie, die ich zur Erklärung von Zeigern als hilfreich empfunden habe, sind Hyperlinks.Die meisten Menschen können verstehen, dass ein Link auf einer Webseite auf eine andere Seite im Internet verweist. Wenn Sie diesen Hyperlink kopieren und einfügen können, verweisen beide auf dieselbe ursprüngliche Webseite.Wenn Sie die ursprüngliche Seite bearbeiten und dann einem dieser Links (Zeiger) folgen, erhalten Sie die neue, aktualisierte Seite.

Der Grund dafür, dass Zeiger so viele Menschen zu verwirren scheinen, liegt darin, dass sie meist nur über geringe oder gar keine Kenntnisse in der Computerarchitektur verfügen.Da viele scheinbar keine Vorstellung davon haben, wie Computer (die Maschine) tatsächlich implementiert sind, erscheint die Arbeit in C/C++ fremd.

Eine Übung besteht darin, sie zu bitten, eine einfache Bytecode-basierte virtuelle Maschine (in jeder von ihnen gewählten Sprache, Python eignet sich hervorragend dafür) mit einem Befehlssatz zu implementieren, der sich auf Zeigeroperationen (Laden, Speichern, direkte/indirekte Adressierung) konzentriert.Bitten Sie sie dann, einfache Programme für diesen Befehlssatz zu schreiben.

Alles, was etwas mehr als eine einfache Addition erfordert, erfordert Hinweise, und sie werden es mit Sicherheit verstehen.

Warum sind Zeiger für viele neue und sogar alte Studenten der C/C++-Sprache ein so großer Verwirrungsfaktor?

Das Konzept eines Platzhalters für einen Wert – Variablen – lässt sich auf etwas übertragen, das uns in der Schule beigebracht wird – Algebra.Es gibt keine Parallele, die man ziehen kann, ohne zu verstehen, wie der Speicher in einem Computer physisch angeordnet ist, und niemand denkt über so etwas nach, bis er sich mit Dingen auf niedriger Ebene befasst – auf der C/C++/Byte-Kommunikationsebene .

Gibt es Tools oder Denkprozesse, die Ihnen geholfen haben zu verstehen, wie Zeiger auf Variablen-, Funktions- und darüber hinausgehender Ebene funktionieren?

Adressfelder.Ich erinnere mich, als ich lernte, BASIC in Mikrocomputer zu programmieren, gab es diese hübschen Bücher mit Spielen darin, und manchmal musste man Werte in bestimmte Adressen einfügen.Sie hatten ein Bild von einer Reihe von Kisten, die nach und nach mit 0, 1, 2 ... beschriftet waren.und es wurde erklärt, dass nur ein kleines Ding (ein Byte) in diese Kästchen passte, und davon gab es viele – manche Computer hatten sogar 65535!Sie standen nebeneinander und hatten alle eine Adresse.

Welche guten Übungsmaßnahmen können durchgeführt werden, um jemanden auf die Ebene von „Ah-hah, ich habe es verstanden“ zu bringen, ohne dass er sich im Gesamtkonzept verzettelt?Grundsätzlich handelt es sich um Drill-ähnliche Szenarios.

Für eine Übung?Erstellen Sie eine Struktur:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Gleiches Beispiel wie oben, außer in C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Ausgabe:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Vielleicht erklärt das einige der Grundlagen anhand von Beispielen?

Der Grund dafür, dass es mir anfangs schwerfiel, Hinweise zu verstehen, liegt darin, dass viele Erklärungen eine Menge Blödsinn über die Weitergabe von Referenzen enthalten.Dies führt lediglich zur Verwirrung des Problems.Wenn Sie einen Zeigerparameter verwenden, sind Sie Trotzdem Wertübergabe;aber der Wert ist zufällig eine Adresse und nicht, sagen wir, ein int.

Jemand anderes hat bereits auf dieses Tutorial verlinkt, aber ich kann den Moment hervorheben, als ich anfing, Hinweise zu verstehen:

Ein Tutorial zu Zeigern und Arrays in C:Kapitel 3 – Zeiger und Strings

int puts(const char *s);

Ignorieren Sie im Moment das const. Der Parameter, an den übergeben wird puts() ist ein Zeiger, Das ist der Wert eines Zeigers (da alle Parameter in C als Wert übergeben werden), und der Wert eines Zeigers ist die Adresse, auf die er zeigt, oder einfach eine Adresse. So, wenn wir schreiben puts(strA); Wie wir gesehen haben, übergeben wir die Adresse von strA[0].

In dem Moment, als ich diese Worte las, teilten sich die Wolken und ein Sonnenstrahl umhüllte mich mit verständnisvollem Verständnis.

Auch wenn Sie (wie ich) VB .NET- oder C#-Entwickler sind und niemals unsicheren Code verwenden, lohnt es sich dennoch zu verstehen, wie Zeiger funktionieren, sonst verstehen Sie nicht, wie Objektverweise funktionieren.Dann haben Sie die weitverbreitete, aber irrige Vorstellung, dass die Übergabe einer Objektreferenz an eine Methode das Objekt kopiert.

Ich fand Ted Jensens „Tutorial on Pointers and Arrays in C“ eine hervorragende Quelle zum Erlernen von Zeigern.Es ist in 10 Lektionen unterteilt, beginnend mit einer Erklärung dessen, was Zeiger sind (und wozu sie dienen) und abschließend mit Funktionszeigern. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Von dort aus lehrt Beej's Guide to Network Programming die Unix-Sockets-API, mit der Sie beginnen können, wirklich unterhaltsame Dinge zu tun. http://beej.us/guide/bgnet/

Die Komplexität von Zeigern geht über das hinaus, was wir leicht lehren können.Wenn die Schüler aufeinander zeigen und Zettel mit Hausadressen verwenden, sind beides großartige Lernmittel.Sie leisten hervorragende Arbeit bei der Einführung in die Grundkonzepte.Das Erlernen der Grundkonzepte ist in der Tat sinnvoll lebenswichtig zur erfolgreichen Verwendung von Zeigern.Im Produktionscode kommt es jedoch häufig vor, dass man sich mit viel komplexeren Szenarien beschäftigt, als diese einfachen Demonstrationen darstellen können.

Ich habe mit Systemen zu tun gehabt, bei denen wir Strukturen hatten, die auf andere Strukturen zeigten, die auf andere Strukturen zeigten.Einige dieser Strukturen enthielten auch eingebettete Strukturen (anstelle von Verweisen auf zusätzliche Strukturen).Hier werden Hinweise wirklich verwirrend.Wenn Sie über mehrere Indirektionsebenen verfügen und Code wie diesen erhalten:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

Es kann sehr schnell verwirrend werden (stellen Sie sich viel mehr Zeilen und möglicherweise mehr Ebenen vor).Fügen Sie Arrays von Zeigern und Knoten-zu-Knoten-Zeigern (Bäume, verknüpfte Listen) hinzu, und es wird noch schlimmer.Ich habe einige wirklich gute Entwickler gesehen, die verloren gegangen sind, als sie mit der Arbeit an solchen Systemen begonnen haben, selbst Entwickler, die die Grundlagen wirklich gut verstanden haben.

Komplexe Zeigerstrukturen weisen auch nicht unbedingt auf eine schlechte Codierung hin (obwohl dies der Fall sein kann).Die Komposition ist ein wesentlicher Bestandteil einer guten objektorientierten Programmierung und führt in Sprachen mit Rohzeigern unweigerlich zu einer mehrschichtigen Indirektion.Darüber hinaus müssen Systeme häufig Bibliotheken von Drittanbietern verwenden, deren Strukturen weder im Stil noch in der Technik zueinander passen.In solchen Situationen entsteht natürlich Komplexität (obwohl wir sie auf jeden Fall so weit wie möglich bekämpfen sollten).

Ich denke, das Beste, was Hochschulen tun können, um Studenten beim Erlernen von Zeigern zu helfen, ist die Verwendung guter Demonstrationen in Kombination mit Projekten, die die Verwendung von Zeigern erfordern.Ein schwieriges Projekt wird mehr zum Verständnis des Zeigers beitragen als tausend Demonstrationen.Demonstrationen können Ihnen ein oberflächliches Verständnis vermitteln, aber um Hinweise wirklich zu verstehen, müssen Sie sie wirklich nutzen.

Ich dachte, ich würde dieser Liste eine Analogie hinzufügen, die ich (damals) als Informatiklehrer sehr hilfreich fand, als ich Hinweise erklärte;Lassen Sie uns zunächst Folgendes tun:


Bereiten Sie die Bühne vor:

Stellen Sie sich einen Parkplatz mit 3 Stellplätzen vor. Diese Stellplätze sind nummeriert:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

In gewisser Weise ist dies wie bei Speicherorten, sie sind sequentiell und zusammenhängend.so etwas wie ein Array.Im Moment sind keine Autos darin, also ist es wie ein leeres Array (parking_lot[3] = {0}).


Fügen Sie die Daten hinzu

Ein Parkplatz bleibt nie lange leer...Wenn es so wäre, wäre es sinnlos und niemand würde welche bauen.Nehmen wir also an, im Laufe des Tages füllt sich der Parkplatz mit drei Autos, einem blauen Auto, einem roten Auto und einem grünen Auto:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Diese Autos sind alle vom gleichen Typ (Auto). Eine Möglichkeit, sich das vorzustellen, ist also, dass unsere Autos eine Art Daten sind (z. B. ein int), aber sie haben unterschiedliche Werte (blue, red, green;das könnte eine Farbe sein enum)


Geben Sie den Zeiger ein

Wenn ich Sie nun auf diesen Parkplatz bringe und Sie bitte, mir ein blaues Auto zu suchen, strecken Sie einen Finger aus und zeigen damit auf ein blaues Auto an Stelle 1.Das ist, als würde man einen Zeiger nehmen und ihn einer Speicheradresse zuweisen (int *finger = parking_lot)

Ihr Finger (der Zeiger) ist nicht die Antwort auf meine Frage.Schauen bei Dein Finger sagt mir nichts, aber wenn ich schaue, wo dein Finger ist zeigt auf (Dereferenzierung des Zeigers) kann ich das Auto (die Daten) finden, nach dem ich gesucht habe.


Den Zeiger neu zuweisen

Jetzt kann ich Sie bitten, stattdessen ein rotes Auto zu finden, und Sie können Ihren Finger auf ein neues Auto umleiten.Jetzt zeigt mir Ihr Zeiger (derselbe wie zuvor) neue Daten (den Parkplatz, auf dem sich das rote Auto befindet) des gleichen Typs (das Auto) an.

Der Zeiger hat sich physisch nicht verändert, das ist er immer noch dein Finger, nur die Daten, die es mir zeigte, haben sich geändert.(die „Parkplatz“-Adresse)


Doppelte Zeiger (oder ein Zeiger auf einen Zeiger)

Dies funktioniert auch mit mehr als einem Zeiger.Ich kann fragen, wo der Zeiger ist, der auf das rote Auto zeigt, und Sie können Ihre andere Hand verwenden und mit einem Finger auf den Zeigefinger zeigen.(das ist wie int **finger_two = &finger)

Wenn ich nun wissen möchte, wo sich das blaue Auto befindet, kann ich der Richtung des ersten Fingers zum zweiten Finger folgen, zum Auto (den Daten).


Der baumelnde Zeiger

Nehmen wir an, Sie fühlen sich wie eine Statue und möchten Ihre Hand für unbegrenzte Zeit auf das rote Auto zeigen lassen.Was ist, wenn das rote Auto wegfährt?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

Ihr Zeiger zeigt immer noch auf das rote Auto War ist es aber nicht mehr.Nehmen wir an, da fährt ein neues Auto vorbei...ein orangefarbenes Auto.Wenn ich Sie jetzt noch einmal frage: „Wo ist das rote Auto“, zeigen Sie immer noch dorthin, aber jetzt liegen Sie falsch.Das ist kein rotes Auto, das ist orange.


Zeigerarithmetik

Ok, Sie zeigen also immer noch auf den zweiten Parkplatz (der jetzt vom orangefarbenen Auto belegt ist).

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Nun, ich habe jetzt eine neue Frage ...Ich möchte wissen, welche Farbe das Auto hat nächste Parkplatz.Sie können sehen, dass Sie auf Punkt 2 zeigen. Fügen Sie also einfach 1 hinzu und zeigen Sie auf den nächsten Punkt.(finger+1), da ich nun wissen wollte, welche Daten dort waren, müssen Sie diese Stelle überprüfen (nicht nur den Finger), damit Sie den Zeiger respektieren können (*(finger+1)), um zu sehen, dass dort ein grünes Auto vorhanden ist (die Daten an diesem Standort)

Ich glaube nicht, dass Zeiger als Konzept besonders knifflig sind – die mentalen Modelle der meisten Schüler lassen sich auf so etwas abbilden, und ein paar schnelle Boxskizzen können hilfreich sein.

Die Schwierigkeit, zumindest die, die ich in der Vergangenheit erlebt habe und mit der sich andere befasst haben, besteht darin, dass die Verwaltung von Zeigern in C/C++ unnötig kompliziert sein kann.

Ein Beispiel für ein Tutorial mit einer guten Reihe von Diagrammen hilft sehr beim Verständnis von Zeigern.

Joel Spolsky bringt in seinem Beitrag einige gute Argumente zum Verständnis von Hinweisen vor Guerilla-Leitfaden für Vorstellungsgespräche Artikel:

Aus irgendeinem Grund scheinen die meisten Menschen ohne den Teil des Gehirns geboren zu werden, der Zeiger versteht.Dabei handelt es sich um eine Frage der Begabung, nicht der Fertigkeit – es erfordert eine komplexe Form des doppelt indirekten Denkens, zu der manche Menschen einfach nicht in der Lage sind.

Ich denke, das Haupthindernis für das Verständnis von Hinweisen sind schlechte Lehrer.

Fast jedem werden Lügen über Hinweise beigebracht:Dass sie sind nichts weiter als Speicheradressen, oder auf die Sie zeigen können beliebige Standorte.

Und natürlich, dass sie schwer zu verstehen, gefährlich und halbmagisch sind.

Nichts davon ist wahr.Zeiger sind eigentlich ziemlich einfache Konzepte, solange Sie sich an das halten, was die C++-Sprache dazu zu sagen hat und statten Sie sie nicht mit Attributen aus, die „normalerweise“ in der Praxis funktionieren, aber dennoch nicht durch die Sprache garantiert werden und daher nicht Teil des eigentlichen Konzepts eines Zeigers sind.

Ich habe vor ein paar Monaten versucht, eine Erklärung dazu zu verfassen dieser Blogbeitrag – hoffentlich hilft es jemandem.

(Beachten Sie, bevor mich jemand pedantisch macht: Ja, der C++-Standard sagt, dass Zeiger vertreten Speicheradressen.Aber es heißt nicht, dass „Zeiger Speicheradressen und nichts anderes als Speicheradressen sind und austauschbar mit Speicheradressen verwendet oder betrachtet werden können“.Die Unterscheidung ist wichtig)

Das Problem mit Zeigern ist nicht das Konzept.Es kommt auf die Ausführung und die Sprache an.Zusätzliche Verwirrung entsteht, wenn Lehrer annehmen, dass es das KONZEPT der Zeiger ist, das schwierig ist, und nicht der Fachjargon oder das verworrene Durcheinander, das C und C++ aus dem Konzept macht.Es werden also enorme Anstrengungen unternommen, um das Konzept zu erklären (wie in der akzeptierten Antwort auf diese Frage), und für jemanden wie mich ist es so gut wie reine Verschwendung, weil ich das alles bereits verstehe.Es erklärt nur den falschen Teil des Problems.

Um Ihnen eine Vorstellung davon zu geben, woher ich komme: Ich bin jemand, der Zeiger perfekt versteht und sie in der Assembler-Sprache kompetent verwenden kann.Denn in der Assemblersprache werden sie nicht als Zeiger bezeichnet.Sie werden als Adressen bezeichnet.Wenn es um die Programmierung und Verwendung von Zeigern in C geht, mache ich viele Fehler und bin wirklich verwirrt.Ich habe das immer noch nicht geklärt.Lassen Sie mich Ihnen ein Beispiel geben.

Wenn eine API sagt:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

was will es?

es könnte wollen:

eine Zahl, die eine Adresse für einen Puffer darstellt

(Um es so zu sagen, sage ich doIt(mybuffer), oder doIt(*myBuffer)?)

eine Zahl, die die Adresse zu einer Adresse zu einem Puffer darstellt

(ist das doIt(&mybuffer) oder doIt(mybuffer) oder doIt(*mybuffer)?)

eine Zahl, die die Adresse zur Adresse zur Adresse zum Puffer darstellt

(Vielleicht ist das so doIt(&mybuffer).oder ist es doIt(&&mybuffer) ?oder auch doIt(&&&mybuffer))

und so weiter, und die verwendete Sprache macht es nicht so klar, weil es sich um die Wörter „Zeiger“ und „Referenz“ handelt, die für mich nicht so viel Bedeutung und Klarheit haben wie „x enthält die Adresse zu y“ und „ Diese Funktion erfordert eine Adresse an y".Die Antwort hängt außerdem davon ab, was zum Teufel „mybuffer“ überhaupt ist und was doIt damit vorhat.Die Sprache unterstützt nicht die in der Praxis vorkommenden Verschachtelungsebenen.Zum Beispiel, wenn ich einen „Zeiger“ an eine Funktion übergeben muss, die einen neuen Puffer erstellt, und diese den Zeiger so ändert, dass er auf die neue Position des Puffers zeigt.Will es wirklich den Zeiger oder einen Zeiger auf den Zeiger, damit es weiß, wohin es gehen muss, um den Inhalt des Zeigers zu ändern?Meistens muss ich nur raten, was mit „Zeiger“ gemeint ist, und meistens liege ich falsch, unabhängig davon, wie viel Erfahrung ich im Raten habe.

„Pointer“ ist einfach zu überladen.Ist ein Zeiger eine Adresse auf einen Wert?oder ist es eine Variable, die eine Adresse für einen Wert enthält?Wenn eine Funktion einen Zeiger benötigt, möchte sie dann die Adresse, die die Zeigervariable enthält, oder möchte sie die Adresse der Zeigervariablen?Ich bin verwirrt.

Ich denke, was das Erlernen von Zeigern schwierig macht, ist die Tatsache, dass man bis zu Zeigern mit der Idee vertraut ist, dass „an diesem Speicherort eine Reihe von Bits ist, die ein int, ein Double, ein Zeichen, was auch immer darstellen“.

Wenn Sie zum ersten Mal einen Zeiger sehen, verstehen Sie nicht wirklich, was sich an diesem Speicherort befindet.„Was meinst du damit, es hält eine Adresse?"

Ich stimme nicht mit der Vorstellung überein, dass man sie entweder bekommt oder nicht.

Sie werden leichter zu verstehen, wenn Sie anfangen, echte Verwendungsmöglichkeiten für sie zu finden (z. B. keine großen Strukturen in Funktionen zu übergeben).

Der Grund dafür, dass es so schwer zu verstehen ist, liegt nicht darin, dass es ein schwieriges Konzept ist, sondern darin Die Syntax ist inkonsistent.

   int *mypointer;

Sie erfahren zunächst, dass der ganz linke Teil einer Variablenerstellung den Typ der Variablen definiert.Die Zeigerdeklaration funktioniert in C und C++ nicht auf diese Weise.Stattdessen sagen sie, dass die Variable auf den Typ links zeigt.In diesem Fall: *meinpointer zeigt auf einem int.

Ich habe die Zeiger erst vollständig verstanden, als ich sie in C# (mit unsicherem Code) ausprobiert habe. Sie funktionieren genauso, aber mit logischer und konsistenter Syntax.Der Zeiger ist selbst ein Typ.Hier meinpointer Ist ein Zeiger auf einen int.

  int* mypointer;

Lassen Sie mich gar nicht erst mit Funktionszeigern anfangen ...

Ich konnte mit Zeigern arbeiten, als ich nur C++ kannte.In manchen Fällen wusste ich durch Versuch/Irrtum, was ich tun sollte und was nicht.Aber was mir ein umfassendes Verständnis verschafft hat, ist die Assemblersprache.Wenn Sie ein ernsthaftes Debugging auf Befehlsebene mit einem von Ihnen geschriebenen Assemblerprogramm durchführen, sollten Sie in der Lage sein, viele Dinge zu verstehen.

Mir gefällt die Analogie zur Hausadresse, aber ich habe immer daran gedacht, dass die Adresse zum Briefkasten selbst gehört.Auf diese Weise können Sie sich das Konzept der Dereferenzierung des Zeigers (Öffnen des Postfachs) vorstellen.

Zum Beispiel einer verknüpften Liste folgen:1) Beginnen Sie mit Ihrem Papier mit der Adresse 2) Gehen Sie zur Adresse auf dem Papier 3) Öffnen Sie die Mailbox, um ein neues Stück Papier mit der nächsten Adresse zu finden

In einer linear verknüpften Liste enthält das letzte Postfach nichts (Ende der Liste).In einer zirkulär verknüpften Liste enthält das letzte Postfach die Adresse des ersten Postfachs.

Beachten Sie, dass in Schritt 3 die Dereferenzierung erfolgt und es zu Abstürzen oder Fehlern kommt, wenn die Adresse ungültig ist.Angenommen, Sie könnten zum Briefkasten einer ungültigen Adresse gehen und sich vorstellen, dass sich dort ein schwarzes Loch oder etwas befindet, das die Welt auf den Kopf stellt :)

Ich denke, dass der Hauptgrund dafür, dass die Leute Probleme damit haben, darin besteht, dass es im Allgemeinen nicht auf interessante und ansprechende Weise gelehrt wird.Ich würde gerne sehen, wie ein Dozent 10 Freiwillige aus der Menge zusammenholt und ihnen jeweils ein 1-Meter-Lineal gibt, sie dazu bringt, in einer bestimmten Anordnung herumzustehen und die Lineale zu benutzen, um aufeinander zu zeigen.Zeigen Sie dann Zeigerarithmetik, indem Sie Menschen bewegen (und wohin sie mit ihren Linealen zeigen).Es wäre eine einfache, aber effektive (und vor allem einprägsame) Möglichkeit, die Konzepte zu zeigen, ohne sich zu sehr in der Mechanik zu verzetteln.

Sobald man sich mit C und C++ beschäftigt, scheint es für manche Leute schwieriger zu werden.Ich bin mir nicht sicher, ob das daran liegt, dass sie endlich eine Theorie, die sie nicht richtig verstehen, in die Praxis umsetzen, oder ob die Zeigermanipulation in diesen Sprachen von Natur aus schwieriger ist.Ich kann mich nicht so gut an meinen eigenen Übergang erinnern, aber ich wusste Zeiger in Pascal und wechselte dann zu C und ging völlig verloren.

Ich glaube nicht, dass Zeiger selbst verwirrend sind.Die meisten Menschen können das Konzept verstehen.Nun, über wie viele Hinweise können Sie nachdenken oder mit wie vielen Ebenen der Indirektion sind Sie vertraut?Es braucht nicht zu viele, um die Leute aus der Fassung zu bringen.Die Tatsache, dass sie durch Fehler in Ihrem Programm versehentlich geändert werden können, kann es auch sehr schwierig machen, sie zu debuggen, wenn in Ihrem Code etwas schief geht.

Ich denke, es könnte tatsächlich ein Syntaxproblem sein.Die C/C++-Syntax für Zeiger scheint inkonsistent und komplexer zu sein, als sie sein müsste.

Ironischerweise hat mir die Begegnung mit dem Konzept eines Iterators in C++ tatsächlich geholfen, Zeiger zu verstehen Standardvorlagenbibliothek.Das ist ironisch, weil ich nur annehmen kann, dass Iteratoren als Verallgemeinerung des Zeigers konzipiert wurden.

Manchmal kann man den Wald einfach nicht sehen, bis man lernt, die Bäume zu ignorieren.

Die Verwirrung rührt von den mehreren Abstraktionsebenen her, die im „Zeiger“-Konzept miteinander vermischt werden.Programmierer lassen sich durch gewöhnliche Referenzen in Java/Python nicht verwirren, aber Zeiger unterscheiden sich darin, dass sie Merkmale der zugrunde liegenden Speicherarchitektur offenlegen.

Es ist ein gutes Prinzip, die Abstraktionsebenen sauber zu trennen, und Zeiger tun dies nicht.

Ich habe es gerne anhand von Arrays und Indizes erklärt – die Leute sind vielleicht nicht mit Zeigern vertraut, aber sie wissen im Allgemeinen, was ein Index ist.

Ich sage also, stellen Sie sich vor, dass der RAM ein Array ist (und Sie nur 10 Byte RAM haben):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Dann ist ein Zeiger auf eine Variable eigentlich nur der Index (das erste Byte) dieser Variablen im RAM.

Wenn Sie also einen Zeiger/Index haben unsigned char index = 2, dann ist der Wert offensichtlich das dritte Element oder die Zahl 4.Bei einem Zeiger auf einen Zeiger nehmen Sie diese Zahl und verwenden sie beispielsweise als Index RAM[RAM[index]].

Ich würde ein Array auf eine Liste Papier zeichnen und es einfach verwenden, um Dinge wie viele Zeiger anzuzeigen, die auf denselben Speicher zeigen, Zeigerarithmetik, Zeiger auf Zeiger und so weiter.

Postfachnummer.

Es handelt sich um eine Information, die Ihnen den Zugriff auf etwas anderes ermöglicht.

(Und wenn Sie mit Postfachnummern rechnen, könnten Sie ein Problem haben, weil der Brief in das falsche Postfach kommt.Und wenn jemand in einen anderen Staat zieht – ohne Weiterleitungsadresse –, dann haben Sie einen baumelnden Zeiger.Wenn andererseits die Post die Post weiterleitet, dann haben Sie einen Zeiger auf einen Zeiger.)

Keine schlechte Möglichkeit, es über Iteratoren zu erfassen.Aber schauen Sie weiter, Sie werden feststellen, dass Alexandrescu anfängt, sich darüber zu beschweren.

Viele ehemalige C++-Entwickler (die nie verstanden haben, dass Iteratoren ein moderner Zeiger sind, bevor sie die Sprache löschen) wechseln zu C# und glauben immer noch, dass sie anständige Iteratoren haben.

Hmm, das Problem ist, dass alles, was Iteratoren sind, völlig im Widerspruch zu dem steht, was die Laufzeitplattformen (Java/CLR) erreichen wollen:Neue, einfache, Jeder-ist-ein-Entwickler-Nutzung.Was gut sein kann, aber sie haben es einmal im Lila Buch gesagt und sie haben es sogar vor und vor C gesagt:

Indirektion.

Ein sehr wirkungsvolles Konzept, das aber niemals überzeugend sein wird, wenn man es bis zum Ende durchführt.Iteratoren sind nützlich, da sie bei der Abstraktion von Algorithmen helfen, ein weiteres Beispiel.Und die Kompilierungszeit ist der Ort für einen Algorithmus, ganz einfach.Sie kennen Code + Daten oder in dieser anderen Sprache C#:

IEnumerable + LINQ + Massive Framework = 300 MB Laufzeitstrafe, umgeleitet von miesen, ziehenden Apps über haufenweise Instanzen von Referenztypen.

„Le Pointer ist billig.“

Einige Antworten oben haben behauptet, dass "Zeiger nicht wirklich hart" sind, aber nicht direkt ansprechen, wo "Zeiger hart sind!" kommt von.Vor einigen Jahren habe ich CS-Studenten im ersten Jahr Nachhilfe gegeben (nur ein Jahr lang, da ich eindeutig darin scheiße war) und mir war klar, dass das Idee des Zeigers ist nicht schwer.Was schwierig ist, ist das Verstehen warum und wann Sie einen Zeiger wünschen würden.

Ich glaube nicht, dass man die Frage – warum und wann man einen Zeiger verwenden sollte – von der Erklärung umfassenderer Software-Engineering-Probleme trennen kann.Warum jede Variable sollte nicht eine globale Variable sein und warum man ähnlichen Code in Funktionen ausgliedern sollte (die, verstehen Sie das, verwenden). Hinweise ihr Verhalten auf ihre Anrufstelle zu spezialisieren).

Ich verstehe nicht, was an Zeigern so verwirrend sein soll.Sie verweisen auf eine Stelle im Speicher, d. h. sie speichern die Speicheradresse.In C/C++ können Sie den Typ angeben, auf den der Zeiger zeigt.Zum Beispiel:

int* my_int_pointer;

Sagt, dass my_int_pointer die Adresse zu einem Ort enthält, der einen int enthält.

Das Problem mit Zeigern besteht darin, dass sie auf eine Stelle im Speicher verweisen, sodass es leicht ist, an eine Stelle zu gelangen, an der man sich nicht befinden sollte.Schauen Sie sich als Beweis die zahlreichen Sicherheitslücken in C/C++-Anwendungen an, die durch einen Pufferüberlauf (Erhöhen des Zeigers über die zugewiesene Grenze hinaus) entstehen.

Um die Sache noch ein wenig zu verwirren, muss man manchmal mit Handles statt mit Zeigern arbeiten.Handles sind Zeiger auf Zeiger, sodass das Backend Dinge im Speicher verschieben kann, um den Heap zu defragmentieren.Wenn sich der Zeiger mitten in der Routine ändert, sind die Ergebnisse unvorhersehbar. Daher müssen Sie zuerst den Griff sperren, um sicherzustellen, dass nichts irgendwohin geht.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 spricht etwas schlüssiger darüber als ich.:-)

Jeder C/C++-Anfänger hat das gleiche Problem und dieses Problem tritt nicht auf, weil „Zeiger schwer zu lernen sind“, sondern weil „wer und wie sie erklärt werden“.Manche Lernende erfassen es verbal, andere visuell, und die beste Art, es zu erklären, ist die Verwendung von Beispiel „Zug“. (geeignet für verbale und visuelle Beispiele).

Wo "Lokomotive" ist ein Zeiger, der kann nicht alles festhalten und "Wagen" ist das, was „Lokomotive“ versucht zu ziehen (oder darauf hinzuweisen).Anschließend können Sie den „Wagen“ selbst klassifizieren, ob er Tiere, Pflanzen oder Menschen (oder eine Mischung daraus) beherbergen kann.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top