Wie kann man einem Anfänger C-Zeiger (Deklaration vs. unäre Operatoren) erklären?

141

Ich hatte kürzlich das Vergnügen, einem C-Programmier-Anfänger Hinweise zu erklären, und bin auf die folgende Schwierigkeit gestoßen. Es scheint vielleicht überhaupt kein Problem zu sein, wenn Sie bereits wissen, wie man Zeiger verwendet, aber versuchen Sie, das folgende Beispiel mit klarem Verstand zu betrachten:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Für den absoluten Anfänger könnte die Ausgabe überraschend sein. In Zeile 2 hatte er / sie gerade * bar als & foo deklariert, aber in Zeile 4 stellte sich heraus, dass * bar tatsächlich foo statt & foo ist!

Die Verwirrung, könnte man sagen, ergibt sich aus der Mehrdeutigkeit des Symbols *: In Zeile 2 wird ein Zeiger deklariert. In Zeile 4 wird es als unärer Operator verwendet, der den Wert abruft, auf den der Zeiger zeigt. Zwei verschiedene Dinge, richtig?

Diese "Erklärung" hilft einem Anfänger jedoch überhaupt nicht. Es wird ein neues Konzept eingeführt, indem auf eine subtile Diskrepanz hingewiesen wird. Dies kann nicht der richtige Weg sein, es zu lehren.

Wie haben Kernighan und Ritchie das erklärt?

Der unäre Operator * ist der Indirektions- oder Dereferenzierungsoperator. Wenn es auf einen Zeiger angewendet wird, greift es auf das Objekt zu, auf das der Zeiger zeigt. […]

Die Deklaration des Zeigers ip int *ipist als Mnemonik gedacht; es heißt, dass der Ausdruck *ipein int ist. Die Syntax der Deklaration für eine Variable ahmt die Syntax der Ausdrücke nach, in denen die Variable möglicherweise erscheint .

int *ipsollte gelesen werden wie " *ipwird ein int" zurückgeben? Aber warum folgt dann die Zuordnung nach der Deklaration nicht diesem Muster? Was ist, wenn ein Anfänger die Variable initialisieren möchte? int *ip = 1(read: *ipgibt ein intund das intis zurück 1) funktioniert nicht wie erwartet. Das konzeptionelle Modell scheint einfach nicht kohärent zu sein. Vermisse ich hier etwas?


Bearbeiten: Es wurde versucht, die Antworten hier zusammenzufassen .

armin
quelle
15
Die beste Erklärung ist, Dinge auf Papier zu zeichnen und sie mit Pfeilen zu verbinden;)
Maroun
16
Wenn ich die Zeigersyntax erklären musste, bestand ich immer darauf, dass *eine Deklaration ein Token ist, das "einen Zeiger deklarieren" bedeutet, in Ausdrücken der Dereferenzierungsoperator ist und dass diese beiden verschiedene Dinge darstellen, die zufällig dasselbe Symbol haben (wie der Multiplikationsoperator - gleiches Symbol, andere Bedeutung). Es ist verwirrend, aber alles andere als der tatsächliche Zustand wird noch schlimmer sein.
Matteo Italia
40
Vielleicht int* barmacht es das Schreiben so, dass es offensichtlicher wird, dass der Stern tatsächlich Teil des Typs ist, nicht Teil des Bezeichners. Natürlich stößt dies auf verschiedene Probleme mit unintuitiven Dingen wie int* a, b.
Niklas B.
9
Ich habe immer gedacht, dass die K & R-Erklärung albern und unnötig ist. Die Sprache verwendet dasselbe Symbol für zwei verschiedene Dinge und wir müssen uns nur damit befassen. *kann je nach Kontext zwei verschiedene Bedeutungen haben. Genauso wie derselbe Buchstabe je nach Wort unterschiedlich ausgesprochen werden kann, was es schwierig macht, das Sprechen vieler Sprachen zu lernen. Wenn jedes einzelne Konzept / jede Operation ein eigenes Symbol hätte, würden wir viel größere Tastaturen benötigen, sodass Symbole recycelt werden, wenn dies sinnvoll ist.
Art
8
Ich bin oft auf dasselbe Problem gestoßen, als ich anderen C beigebracht habe, und meiner Erfahrung nach kann es so gelöst werden, wie es die meisten Leute hier vorgeschlagen haben. Erklären Sie zunächst das Konzept eines Zeigers ohne die C-Syntax. Lehren Sie dann die Syntax und betonen Sie das Sternchen als Teil des Typs ( int* p), während Sie Ihren Schüler davor warnen , mehrere Deklarationen in derselben Zeile zu verwenden, wenn Zeiger beteiligt sind. Wenn der Schüler das Konzept der Zeiger vollständig verstanden hat, erklären Sie dem Schüler, dass die int *pSyntax is äquivalent ist, und erklären Sie dann das Problem mit mehreren Deklarationen.
Theodoros Chatzigiannakis

Antworten:

43

Damit Ihr Schüler die Bedeutung des *Symbols in verschiedenen Kontexten verstehen kann, muss er zunächst verstehen, dass die Kontexte tatsächlich unterschiedlich sind. Sobald sie verstehen, dass die Kontexte unterschiedlich sind (dh der Unterschied zwischen der linken Seite einer Aufgabe und einem allgemeinen Ausdruck), ist es kein allzu großer kognitiver Sprung, um die Unterschiede zu verstehen.

Erklären Sie zunächst, dass die Deklaration einer Variablen keine Operatoren enthalten darf (zeigen Sie dies, indem Sie zeigen, dass das Einfügen eines -oder eines +Symbols in eine Variablendeklaration lediglich einen Fehler verursacht). Zeigen Sie dann weiter, dass ein Ausdruck (dh auf der rechten Seite einer Zuweisung) Operatoren enthalten kann. Stellen Sie sicher, dass der Schüler versteht, dass ein Ausdruck und eine Variablendeklaration zwei völlig unterschiedliche Kontexte sind.

