Das verwirrende char in C/C++



  • Hallo, ich bin sehr neu in der Welt von C/C++, bin aber gezwungen das zu lernen (beides!!).

    Bisher kannte ich von JAVA die Datentypen char für ein Zeichen und String für eine Zeichenkette. Doch das wird jetzt bei C/C++ komplett über den Haufen geworfen.

    Ich weiß das es mittlerweile in C++ auch den DT String gibt, aber in C nicht und darum wäre es super, wenn mir jemand die Sache genauer erklären könnte.

    Es geht darum, dass es zig verschiedene Möglichkeiten gibt das zu definieren und alles bedeutet irgendwie was anderes:
    char, char *, char **, char [] etc..

    Könnte mir das einer vllt erklären, eventuell sogar mit Bezug/Vergleich zu Java, dann weiß ich eher was ich damit anfangen kann.

    Ich weiß zwar das * Pointer bedeutet und ** Pointer auf einen Pointer, ist aber irgendwie einfach zu unverständlich für mich.



  • Fangen wir ganz einfach an. Der Datentyp char steht für ein einzelnes Zeichen. Ein einzelnes Zeichen wird mit einfachen Gänsefüßchen angegeben:

    char c = 'A';
    

    Ein String ist dabei nichts anderes als mehrere Zeichen in Folge. Man nennt das auch Array. Ein Array mit fester Größe, kann man so anlegen:

    char arr[256];
    

    Das Ende eines Strings wird immer mit 0 oder '\0' gekennzeichnet. Man nennt das terminieren.

    Ein Array mit fester Größe kann man nun sehr einfach füllen. Entweder man macht das selber:

    char arr[256];
    
    arr[0] = 'H';
    arr[1] = 'a';
    arr[2] = 'l';
    arr[3] = 'l';
    arr[4] = 'o';
    arr[5] = '\0'; // Terminierung
    

    Oder man nutzt die Funktion sprintf:

    char arr[256];
    
    sprintf(arr, "Hallo");
    

    Die Terminierung nimmt sprintf selber vor. Deshalb ist sie hier nicht notwendig!

    Ein wichter Hinweis dazu: Das Programm stürzt natürlich ab, wenn man über die Größe des Arrays hinausschreibt.

    Da ein Array nichts anderes ist, als ein Zeiger, kann man einen String auch mit char * anlegen:

    char *VERSION = "v0.1.1.2";
    

    Dieser String wird durch den Compiler allerdings in einem speziellen Bereich des Programms abgelegt. Man kann damit lesend darauf zugreifen, aber nicht schreibend.

    Ansonsten kann man mit einem Zeiger auch Speicher reservieren. In C geschieht dies durch die Funktion malloc. Das ist dann notwendig, wenn man die Größe des Arrays erst zur Laufzeit weiß:

    char *str = malloc(sizeof(char) * (3 + 1));
    
    // Reservierung von 4 Byte
    // 3 Byte für 3 einzelne Zeichen
    // + 1 Byte für die Terminierung
    str[0] = 'A';
    str[1] = 'B';
    str[2] = 'C';
    str[3] = '\0'; // Terminierung!
    
    // Speicher später wieder freigeben!
    free(str);
    

    Ich bin mir gerade nicht sicher, aber char [] und char * müssten identisch sein. Ist wohl einfach eine andere Schreibweise. Damit unterliegt char [] natürlich den gleichen Einschränkungen, wie char *



  • char [] schiebt den String auf den Stack, er ist also nicht konstant und darf verändert werden.
    char* p = "Hallo"; sollte gleich als const char* p = "Hallo"; geschrieben werden.
    Der Comiler kann nun blödes Verhalten des Programmieres unterbinden:

    const char* p = "Hallo";
    p[1]='A'; // Compilerfehler
    


  • Noch eins: Im Gegensatz zu Java ist ein Char in C nur 1 Byte groß. Du kannst Unicode damit also nur umständlich (und dann am ehesten mit externen Libraries oder API-Funktionen) in dein Programm einbauen. char ** ist ein Pointer auf einen Pointer, das hast du richtig verstanden. Es funktioniert so wie String[] args in Java. Du musst allerdings den Speicher von char** selbst allozieren. char a[][] ist nicht das Gleiche!

    Es gibt ein paar Unterschiede zwischen Pointern und Arrays:
    Arrays sind keine Pointer, können aber implizit zu Pointern konvertiert werden. sizeof(array) gibt bei bekannter Arraygröße diese aus, während sizeof(pointer) die Größe des Pointers auf das Array ausgibt. Außerdem ist &array das Gleiche wie ein Pointer auf das Array und nicht ein Pointer auf den Pointer, der auf den Anfang des Arrays zeigt.

    So, genug geredet, den Rest wirst du schon selbst herausfinden!



  • ,ljklj schrieb:

    Ein wichter Hinweis dazu: Das Programm stürzt natürlich ab, wenn man über die Größe des Arrays hinausschreibt.

    Falsch. So ein Zugriff bewirkt undefiniertes Verhalten, es kann alles passieren. Ein Absturz ist nicht garantiert.

    ,ljklj schrieb:

    Da ein Array nichts anderes ist, als ein Zeiger

    Ganz falsch. Array und Zeiger sind zwei unterschiedliche Dinge.



  • ,ljklj schrieb:

    Ein Array mit fester Größe kann man nun sehr einfach füllen. Entweder man macht das selber:

    char arr[256];
    
    arr[0] = 'H';
    arr[1] = 'a';
    arr[2] = 'l';
    arr[3] = 'l';
    arr[4] = 'o';
    arr[5] = '\0'; // Terminierung
    

    Es geht natürlich auch viel einfacherer und lesbarer:

    char arr[256] = "Hallo";
    

    ,ljklj schrieb:

    Oder man nutzt die Funktion sprintf:

    char arr[256];
    sprintf(arr, "Hallo");
    

    Ist ebensolcher Schrott, dafür gibt es die Standardbibliotheksfunktion strcpy:

    strcpy(arr,"Hallo");
    

    wobei wie schon erwähnt undefiniertes Verhalten vorliegt, falls der definierte Speicherbereich von arr zur Aufnahme der Zeichen inkl. abschließendem '\0' Zeichen nicht ausreicht.



  • Und weil alles so umständlich ist, nimmt man in C++ die String-Klasse:

    #include <string> // ohne .h
    #include <iostream>
    
    int main()
    {
        using namespace std;
    
        string str("Hallo!");
        str += " Ich bin ein C++ String.";
    
        cout << str << endl; // gibt "Hallo! Ich bin ein C++ String." aus
    
        size_t found = str.find("ein");
        cout << "\"ein\" steht an Position " << int(found) << endl;
    }
    

    Reference: http://www.cplusplus.com/reference/string/string/

    Keine Macht den Buffer Overflows! ⚠ 😃



  • Artchi schrieb:

    size_t found = str.find("ein");
    cout << "\"ein\" steht an Position " << int(found) << endl;

    Bei sowas kräuseln sich bei mir doch schon wieder die Schamhaare, was soll der Rückgabetyp dort size_t, wenn ich ihn nachher doch sowieso casten muss.



  • Hey, erstmal Danke für eure Hilfe! Ich seh jetzt schon ein bisschen besser durch als vorher 🙂

    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?

    Im Endeffekt habe ich jetzt gelernt, das
    char c, ein Character/Zeichen ist wie in JAVA

    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.


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


Anmelden zum Antworten