Das verwirrende char in C/C++


  • Mod

    Invisible schrieb:

    char *c, ein Pointer auf eine Zeichenkette, die allerdings only readable ist, also eine Konstate (in JAVA final) | besserer Stil wäre also const char *c

    Nein. char *c ist ein Zeiger auf ein (oder mehrere) veränderliche Zeichen

    char [156] c, aehnlich wie *c nur das ich mit dieser Zeichenkette auch schreiben kann, da aber char nur 1 Bit groß ist, gehen also maximal 2^8=256 Zeichen

    Häh was? Wo hast du den ganzen Blödsinn her?

    Ist das so jetzt alles richtig?

    Nicht im geringsten.

    Könnt ihr mir vielleicht noch sagen, wann ich * und wann ich & benutzen sollte? Ich hab da jetzt irgendwas mit Call by Value und Reference im Kopf, kanns aber nicht mehr genau zu ordnen.

    Das kommt stark auf die Sprache an. C und C++ sind da komplett anders. Weißt du überhaupt, was * und & bedeuten?



  • Invisible schrieb:

    Die Möglichkeit char [] = "Hallo" zu schreiben gibt es wohl, aber das ist dann so als ob ich selber char[5] geschrieben haette, und ich könnte später keine Zeichenkette reinschreiben die länger als 5 Zeichen ist, solange ich nicht mit malloc mehr Speicherplatz allokiere, ist das so richtig?

    Ganz falsch.

    char arr[256];
    

    definiert einen Speicherbereich von 256 Bytes und deklariert einen Verweis namens "arr" auf den Beginn dieses Speicherbereiches. Du kannst in diesen Speicherbereich max. 256 Bytes reinschreiben oder auch max. 256 char (Zeichen).

    char arr[256] = "Hallo";
    

    definiert also 256 Bytes und füllt die ersten 6 Zeichen mit 5 Zeichen + abschließendem '\0'. Die Anweisung nimmt also eine Initialisierung des Speicherbereiches vor. Danach kannst du beliebig darin schreiben oder lesen.
    bei

    const char arr[256] = "Hallo";
    

    passiert dasgleiche, nur darfst du jetzt hier nicht mehr schreiben.

    const char *arr = "Hallo";
    

    ist nochmal was anderes und nur für Laien dasgleiche wie o.g., also etwas für dein späteres fortgeschrittenes Wirken 😉



  • Invisible schrieb:

    char *c, ein Pointer auf eine Zeichenkette, die allerdings only readable ist, also eine Konstate (in JAVA final) | besserer Stil wäre also const char *c

    char [156] c, aehnlich wie *c nur das ich mit dieser Zeichenkette auch schreiben kann, da aber char nur 1 Bit groß ist, gehen also maximal 2^8=256 Zeichen

    Ist das so jetzt alles richtig?

    Könnt ihr mir vielleicht noch sagen, wann ich * und wann ich & benutzen sollte? Ich hab da jetzt irgendwas mit Call by Value und Reference im Kopf, kanns aber nicht mehr genau zu ordnen.

    1. char *c kann auch writable sein, kommt ganz drauf an, was an der Adresse steht, wo dein Pointer eben hinzeigt. String-Literale in Anführungszeichen sind eigentlich ein const char *-Zeiger, der auf nicht-modifizierbaren Speicherbereich zeigt. Du kannst das const zwar weg-casten, aber dein Programm stürzt beim Zugriff darauf dann trotzdem ab.

    2. Du meintest wohl char c[256]. Der Unterschied ist, dass char c[Größe] automatisch auf einen allozierten Speicherbereich im Stack zeigt, der eine bekannte Größe hat. char *c zeigt erstmal auf gar nichts, du musst ihn erst auf irgendeinen Speicherbereich zeigen lassen. MERKE:

    char *c = malloc(256);
    c = "X";
    

    Hier wird nicht ein X und eine 0 in deinen allozierten Speicherbereich geschrieben, sondern der Pointer zeigt auf ein konstantes Stringliteral. Darum: nimm strcpy().

    3. * benutzt du bei Deklarationen, um zu zeigen, dass die Variable ein Pointer ist. Sonst benutzt du es, um die Variable zu bekommen, die an der Adresse steht, auf die der Pointer zeigt.
    & kannst du bei Variablendeklarationen nur in C++ verwenden. Das ist dann aber ein anderes Thema. Ansonsten verwendest du &, um die Adresse einer Variable zu bekommen (also das Gegenteil von *).



  • wxSkip schrieb:

    String-Literale in Anführungszeichen sind eigentlich ein const char *-Zeiger, der auf nicht-modifizierbaren Speicherbereich zeigt.

    Eben nicht, sondern const char[] - also arrays.



  • inter2k3 schrieb:

    wxSkip schrieb:

    String-Literale in Anführungszeichen sind eigentlich ein const char *-Zeiger, der auf nicht-modifizierbaren Speicherbereich zeigt.

    Eben nicht, sondern const char[] - also arrays.

    Das heißt, man könnte es so schreiben: printf(&("Hello World!));

    Warum nehmen dann die ganzen Funktionen const char * an und nicht const char []?



  • wxSkip schrieb:

    Das heißt, man könnte es so schreiben: printf(&("Hello World!));

    Nein, da printf einen Zeiger auf const char* erwartet, keinen Zeiger auf ein char-Array. Praktischerweise zerfällt ein Array-Ausdruck bei fast jeder sich bietenden Gelegenheit sofort zu einem Zeiger auf sein erstes Element ...

    Warum nehmen dann die ganzen Funktionen const char * an und nicht const char []?

    Das ist eh das gleiche.



  • 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.


Anmelden zum Antworten