Wenn sie verstehen, dass die Kontexte unterschiedlich sind, können Sie weiter erklären, dass das *Symbol in einer Variablendeklaration vor dem Variablenbezeichner "diese Variable als Zeiger deklarieren" bedeutet. Dann können Sie erklären, dass das *Symbol bei Verwendung in einem Ausdruck (als unärer Operator) der "Dereferenzierungsoperator" ist und "der Wert an der Adresse von" bedeutet und nicht seine frühere Bedeutung.

Um Ihren Schüler wirklich zu überzeugen, erklären Sie, dass die Ersteller von C jedes Symbol verwendet haben könnten, um den Dereferenzierungsoperator zu bezeichnen (dh sie hätten @stattdessen verwenden können), aber aus welchem ​​Grund auch immer sie die Entwurfsentscheidung getroffen haben *.

Alles in allem führt kein Weg daran vorbei zu erklären, dass die Kontexte unterschiedlich sind. Wenn der Schüler nicht versteht, dass die Kontexte unterschiedlich sind, kann er nicht verstehen, warum das *Symbol unterschiedliche Bedeutungen haben kann.

Pharap
quelle
80

Der Grund, warum die Kurzschrift:

int *bar = &foo;

In Ihrem Beispiel kann es verwirrend sein, dass es leicht falsch interpretiert werden kann als:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

wenn es tatsächlich bedeutet:

int *bar;
bar = &foo;

So geschrieben, mit getrennter Variablendeklaration und Zuordnung, besteht kein derartiges Verwirrungspotential, und die in Ihrem K & R-Zitat beschriebene Parallelität der Deklaration der Verwendung funktioniert perfekt:

  • Die erste Zeile deklariert eine Variable bar, also *bareine int.

  • Die zweite Zeile weist die Adresse von foozu bar, wodurch *bar(ein int) ein Alias ​​für foo(auch ein int) wird.

Bei der Einführung der C-Zeigersyntax für Anfänger kann es hilfreich sein, zunächst an dieser Art der Trennung von Zeigerdeklarationen von Zuweisungen festzuhalten und die kombinierte Kurzschrift-Syntax (mit entsprechenden Warnungen vor Verwechslungsgefahr) erst dann einzuführen, wenn die grundlegenden Konzepte des Zeigers verwendet werden C wurden angemessen verinnerlicht.

Ilmari Karonen
quelle
4
Ich wäre versucht typedef. typedef int *p_int;bedeutet, dass eine Variable vom Typ p_intdie Eigenschaft hat, *p_intdie ein ist int. Dann haben wir p_int bar = &foo;. Es scheint ... eine schlechte Idee zu sein, jemanden zu ermutigen, nicht initialisierte Daten zu erstellen und sie später als Standardgewohnheit zuzuweisen.
Yakk - Adam Nevraumont
6
Dies ist nur der gehirngeschädigte Stil von C-Deklarationen; Es ist nicht spezifisch für Zeiger. Bedenken Sie int a[2] = {47,11};, dass dies keine Initialisierung des (nicht existierenden) Elements a[2]ist.
Marc van Leeuwen
5
@MarcvanLeeuwen Stimmen Sie dem Hirnschaden zu. Idealerweise *sollte es Teil des Typs sein, nicht an die Variable gebunden, und dann können Sie schreiben int* foo_ptr, bar_ptr, um zwei Zeiger zu deklarieren. Aber es deklariert tatsächlich einen Zeiger und eine ganze Zahl.
Barmar
1
Es geht nicht nur um "Kurzform" -Erklärungen / Aufgaben. Das ganze Problem tritt erneut auf, sobald Sie Zeiger als Funktionsargumente verwenden möchten.
Armin
30

Kurze Erklärungen

Es ist schön, den Unterschied zwischen Deklaration und Initialisierung zu kennen. Wir deklarieren Variablen als Typen und initialisieren sie mit Werten. Wenn wir beides gleichzeitig tun, nennen wir es oft eine Definition.

1. int a; a = 42;

int a;
a = 42;

Wir erklären ein intbenanntes a . Dann initialisieren wir es, indem wir ihm einen Wert geben 42.

2. int a = 42;

Wir erklären undint benennen a und geben ihm den Wert 42. Es wird mit initialisiert 42. Eine Definition.

3. a = 43;

Wenn wir die Variablen verwenden, sagen wir , dass wir sie bearbeiten . a = 43ist eine Zuweisungsoperation. Wir weisen der Variablen a die Nummer 43 zu.

Indem ich sage

int *bar;

Wir deklarieren bar als Zeiger auf ein int. Indem ich sage

int *bar = &foo;

wir erklären bar und initialisieren es mit der adresse von foo .

Nachdem wir initialisiert haben Balken haben, können wir denselben Operator, das Sternchen, verwenden, um auf den Wert von foo zuzugreifen und ihn zu bearbeiten . Ohne den Operator greifen wir auf die Adresse zu und bearbeiten sie, auf die der Zeiger zeigt.

Außerdem habe ich das Bild sprechen lassen.

Was

Eine vereinfachte ASCIIMATION darüber, was los ist. (Und hier eine Player-Version, wenn Sie pausieren möchten usw.)

          ASCIIMATION

Morpfh
quelle
22

Die 2. Aussage int *bar = &foo;kann bildlich im Gedächtnis betrachtet werden als,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Jetzt barist ein Zeiger vom Typ intmit der Adresse &von foo. Mit dem unären Operator wird *der in 'foo' enthaltene Wert mithilfe des Zeigers abgerufen bar.

EDIT : Mein Ansatz mit Anfängern ist es, die memory addresseiner Variablen zu erklären , dh

Memory Address:Jeder Variablen ist eine vom Betriebssystem bereitgestellte Adresse zugeordnet. In int a;, &aist die Adresse der Variablen a.

