Das verwirrende char in C/C++



  • Bashar schrieb:

    Nein, da printf einen Zeiger auf const char* erwartet, keinen Zeiger auf ein char-Array.

    Muss das ein C-Compiler als einen Fehler ansehen?



  • Ich glaube nicht. Der gcc schmeißt bei sowas Warnungen. Aber wir reden ja hier nicht über C, sondern "C/C++" 🤡



  • Bashar schrieb:

    Aber wir reden ja hier nicht über C, sondern "C/C++" 🤡

    🙂



  • Zeiger und Arrays, eine Geschichte voller Missverständnisse. Ich schätze, das liegt daran, dass viele versuchen, C bzw C++ nur anhand von bescheidenen Tutorials / Beispielen zu lernen und damit all die echten Regeln und Sonderregeln nur von den schlechten/falschen Erklärungen in den bescheidenen Tutorials bzw den Beispielen her nicht ableiten können.

    Zunächst sollte man erst einmal Arrays richtig verstehen. Die funktionieren auch schon ganz anders als in Sprachen wie zB Java. In Java sind Variablen eines Array-Typs selbst nur Referenzen. Sie speichern die nötige Information (zB Adresse), um das eigentliche Array erreichen zu können. Java fügt dazu noch Metainformationen dazu, so dass sich die Länge eines Arrays abfragen lässt. In C und C++ sieht das anders aus. Da ist die Variable selbst das Array und vergleichbar mit "normalen" Variablen, die sozusagen nur einelementige Arrays sind. Die einzelnen Elemente stehen dabei hintereinander im Speicher. Damit ist auch klar, dass Arrays, die als Datenelement in einer Klasse leben oder im automatischen Speicherbereich, eine zur Compile-Zeit festgelegte Länge haben müssen. Wenn man das dynamisch haben will, muss man explizit das machen, was bei anderen Sprachen implizit passiert: Indirektion, Freispeichernutzung.

    Wenn das soweit klar ist, könnte man sich mit der Initialisierung beschäftigen. Das knann so aussehen:

    void foo() {
      double s[8] = {1,2,3};            // #1
      double t[] = {5,6,7};             // #2
      char x[] = {'H','e','l','l','o'}; // #3
      char y[] = "Hello";               // #4
    }
    

    #1: s ist ein 8-elementiges double Array mit den Werten 1,2,3,0,0,0,0,0
    #2: t ist ein 3-elementiges double Array. Der Compiler hat für uns die Elemente gezählt und den Typ double[] zu double[3] vervollständigt.
    #3: x ist ein 5-elementiges char-Array. Die in Hochkommas eingescholssenen Zeichen sind Zeichenliterale
    #4: y ist ein 6-elementiges char-Array. Das 6. Element ist der Nullterminator. Diese Initialisierung ist eine Sonderregel für char-Arrays.
    Weder s,t,x noch y sind Zeiger

    Zeichenkettenliterale: Eine mit Anführungszeichen eingeschlossene Zeichenkette ist ein Zeichenkettenliteral. Es bezieht sich auf ein konstantes char-Array, welches als letztes Element einen Nullterminator besitzt. "abc" ist also ein ("Lvalue-Ausdruck" vom Typ) const char[4]. Dieses Array lebt im statischen Speicherbereich. Es ist also schon die ganze Zeit während das Programm läuft existent.

    Array-to-Pointer-Decay. Verwendet man einen Ausdruck, der sich auf ein Array bezieht, kann je nach Kontext eine implizite Konvertierung zu einem Zeiger stattfinden:

    void foo() {
      double x[4] = {0};
      double *p = &x[0];   // p ist ein Zeiger auf das erste Element von x
      double *q = x;  // Auch q ist ein Zeiger auf das erste Element von x
    }
    

    Letztere Zeigerinitialisierung macht vom dem "Array-to-Pointer-Decay" gebrauch. Daher kann man auch so etwas schreiben:

    void foo() {
       // implizite array->Zeiger Konvertierung...
       const char *sz =  "Hello World";    
    
    // // explizites Adresse des ersten Elements holen...
    // const char *sz = &"Hello World"[0]; 
    }
    

    sz ist ein Zeiger auf eine konstante Zeichenkette, die im statischen Speicher lebt und nicht verändert werden kann/darf.

    Schließlich gibt es noch eine Sonderregel, die Anfänger wahrscheinlich total durcheinander bringt, wenn sie versuchen, sich das Verhalten von Arrays und Zeigern selbst zusammenzureimen. Folgende drei Funktionsdeklarationen sind äquivalent:

    void bar(double t[8]);
    void bar(double t[]);
    void bar(double *t);
    

    Arrays kann man nicht wie "normale Variablen", zuweisen oder kopieren. Man kann sie daher auch nicht direkt an Funktionen als Kopie geben ider über Funktionen zurückgeben lassen. Die Funktion bar wurde hier so deklariert, dass sie einen Zeiger als Parameter bekommt. Der Compiler ersetzt nämlich bei den Funktionsparametern das top-level Array durch einen Zeigertypen. Die Größenangabe wird dabei auch ignoriert. Man kann aber Arrays in structs und classes verpacken und sie dann "normal behandeln":

    struct array {
      int elemente[10];
    };
    
    array quelle();
    void senke(array);
    

    Alles andere ergibt sich aus den obigen Erklärungen. Wenn man also mit "Arras von Arrays" zu tun hat, dann müsste man mit obigen Erklärungen eigentlich gerüstet sein

    void auweia() {
      double matrix[3][5];
      double (*pm)[3][5] = &matrix;
      (*pm)[1][2] = 42;
      double *q = matrix[1];
      cout << q[2] << '\n';
    }
    

    u.s.w. (ungetestet)

    Noch ein Wort zu den Prioritäten der Operatoren: Postfix-Operatoren binden stärker als Prefix-Operatoren. Das heißt:

    double *x[3];   // x ist ein Array bestehend aus 3 Zeigern auf double
    double (*x)[3]; // x ist ein Zeiger auf ein 3-elementiges double-Array
    

    In reinem C gibt es außer char* und char[N] nichts, was einer Zeichenkettenvariablen nahe kommt. Einige bauen sich über Structs und Funktionen eine eigene Zeichenketten-Abstraktion, die dynamisch reservierten Speicher verwaltet. In C++ sieht das genauso aus, nur mit dem Unterschied, dass C++ über Klassen, Konstruktoren, Operatorüberladung etc die Nutzung von selbstgebauten Typen stark vereinfachen kann. Die Standardbibliothek bietet hier den Typ std::string (und std::wstring) an. Objekte dieses Type speichern Zeichenketten (indirekt) und können wie andere normale Variablen (int, double, etc) behandelt werden, was das Kopieren und Zuweisen angeht. Wir sprechen hier oft von "Werttypen" (besitzen keine Referenzsemantik).

    Gruß,
    kk



  • krümelkacker schrieb:

    void foo() {
      double s[8] = {1,2,3};        // #1
      double d[] = {5,6,7};         // #2
      char x[] = {'H','e','l','o'}; // #3
      char y[] = "Hello";           // #4
    }
    

    [...]
    Weder s,t,x noch y sind Zeiger

    intern schon. Für den Compiler sind diese Zeiger nur eben mit viel "syntaktischem Zucker" aufbereitet, der in Deinem Posting umfassend erläutert wird.



  • !rr!rr_. schrieb:

    Weder s,t,x noch y sind Zeiger

    intern schon. Für den Compiler sind diese Zeiger nur eben mit viel "syntaktischem Zucker" aufbereitet, der in Deinem Posting umfassend erläutert wird.

    Auch intern nicht.



  • krümelkacker schrieb:

    void foo() {
      double s[8] = {1,2,3};        /* #1 */
      double d[] = {5,6,7};         /* #2 */
      char x[] = {'H','e','l','o'}; /* #3 */
      char y[] = "Hello";           /* #4 */
    }
    

    #3: x ist ein 5-elementiges char-Array. Die in Hochkommas eingescholssenen

    #3: x ist ein 4-elementiges char-Array.



  • !rr!rr_. schrieb:

    krümelkacker schrieb:

    void foo() {
      double s[8] = {1,2,3};        // #1
      double t[] = {5,6,7};         // #2
      char x[] = {'H','e','l','o'}; // #3
      char y[] = "Hello";           // #4
    }
    

    [...]
    Weder s,t,x noch y sind Zeiger

    intern schon.

    s,t,x,y sind keine Zeiger. Punkt. Sich hier s,t,x,y als Zeiger vorzustellen führt nur zur Verwirrung bzw zu bösen Überraschungen.

    Wutz schrieb:

    #3: x ist ein 4-elementiges char-Array.

    Danke. Hatte das 'l' vergessen.



  • Ein grosses Lob an dich krümelkacker für die ausführliche Erklärung.
    Auch wenn es immer noch einige Zweifler gibt, denke ich, dass es für einen Großteil der Anfänger Licht ins Dunkel bringen wird.

    Mich verwirrt allerdings, dass du "abc" als lvalue-Ausdruck bezeichnest.

    ------------------

    P.S.:
    Nachtrag zu meinem Nachredner:

    wxSkip schrieb:

    krümelkacker schrieb:

    "abc" ist also ein ("Lvalue-Ausdruck" vom Typ) const char[4].

    Warum sind const-Variablen lvalues, wenn sie doch (ohne Cast) gar nicht auf der linken Seite einer Zuweisung stehen dürfen?

    lvalue heisst nicht, dass etwas auf der linken Seite des assignment-operators stehen darf.

    const int wert = 5;
    

    wert ist ein lvalue - kann aber nicht auf der linken Seite stehen. 😃



  • krümelkacker schrieb:

    "abc" ist also ein ("Lvalue-Ausdruck" vom Typ) const char[4].

    Warum sind const-Variablen lvalues, wenn sie doch (ohne Cast) gar nicht auf der linken Seite einer Zuweisung stehen dürfen?

    EDIT: 19 Sekunden zu spät 😃



  • Weil die Definition von 'lvalue' nicht davon abhängt, ob etwas auf der linken Seite einer Zuweisung stehen darf. Das mag die ursprüngliche Wortbildung gewesen sein. Man fährt aber wahrscheinlich besser, wenn man lvalue als 'location value' liest.



  • krümelkacker schrieb:

    s,t,x,y sind keine Zeiger. Punkt. Sich hier s,t,x,y als Zeiger vorzustellen führt nur zur Verwirrung bzw zu bösen Überraschungen.

    Ich glaube nicht, daß man C wirklich verstehen kann, ohne sich zu vergegenwärtigen, daß a[..] nunmal "syntaktischer Zucker" für Zeiger und Adreßarithmetik ist.

    Da ist doch nichts dabei - Zeiger und Adreßarithmetik laufen auf der CPU nunmal schneller, als ein "echter" Datentyp "array" laufen würde, der z.B. bei jedem Lese- und Schreib-Zugriff auf ein array-Element den Test auf Index-Grenzen durchführen würde.



  • inter2k3 schrieb:

    Mich verwirrt allerdings, dass du "abc" als lvalue-Ausdruck bezeichnest.

    Ich antworte mir mal selber.
    Wenn ich mir das recht überlege müsste das String-Literal / Zeichenketten-Literal "abc" tatsächlich ein lvalue sein.
    Grund dafür ist eben die von krümelkacker beschriebene Eigenschaft, dass das array implizit zu einem Pointer zerfällt (array-to-pointer-decay).

    Daher ist folgendes auch problemlos möglich:

    std::cout<<&"Hallo";
    

    Was lerne ich daraus? Erst denken - dann fragen 😃

    Nachtrag zum Vorredner:

    !rr!rr_. schrieb:

    Ich glaube nicht, daß man C wirklich verstehen kann, ohne sich zu vergegenwärtigen, daß a[..] nunmal "syntaktischer Zucker" für Zeiger und Adreßarithmetik ist.

    Fakt ist, dass es sich um arrays handelt und nicht um Zeiger. Soweit sind wir uns hoffentlich einig.
    Greifst du nun mit arrayname[index] auf ein Element zu, passiert tatsächlich nichts anderes, als besagter Zerfall zu einem pointer, gefolgt von pointer-arithmetik und danach die Indirektion. Da hast du natürlich absolut recht.

    const char name[] = "Hans";
    std::cout<<name[2];
    

    ist äquivalent zu

    const char name[] = "Hans";
    std::cout<<*(name+2);
    

    Trotzdem ist das Zeichenketten-Literal "Hans" vom Typ char const [5].



  • @rr_rr!-wie auch immer 😃
    Im Grunde genommen machst du ja auch mit Adressen rum, wenn du auf ganz normale Variablen zugreifst. Bloß halt nicht explizit, der Compiler macht das automatisch fü dich. Arrays liegen wie normale Variablen auf dem Stack und nicht wie bei dynamischen Arrays der Pointer aufs erste Element. Manchmal brauchst du aber diesen den Pointer, und den kannst du dir über den Stack-Pointer ausrechnen. Auch das macht der Compiler in bestimmten Fällen implizit automatisch, ansonsten rechnet er ihn dir immer noch bei explizitem Casting oder Addressenholen aus.



  • !rr!rr_. schrieb:

    Ich glaube nicht, daß man C wirklich verstehen kann, ohne sich zu vergegenwärtigen, daß a[..] nunmal "syntaktischer Zucker" für Zeiger und Adreßarithmetik ist.

    Genau andersrum. Man wird C nicht verstehen, wenn man das glaubt. Weil es schlicht und einfach nicht der Fall ist.

    Da ist doch nichts dabei - Zeiger und Adreßarithmetik laufen auf der CPU nunmal schneller, als ein "echter" Datentyp "array" laufen würde, der z.B. bei jedem Lese- und Schreib-Zugriff auf ein array-Element den Test auf Index-Grenzen durchführen würde.

    Das ist das, was mit "Array-to-Pointer-Decay" gemeint ist.

    Aber zuerst ist das Array da, und erst nach diesem "Zerfall" kann es als Pointer behandelt werden. Wenn man von vornherein annimmt, dass ein Array ein Pointer ist, wird man nicht in der Lage sein zu verstehen, was es mit Arrays vor dem Zerfall auf sich hat.



  • Bashar schrieb:

    !rr!rr_. schrieb:

    Ich glaube nicht, daß man C wirklich verstehen kann, ohne sich zu vergegenwärtigen, daß a[..] nunmal "syntaktischer Zucker" für Zeiger und Adreßarithmetik ist.

    Genau andersrum. Man wird C nicht verstehen, wenn man das glaubt. Weil es schlicht und einfach nicht der Fall ist.

    dann nenne doch mal eine Situation, in welcher mit einem Array hantiert wird, wobei nicht nur Zeiger + Adreßarithmetik im Spiel sind ?



  • @ !rr!rr_
    Nur weil man in vielen Situationen die implizite Umwandlung von Arrays zu Zeigern benutzt, heisst das noch lange nicht, dass es sich um dasselbe Konzept handelt. Arrays sind keine Zeiger. Darum sollte man sie sich auch nicht als solche vorstellen. Denn Situationen, wo sich beide Konzepte komplett verschieden verhalten, gibt es genug.

    C++ geht da sogar noch einen Schritt weiter und bietet Klassen an, die dem Grundkonzept Array (Speicherung einer Folge von Werten, ganz ohne Zeiger) näher kommen, aber intern genau gleich wie gewöhnliche C-Arrays arbeiten.



  • !rr!rr_. schrieb:

    dann nenne doch mal eine Situation, in welcher mit einem Array hantiert wird, wobei nicht nur Zeiger + Adreßarithmetik im Spiel sind ?

    Moment mal, du versuchst deine ursprüngliche Aussage abzuschwächen. Du hattest behauptet, ein Array wäre nichts weiter als "syntaktischer Zucker" für einen Pointer. Dass da auf Maschinenebene immer nur mit Adressen rumgerechnet wird hat niemand bestritten.

    Ich werde dir ein Beispiel geben, das zeigt, dass Arrays keine Pointer sind. Man sieht relativ häufig, dass Anfänger sowas versuchen:

    void f(int **p);
    
    int A[10][10];
    f(A);
    

    Hat ja Logik, wenn ein Array ein Pointer ist, dann muss ein 2D-Array ein Doppelpointer sein. Leider erzählt ihnen der Compiler was von nicht passenden Typen und sie fragen hier, wie man das casten muss. Was erzählst du ihnen?



  • Nexus schrieb:

    Arrays sind keine Zeiger.

    ja, wer behauptet denn das Gegenteil? 😮

    Ich sagte, Arrays seien "syntaktischer Zucker" für Zeiger und Adreßarithmetik. Intern (Maschinenebene) sind es dann letztendlich doch Zeiger, mit denen hantiert wird.



  • Arrays sind ein Speicherbereich, keine Zeiger.


Anmelden zum Antworten