Fahren Sie mit der Erläuterung grundlegender Variablentypen in Cas,

Types of variables: Variablen können Werte des jeweiligen Typs enthalten, jedoch keine Adressen.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Wie oben erwähnt, zum Beispiel Variablen

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Es ist möglich, b = aaber nicht zuzuweisen b = &a, da die Variable bden Wert, aber nicht die Adresse enthalten kann. Daher benötigen wir Zeiger .

Pointer or Pointer variables :Wenn eine Variable eine Adresse enthält, wird sie als Zeigervariable bezeichnet. Verwenden Sie *in der Deklaration, um anzuzeigen, dass es sich um einen Zeiger handelt.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable
Sunil Bojanapally
quelle
3
Das Problem ist, dass beim Lesen int *ipals "ip ist ein Zeiger (*) vom Typ int" Probleme beim Lesen von so etwas auftreten x = (int) *ip.
Armin
2
@abw Das ist etwas völlig anderes, daher die Klammern. Ich glaube nicht, dass die Leute Schwierigkeiten haben werden, den Unterschied zwischen Erklärungen und Casting zu verstehen.
Bzeaman
@abw In x = (int) *ip;erhalten Sie den Wert, indem Sie den Zeiger dereferenzieren, ipund wandeln Sie den Wert in einen intbeliebigen Typ ipum.
Sunil Bojanapally
1
@BennoZeeman Sie haben Recht: Casting und Erklärungen sind zwei verschiedene Dinge. Ich habe versucht, auf die unterschiedliche Rolle des Sterns hinzuweisen: 1. "Dies ist kein int, sondern ein Zeiger auf int" 2. "Dies gibt Ihnen das int, aber nicht den Zeiger auf int".
Armin
2
@abw: Deshalb int* bar = &foo;macht das Unterrichten viel mehr Sinn. Ja, ich weiß, dass es Probleme verursacht, wenn Sie mehrere Zeiger in einer einzelnen Deklaration deklarieren. Nein, ich denke, das ist überhaupt nicht wichtig.
Leichtigkeitsrennen im Orbit
17

Wenn man sich die Antworten und Kommentare hier ansieht, scheint es eine allgemeine Übereinstimmung zu geben, dass die fragliche Syntax für einen Anfänger verwirrend sein kann. Die meisten von ihnen schlagen etwas in diese Richtung vor:

  • Verwenden Sie Diagramme, Skizzen oder Animationen, um die Funktionsweise von Zeigern zu veranschaulichen, bevor Sie Code anzeigen.
  • Erklären Sie bei der Darstellung der Syntax die beiden unterschiedlichen Rollen des Sternsymbols . Viele Tutorials fehlen oder umgehen diesen Teil. Es kommt zu Verwirrung ("Wenn Sie eine initialisierte Zeigerdeklaration in eine Deklaration und eine spätere Zuweisung aufteilen , müssen Sie daran denken, die häufig gestellten Fragen *" - comp.lang.c zu entfernen ). Ich hatte gehofft, einen alternativen Ansatz zu finden, aber ich denke, dies ist der Fall der Weg, den man gehen sollte.

Sie können schreiben, int* baranstatt int *barden Unterschied hervorzuheben. Dies bedeutet, dass Sie nicht dem K & R-Ansatz "Declaration Mimics Use" folgen, sondern dem Stroustrup C ++ - Ansatz :

Wir erklären nicht *bar eine ganze Zahl zu sein. Wir erklären uns barzu einem int*. Wenn wir eine neu erstellte Variable in derselben Zeile initialisieren möchten, ist klar, dass es sich barnicht um eine Variable handelt *bar.int* bar = &foo;

Die Nachteile:

  • Sie müssen Ihren Schüler vor dem Problem der Deklaration mehrerer Zeiger ( int* foo, barvs int *foo, *bar) warnen .
  • Sie müssen sie auf eine vorbereiten Welt voller Verletzungen . Viele Programmierer möchten das Sternchen neben dem Namen der Variablen sehen, und sie werden große Anstrengungen unternehmen, um ihren Stil zu rechtfertigen. Viele Styleguides erzwingen diese Notation explizit (Linux-Kernel-Codierungsstil, NASA C Style Guide usw.).

Bearbeiten: Ein anderer Ansatz , der vorgeschlagen wurde, besteht darin, den K & R-Weg "nachzuahmen", jedoch ohne die "Kurzschrift" -Syntax (siehe hier) ). Sobald Sie eine Deklaration und eine Zuordnung in derselben Zeile unterlassen , sieht alles viel kohärenter aus.

Früher oder später muss sich der Schüler jedoch mit Zeigern als Funktionsargumenten befassen. Und Zeiger als Rückgabetypen. Und Zeiger auf Funktionen. Sie müssen den Unterschied zwischen int *func();und erklären int (*func)();. Ich denke, früher oder später werden die Dinge auseinanderfallen. Und vielleicht ist früher besser als später.

armin
quelle
16

Es gibt einen Grund, warum K & R-Stil int *pund Stroustrup-Stil bevorzugt werden int* p. beide sind in jeder Sprache gültig (und bedeuten dasselbe), aber wie Stroustrup es ausdrückte:

Die Wahl zwischen "int * p;" und "int * p;" Es geht nicht um richtig und falsch, sondern um Stil und Betonung. C betonte Ausdrücke; Erklärungen wurden oft nur als notwendiges Übel angesehen. C ++ hingegen legt großen Wert auf Typen.

Nun, da Sie versuchen, C hier zu unterrichten, würde dies bedeuten, dass Sie Ausdrücke stärker hervorheben sollten als Typen, aber einige Leute können eine Betonung schneller als die andere verstehen, und das betrifft sie und nicht die Sprache.

Daher fällt es einigen Menschen leichter, mit der Idee zu beginnen, dass ein int*etwas anderes ist als ein, intund von dort fortzufahren.

Wenn jemand schnell die Art und Weise des Betrachtens es nicht grok , dass Anwendungen int* barzu haben barals ein Ding , das nicht ein int ist, sondern ein Zeiger auf int, dann werden sie schnell sehen , dass *barist etwas zu tun , zu bar, und der Rest wird folgen. Sobald Sie dies getan haben, können Sie später erklären, warum C-Codierer dies bevorzugen int *bar.

Oder nicht. Wenn es einen Weg gegeben hätte, auf dem jeder das Konzept zuerst verstanden hätte, hätten Sie überhaupt keine Probleme gehabt, und der beste Weg, es einer Person zu erklären, ist nicht unbedingt der beste Weg, es einer anderen Person zu erklären.

Jon Hanna
quelle
1
Ich mag Stroustrups Argument, aber ich frage mich, warum er das Symbol & gewählt hat, um Referenzen zu kennzeichnen - eine weitere mögliche Gefahr.
Armin
1
@abw Ich denke, er hat Symmetrie gesehen, wenn wir es können, int* p = &adann können wir es tun int* r = *p. Ich bin mir ziemlich sicher, dass er es in Das Design und die Evolution von C ++ behandelt hat , aber es ist lange her, dass ich das gelesen habe, und ich habe meine Kopie törichterweise an jemanden gelehnt.
Jon Hanna
3
Ich denke du meinst int& r = *p. Und ich wette, der Kreditnehmer versucht immer noch, das Buch zu verdauen.
Armin
@abw, ja das war genau das was ich meinte. Leider führen Tippfehler in Kommentaren nicht zu Kompilierungsfehlern. Das Buch ist eigentlich ziemlich lebhaft gelesen.
Jon Hanna
4
Einer der Gründe , warum ich Pascal Syntax (wie im Volk erweitert) über C Gunst ist , dass Var A, B: ^Integer;deutlich macht , dass der Typ „Zeiger auf integer“ gilt für beide Aund B. Die Verwendung eines K&RStils int *a, *bist ebenfalls praktikabel. aber eine Erklärung wie int* a,b;sieht jedoch so aus aund bwird beide als deklariert int*, aber in Wirklichkeit deklariert sie aals int*und bals int.
Supercat
9

tl; dr:

F: Wie kann man einem Anfänger C-Zeiger (Deklaration vs. unäre Operatoren) erklären?

A: Nicht. Erklären Sie dem Anfänger Zeiger und zeigen Sie ihm, wie er seine Zeigerkonzepte anschließend in der C-Syntax darstellt.


Ich hatte kürzlich das Vergnügen, einem Anfänger der C-Programmierung Hinweise zu erklären, und bin auf die folgende Schwierigkeit gestoßen.

IMO ist die C-Syntax nicht schrecklich, aber auch nicht wunderbar: Es ist weder ein großes Hindernis, wenn Sie bereits Zeiger verstehen, noch eine Hilfe beim Erlernen dieser.

Deshalb: Erklären Sie zunächst die Zeiger und stellen Sie sicher, dass sie sie wirklich verstehen:

  • Erklären Sie sie mit Box-and-Arrow-Diagrammen. Sie können dies ohne Hex-Adressen tun. Wenn diese nicht relevant sind, zeigen Sie einfach die Pfeile an, die entweder auf ein anderes Feld oder auf ein Nullsymbol zeigen.

  • Erklären Sie mit Pseudocode: Schreiben Sie einfach die Adresse von foo und den in bar gespeicherten Wert .

  • Dann, wenn Ihr Anfänger versteht, was Zeiger sind und warum und wie man sie verwendet; Zeigen Sie dann die Zuordnung zur C-Syntax.

Ich vermute, der Grund, warum der K & R-Text kein konzeptionelles Modell liefert, ist der folgende sie bereits Hinweise verstanden haben und wahrscheinlich davon ausgegangen sind, dass dies auch jeder andere kompetente Programmierer zu dieser Zeit getan hat. Die Mnemonik ist nur eine Erinnerung an die Zuordnung vom gut verstandenen Konzept zur Syntax.

Nutzlos
quelle
Tatsächlich; Beginnen Sie zuerst mit der Theorie, die Syntax kommt später (und ist nicht wichtig). Beachten Sie, dass die Theorie der Speichernutzung nicht sprachabhängig ist. Dieses Box-and-Arrows-Modell hilft Ihnen bei Aufgaben in jeder Programmiersprache.
2.
Hier finden Sie einige Beispiele (obwohl Google auch helfen wird) eskimo.com/~scs/cclass/notes/sx10a.html
2.
7

Dieses Problem ist etwas verwirrend, wenn Sie anfangen, C zu lernen.

Hier sind die Grundprinzipien, die Ihnen beim Einstieg helfen können:

  1. In C gibt es nur wenige Grundtypen:

    • char: Ein ganzzahliger Wert mit der Größe von 1 Byte.

    • short: ein ganzzahliger Wert mit der Größe von 2 Bytes.

    • long: ein ganzzahliger Wert mit der Größe von 4 Bytes.

    • long long: ein ganzzahliger Wert mit der Größe von 8 Bytes.

    • float: Ein nicht ganzzahliger Wert mit einer Größe von 4 Bytes.

    • double: Ein nicht ganzzahliger Wert mit einer Größe von 8 Bytes.

    Beachten Sie, dass die Größe jedes Typs im Allgemeinen vom Compiler und nicht vom Standard festgelegt wird.

    Die Integer - Typen short, longund long longsind in der Regel gefolgt von int.

    Es ist jedoch kein Muss, und Sie können sie ohne die verwenden int .

    Alternativ können Sie einfach angeben int , dies kann jedoch von verschiedenen Compilern unterschiedlich interpretiert werden.

    Um dies zusammenzufassen:

    • shortist das gleiche wie, short intaber nicht unbedingt das gleiche wie int.

    • longist das gleiche wie, long intaber nicht unbedingt das gleiche wie int.

    • long longist das gleiche wie, long long intaber nicht unbedingt das gleiche wie int.

    • Auf einem bestimmten Compiler intist entweder short intoder long intoder long long int.

  2. Wenn Sie eine Variable eines Typs deklarieren, können Sie auch eine andere Variable deklarieren, die darauf verweist.

    Beispielsweise:

    int a;

    int* b = &a;

    Im Wesentlichen haben wir also für jeden Basistyp auch einen entsprechenden Zeigertyp.

    Zum Beispiel: shortund short*.

    Es gibt zwei Möglichkeiten, Variablen zu "betrachten" b (das ist wahrscheinlich das, was die meisten Anfänger verwirrt) :

    • Sie können bals Variable vom Typ betrachten int*.

    • Sie können *bals Variable vom Typ betrachten int.

    Daher würden einige Leute erklären int* b, während andere erklären würdenint *b .

    Tatsache ist jedoch, dass diese beiden Erklärungen identisch sind (die Leerzeichen sind bedeutungslos).

    Sie können entweder bals Zeiger auf einen ganzzahligen Wert oder verwenden*b als tatsächlichen spitzen ganzzahligen Wert verwenden.

    Sie können den angegebenen Wert erhalten (lesen): int c = *b .

    Und Sie können den spitzen Wert setzen (schreiben) : *b = 5.

  3. Ein Zeiger kann auf eine beliebige Speicheradresse verweisen und nicht nur auf die Adresse einer zuvor deklarierten Variablen. Sie müssen jedoch vorsichtig sein, wenn Sie Zeiger verwenden, um den Wert abzurufen oder festzulegen, der sich an der spitzen Speicheradresse befindet.

    Beispielsweise:

    int* a = (int*)0x8000000;

    Hier haben wir eine Variable, adie auf die Speicheradresse 0x8000000 zeigt.

    Wenn diese Speicheradresse nicht im Speicher Ihres Programms zugeordnet ist, führt ein Lese- oder Schreibvorgang mit *ahöchstwahrscheinlich dazu, dass Ihr Programm aufgrund einer Speicherzugriffsverletzung abstürzt.

    Sie können den Wert von sicher ändern a, aber Sie sollten sehr vorsichtig sein, wenn Sie den Wert von ändern *a.

  4. Der Typ void*ist insofern außergewöhnlich, als er keinen entsprechenden "Werttyp" hat, der verwendet werden kann (dh Sie können ihn nicht deklarieren void a). Dieser Typ wird nur als allgemeiner Zeiger auf eine Speicheradresse verwendet, ohne den Datentyp anzugeben, der sich in dieser Adresse befindet.

Barak Manos
quelle
7

Vielleicht macht es ein bisschen mehr, wenn man es ein wenig durchgeht:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Lassen Sie sich von ihnen sagen, was sie von der Ausgabe in jeder Zeile erwarten, und lassen Sie sie dann das Programm ausführen und sehen, was auftaucht. Erklären Sie ihre Fragen (die nackte Version dort wird sicherlich einige dazu veranlassen - aber Sie können sich später über Stil, Strenge und Portabilität Gedanken machen). Schreiben Sie dann eine Funktion, die einen Wert annimmt, und dieselbe, die einen Zeiger nimmt, bevor sich ihr Geist vom Überdenken zu Brei verwandelt oder sie zu einem Zombie nach dem Mittagessen werden.

Nach meiner Erfahrung geht es darum, "warum druckt das so?" Buckel, und dann sofort zu zeigen, warum dies in Funktionsparametern nützlich ist, indem man praktisch spielt (als Auftakt zu einigen grundlegenden K & R-Materialien wie String-Parsing / Array-Verarbeitung), wodurch die Lektion nicht nur Sinn macht, sondern bleibt.

Der nächste Schritt besteht darin , sie zu bekommen , um zu erklären Sie , wie i[0]bezieht sich auf&i . Wenn sie das können, werden sie es nicht vergessen und Sie können anfangen, über Strukturen zu sprechen, sogar ein wenig im Voraus, nur damit es einsinkt.

Die obigen Empfehlungen zu Kästchen und Pfeilen sind ebenfalls gut, können aber auch zu einer ausführlichen Diskussion über die Funktionsweise des Gedächtnisses führen. Dies ist ein Gespräch, das irgendwann stattfinden muss, aber von dem unmittelbar ablenkenden Punkt ablenken kann : wie man die Zeigernotation in C interpretiert.

zxq9
quelle
Dies ist eine gute Übung. Aber das Thema, das ich ansprechen wollte, ist ein spezifisches syntaktisches, das sich auf das mentale Modell auswirken kann, das die Schüler entwickeln. Beachten Sie Folgendes : int foo = 1;. Nun ist das in Ordnung : int *bar; *bar = foo;. Dies ist nicht in Ordnung:int *bar = foo;
Armin
1
@abw Das einzige, was Sinn macht, ist, was sich die Schüler am Ende selbst sagen. Das bedeutet "eins sehen, eins tun, eins lehren". Sie können nicht schützen oder vorhersagen, welche Syntax oder welchen Stil sie im Dschungel sehen werden (sogar Ihre alten Repos!), Daher müssen Sie genügend Permutationen zeigen, damit die Grundkonzepte unabhängig vom Stil verstanden werden - und Fangen Sie dann an, ihnen beizubringen, warum bestimmte Stile festgelegt wurden. Wie das Unterrichten von Englisch: Grundausdruck, Redewendungen, Stile, bestimmte Stile in einem bestimmten Kontext. Leider nicht einfach. Auf jeden Fall viel Glück!
zxq9
6

Der Typ des Ausdrucks *bar ist int; Somit wird der Typ der variablen (und Expression) barist int *. Da die Variable einen Zeigertyp hat, muss ihr Initialisierer auch einen Zeigertyp haben.

Es besteht eine Inkonsistenz zwischen der Initialisierung und Zuweisung von Zeigervariablen. Das ist nur etwas, was man auf die harte Tour lernen muss.

John Bode
quelle
3
Wenn ich mir die Antworten hier ansehe, habe ich das Gefühl, dass viele erfahrene Programmierer das Problem nicht einmal mehr sehen können. Ich denke, das ist ein Nebenprodukt des "Lernens, mit Inkonsistenzen zu leben".
Armin
3
@abw: Die Regeln für die Initialisierung unterscheiden sich von den Regeln für die Zuweisung. Für skalare arithmetische Typen sind die Unterschiede vernachlässigbar, für Zeiger- und Aggregattypen jedoch von Bedeutung. Das müssen Sie zusammen mit allem anderen erklären.
John Bode
5

Ich würde es lieber lesen, als das erste *gilt für intmehr als bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
Grorel
quelle
2
Dann muss man erklären, warum int* a, bman nicht das tut, was man denkt.
Pharap
4
Stimmt, aber ich denke nicht, dass int* a,bdas überhaupt verwendet werden sollte. Zur besseren Lesbarkeit, Aktualisierung usw. sollte es nur eine Variablendeklaration pro Zeile geben und niemals mehr. Dies ist auch Anfängern zu erklären, selbst wenn der Compiler damit umgehen kann.
Grorel
Das ist jedoch die Meinung eines Mannes. Es gibt Millionen von Programmierern, die völlig in Ordnung sind, mehr als eine Variable pro Zeile zu deklarieren und dies täglich als Teil ihrer Arbeit zu tun. Sie können die Schüler nicht vor alternativen Methoden verstecken. Es ist besser, ihnen alle Alternativen zu zeigen und sie entscheiden zu lassen, in welche Richtung sie Dinge tun möchten, denn wenn sie jemals angestellt werden, wird von ihnen erwartet, dass sie einem bestimmten Stil folgen Sie können sich wohl fühlen oder auch nicht. Vielseitigkeit ist für einen Programmierer eine sehr gute Eigenschaft.
Pharap
1
Ich stimme @grorel zu. Es ist einfacher, sich *als Teil des Typs vorzustellen und einfach zu entmutigen int* a, b. Es sei denn, Sie sagen lieber, dass dies eher ein *aTyp ist intals aein int
Hinweis
@grorel ist richtig: int *a, b;sollte nicht verwendet werden. Das Deklarieren von zwei Variablen mit unterschiedlichen Typen in derselben Anweisung ist eine ziemlich schlechte Praxis und ein starker Kandidat für Wartungsprobleme auf der ganzen Linie. Vielleicht ist es anders für diejenigen von uns, die im eingebetteten Bereich arbeiten, wo ein int*und ein intoft unterschiedlich groß sind und manchmal an völlig unterschiedlichen Speicherorten gespeichert werden. Es ist einer der vielen Aspekte der C-Sprache, die am besten als "es ist erlaubt, aber tu es nicht" gelehrt werden.
Evil Dog Pie
5
int *bar = &foo;

Question 1: Was ist bar?

Ans: Es ist eine Zeigervariable (zu tippen int). Ein Zeiger sollte auf einen gültigen Speicherort zeigen und später mit einem unären Operator dereferenziert werden (* bar) *, um den an diesem Speicherort gespeicherten Wert zu lesen.

Question 2: Was ist &foo?

Ans: foo ist eine Variable vom Typ., intdie an einem gültigen Speicherort gespeichert ist, und an diesem Speicherort erhalten wir sie vom Operator. &Jetzt haben wir also einen gültigen Speicherort &foo.

Also beide zusammen, dh was der Zeiger brauchte, war ein gültiger Speicherort und das wird dadurch erreicht, &foodass die Initialisierung gut ist.

Jetzt zeigt der Zeiger barauf einen gültigen Speicherort, und der darin gespeicherte Wert kann abgerufen werden, dh*bar

Gopi
quelle
5

Sie sollten einen Anfänger darauf hinweisen, dass * in der Deklaration und im Ausdruck unterschiedliche Bedeutungen hat. Wie Sie wissen, ist * im Ausdruck ein unärer Operator und * in der Deklaration kein Operator und nur eine Art Syntax, die mit type kombiniert wird, um dem Compiler mitzuteilen, dass es sich um einen Zeigertyp handelt. Es ist besser, einem Anfänger zu sagen: "* hat eine andere Bedeutung. Um die Bedeutung von * zu verstehen, sollten Sie herausfinden, wo * verwendet wird."

Yongkil Kwon
quelle
4

Ich denke der Teufel ist im Raum.

Ich würde schreiben (nicht nur für den Anfänger, sondern auch für mich selbst): int * bar = & foo; anstelle von int * bar = & foo;

Dies sollte deutlich machen, in welchem ​​Verhältnis Syntax und Semantik stehen

rpaulin56
quelle
4

Es wurde bereits festgestellt, dass * mehrere Rollen hat.

Es gibt eine andere einfache Idee, die einem Anfänger helfen kann, Dinge zu erfassen:

Denken Sie, dass "=" auch mehrere Rollen hat.

Wenn die Zuweisung in derselben Zeile wie die Deklaration verwendet wird, stellen Sie sich dies als Konstruktoraufruf vor, nicht als willkürliche Zuweisung.

Wenn du siehst:

int *bar = &foo;

Denken Sie, dass es fast gleichbedeutend ist mit:

int *bar(&foo);

Klammern haben Vorrang vor Sternchen, daher wird "& foo" viel einfacher intuitiv "bar" als "* bar" zugeordnet.

Morfizm
quelle
4

Ich habe diese Frage vor ein paar Tagen gesehen und dann zufällig die Erklärung von Go's Typdeklaration im Go Blog gelesen . Zunächst wird ein Bericht über Deklarationen vom Typ C erstellt, der als nützliche Ressource zum Hinzufügen zu diesem Thread erscheint, obwohl ich denke, dass bereits vollständigere Antworten gegeben wurden.

C verfolgte einen ungewöhnlichen und cleveren Ansatz bei der Deklarationssyntax. Anstatt die Typen mit einer speziellen Syntax zu beschreiben, schreibt man einen Ausdruck, der das deklarierte Element betrifft, und gibt an, welchen Typ dieser Ausdruck haben wird. So

int x;

deklariert x als int: Der Ausdruck 'x' hat den Typ int. Um herauszufinden, wie der Typ einer neuen Variablen geschrieben wird, schreiben Sie im Allgemeinen einen Ausdruck mit dieser Variablen, die zu einem Basistyp ausgewertet wird, und setzen Sie dann den Basistyp links und den Ausdruck rechts.

Also die Erklärungen

int *p;
int a[3];

Geben Sie an, dass p ein Zeiger auf int ist, weil '* p' den Typ int hat, und dass a ein Array von Ints ist, weil a [3] (ohne Berücksichtigung des bestimmten Indexwerts, der als Größe des Arrays bezeichnet wird) den Typ hat int.

(Es wird weiter beschrieben, wie dieses Verständnis auf Funktionszeiger usw. erweitert werden kann.)

Dies ist ein Weg, über den ich vorher noch nicht nachgedacht habe, aber es scheint ein ziemlich einfacher Weg zu sein, um die Überlastung der Syntax zu erklären.

Andy Turner
quelle
3

Wenn das Problem die Syntax ist, kann es hilfreich sein, äquivalenten Code mit template / using anzuzeigen.

template<typename T>
using ptr = T*;

Dies kann dann als verwendet werden

ptr<int> bar = &foo;

Vergleichen Sie danach die normale / C-Syntax mit diesem C ++ - Ansatz. Dies ist auch nützlich, um const-Zeiger zu erklären.

MI3Guy
quelle
2
Für Anfänger wird es viel verwirrender sein.
Karsten
Mein allerdings war, dass Sie die Definition von ptr nicht zeigen würden. Verwenden Sie es einfach für Zeigerdeklarationen.
MI3Guy
3

Die Quelle der Verwirrung ergibt sich aus der Tatsache, dass das *Symbol in C unterschiedliche Bedeutungen haben kann, abhängig von der Tatsache, in der es verwendet wird. Um einem Anfänger den Zeiger zu erklären, sollte die Bedeutung des *Symbols in einem anderen Kontext erklärt werden.

In der Erklärung

int *bar = &foo;  

Das *Symbol ist nicht der Indirektionsoperator . Stattdessen hilft es, den Typ von anzugebenbar Information des Compilersbar ein Zeiger auf eine istint . Wenn es andererseits in einer Anweisung erscheint, führt das *Symbol (wenn es als unärer Operator verwendet wird ) eine Indirektion durch. Daher die Aussage

*bar = &foo;

wäre falsch, da es die Adresse von zuweist foo dem Objektbar auf sich barselbst zeigt, nicht auf sich selbst.

Haccks
quelle
3

"Vielleicht macht das Schreiben als int * bar deutlicher, dass der Stern tatsächlich Teil des Typs ist, nicht Teil des Bezeichners." So ich mache. Und ich sage, dass es so etwas wie Type ist, aber nur für einen Zeigernamen.

"Natürlich stößt man dabei auf verschiedene Probleme mit nicht intuitiven Dingen wie int * a, b."

Павел Бивойно
quelle
2

Hier muss man die Compiler-Logik verwenden, verstehen und erklären, nicht die menschliche Logik (ich weiß, Sie sind ein Mensch, aber hier müssen Sie den Computer nachahmen ...).

Wenn du schreibst

int *bar = &foo;

Der Compiler gruppiert das als

{ int * } bar = &foo;

Das heißt: Hier ist eine neue Variable, ihr Name ist bar, ihr Typ ist Zeiger auf int und ihr Anfangswert ist &foo.

Und Sie müssen hinzufügen: Das =Obige bezeichnet eine Initialisierung, keine Beeinflussung, während dies in den folgenden Ausdrücken der *bar = 2;Fall ist eine Affektiertheit

Per Kommentar bearbeiten:

Achtung: Bei Mehrfachdeklaration *bezieht sich das nur auf die folgende Variable:

int *bar = &foo, b = 2;

bar ist ein Zeiger auf int, der durch die Adresse von foo initialisiert wurde, b ist ein int, das auf 2 und in initialisiert wurde

int *bar=&foo, **p = &bar;

Balken im Standbildzeiger auf int und p ist ein Zeiger auf einen Zeiger auf ein int, das auf die Adresse oder den Balken initialisiert wurde.

Serge Ballesta
quelle
2
Eigentlich gruppiert der Compiler es nicht so: int* a, b;deklariert a als Zeiger auf a int, aber b als an int. Das *Symbol hat nur zwei unterschiedliche Bedeutungen: In einer Deklaration gibt es einen Zeigertyp an, und in einem Ausdruck ist es der unäre Dereferenzierungsoperator.
tmlen
@tmlen: Was ich damit gemeint habe ist, dass bei der Initialisierung das *mit dem Typ verknüpft ist, so dass der Zeiger initialisiert wird, während bei einer Affektaktion der spitze Wert betroffen ist. Aber wenigstens hast du mir einen schönen Hut gegeben :-)
Serge Ballesta
0

Grundsätzlich ist der Zeiger keine Array-Anzeige. Anfänger denken leicht, dass Zeiger wie Array aussieht. Die meisten Zeichenfolgenbeispiele verwenden die

"char * pstr" sieht ähnlich aus

"char str [80]"

Wichtig ist jedoch, dass Pointer in der unteren Ebene des Compilers nur als Ganzzahl behandelt wird.

Schauen wir uns Beispiele an ::

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Die Ergebnisse werden dies mögen 0x2a6b7ed0 ist die Adresse von str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Denken Sie also grundsätzlich daran, dass der Zeiger eine Art Ganzzahl ist. Präsentation der Adresse.

cpplover - Slw Essencial
quelle
-1

Ich würde erklären, dass Ints Objekte sind, ebenso wie Floats usw. Ein Zeiger ist ein Objekttyp, dessen Wert eine Adresse im Speicher darstellt (daher ist ein Zeiger standardmäßig NULL).

Wenn Sie zum ersten Mal einen Zeiger deklarieren, verwenden Sie die Typ-Zeiger-Name-Syntax. Es wird als "Ganzzahlzeiger namens Name, der auf die Adresse eines beliebigen Ganzzahlobjekts verweisen kann" gelesen. Wir verwenden diese Syntax nur während der Dekleration, ähnlich wie wir ein int als 'int num1' deklarieren, aber wir verwenden 'num1' nur, wenn wir diese Variable verwenden möchten, nicht 'int num1'.

int x = 5; // ein ganzzahliges Objekt mit einem Wert von 5

int * ptr; // standardmäßig eine Ganzzahl mit dem Wert NULL

Um einen Zeiger auf eine Adresse eines Objekts zu zeigen, verwenden wir das Symbol '&', das als "Adresse von" gelesen werden kann.

ptr = & x; // Jetzt ist Wert die Adresse von 'x'

Da der Zeiger nur die Adresse des Objekts ist, müssen wir das Symbol '*' verwenden, um den tatsächlichen Wert an dieser Adresse zu erhalten. Wenn er vor einem Zeiger verwendet wird, bedeutet dies "den Wert an der Adresse, auf die durch gezeigt wird".

std :: cout << * ptr; // drucke den Wert an der Adresse aus

Sie können kurz erklären, dass ' ' ein 'Operator' ist, der unterschiedliche Ergebnisse mit unterschiedlichen Objekttypen zurückgibt. Bei Verwendung mit einem Zeiger bedeutet der Operator ' ' nicht mehr "multipliziert mit".

Es ist hilfreich, ein Diagramm zu zeichnen, das zeigt, wie eine Variable einen Namen und einen Wert hat und ein Zeiger eine Adresse (den Namen) und einen Wert hat, und zu zeigen, dass der Wert des Zeigers die Adresse des int ist.

user2796283
quelle
-1

Ein Zeiger ist nur eine Variable zum Speichern von Adressen.

Der Speicher in einem Computer besteht aus Bytes (ein Byte besteht aus 8 Bits), die sequentiell angeordnet sind. Jedem Byte ist eine Nummer zugeordnet, genau wie Index oder Index in einem Array, die als Adresse des Bytes bezeichnet wird. Die Adresse des Bytes beginnt bei 0 bis eins weniger als die Speichergröße. Zum Beispiel gibt es in einem 64 MB RAM 64 * 2 ^ 20 = 67108864 Bytes. Daher beginnt die Adresse dieser Bytes von 0 bis 67108863.

Geben Sie hier die Bildbeschreibung ein

Mal sehen, was passiert, wenn Sie eine Variable deklarieren.

int Marken;

Wie wir wissen, belegt ein int 4 Datenbytes (vorausgesetzt, wir verwenden einen 32-Bit-Compiler), reserviert der Compiler 4 aufeinanderfolgende Bytes aus dem Speicher, um einen ganzzahligen Wert zu speichern. Die Adresse des ersten Bytes der 4 zugewiesenen Bytes wird als Adresse der Variablenmarkierungen bezeichnet. Angenommen, die Adresse von 4 aufeinanderfolgenden Bytes lautet 5004, 5005, 5006 und 5007, dann lautet die Adresse der variablen Markierungen 5004. Geben Sie hier die Bildbeschreibung ein

Zeigervariablen deklarieren

Wie bereits erwähnt, ist ein Zeiger eine Variable, die eine Speicheradresse speichert. Wie bei allen anderen Variablen müssen Sie zuerst eine Zeigervariable deklarieren, bevor Sie sie verwenden können. So können Sie eine Zeigervariable deklarieren.

Syntax: data_type *pointer_name;

Datentyp ist der Typ des Zeigers (auch als Basistyp des Zeigers bekannt). Zeigername ist der Name der Variablen, die eine beliebige gültige C-Kennung sein kann.

Nehmen wir einige Beispiele:

int *ip;

float *fp;

int * ip bedeutet, dass ip eine Zeigervariable ist, die auf Variablen vom Typ int zeigen kann. Mit anderen Worten, eine Zeigervariable ip kann nur die Adresse von Variablen vom Typ int speichern. Ebenso kann die Zeigervariable fp nur die Adresse einer Variablen vom Typ float speichern. Der Variablentyp (auch als Basistyp bezeichnet) ip ist ein Zeiger auf int und der Typ fp ist ein Zeiger auf float. Eine Zeigervariable vom Typ Zeiger auf int kann symbolisch als (int *) dargestellt werden. In ähnlicher Weise kann eine Zeigervariable vom Typ Zeiger auf float als (float *) dargestellt werden.

Nach dem Deklarieren einer Zeigervariablen besteht der nächste Schritt darin, ihr eine gültige Speicheradresse zuzuweisen. Sie sollten niemals eine Zeigervariable verwenden, ohne ihr eine gültige Speicheradresse zuzuweisen, da sie unmittelbar nach der Deklaration einen Müllwert enthält und möglicherweise auf eine beliebige Stelle im Speicher verweist. Die Verwendung eines nicht zugewiesenen Zeigers kann zu einem unvorhersehbaren Ergebnis führen. Es kann sogar zum Absturz des Programms führen.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Quelle: thecguru ist bei weitem die einfachste und doch detaillierteste Erklärung, die ich je gefunden habe.

Cody
quelle