Wie funktionieren char-Zeiger?



  • Hallo,

    mir ist die Verwendung von char-Zeigern noch nicht völlig klar. Im Folgenden habe ich einen Arduino-Sketch aufgeführt, der mein bisheriges Verständnis wiedergibt.

    int variable = 2;
    int *zeiger = &variable;
    
    void setup() {
    Serial.begin(115200);
    delay(200);
    Serial.println(variable); // Wert der Variablen
    Serial.println((unsigned long)&variable); // Speicheradresse der Variablen
    Serial.println((unsigned long)zeiger); // Speicheradresse der Variablen
    Serial.println(*zeiger); // Wert der Variablen
    Serial.println((unsigned long)&zeiger); // Speicheradresse des Zeigers
    }
    
    void loop() {}
    

    Nun habe ich in einem Sketch folgende Verwendung eines Zeigers gefunden.

    const char* text = "test";
    
    void setup() {
    Serial.begin(115200);
    delay(200);
    Serial.println(text); // Ausgabe des char-Arrays
    Serial.println((unsigned long)&text); // Ausgabe einer Speicheradresse
    Serial.println(*text); // Ausgabe des ersten char-Array-Elements
    }
    
    void loop() {}
    
    1. Warum erhalte ich den Inhalt des char-Arrays, wenn ich den Zeiger <text> ausgebe? Ich würde die Speicheradresse erwarten, unter welcher das char-Array mit dem Inhalt "test" gespeichert ist.
    2. Bei der Ausgabe von <&text> würde ich die Speicheradresse des Zeigers und nicht des char-Arrays erwarten. Trifft das zu oder handelt es sich um die erste Speicheradresse meines char-Arrays? Wenn Letzteres zu trifft, wie erhalte ich die Speicheradresse meines Zeigers?
    3. Bei der Ausgabe von *text erhalte ich den ersten char-Wert meines Arrays (hier: "t"). Warum wird mir nicht das gesamte char-Array zurückgegeben? Der Speicher müsste doch solange gelesen werden, bis das Ende meiner Zeichenkette durch \0 markiert wird.
    4. Ich bin es gewohnt, dass der Sternoperator vor der Variablen steht (const char *text). Nun gibt es offenbar auch die Schreibweise const char* text, bei welcher der Sternoperator nach dem Datentyp steht. Sind die beiden Schreibweisen äquivalent oder besteht ein Unterschied?
    5. Welche Vorgehensweise empfehlt ihr, um ein konstantes char-Array (z.B. ein Passwort) zu verwenden? Zum Beispiel könnte ich das char-Array doch auch wie folgt deklarieren:
    char text[] = "test";
    

    Danke im Voraus und beste Grüße

    Jimmy



    1. Weil die für char* passende Überladung von Serial.println so funktioniert. Gemäß der Konvention, die von C herrührt, dass man Strings als nullterminierte char-Arrays ablegt. Hätte man auch anders machen können, aber so wurde es wohl als sinnvoll angesehen.
    2. Es sollte die Adresse des Zeigers ausgegeben werden. Wie hast du denn ermittelt, dass das die Adresse des Arrays ist? Ich seh davon nichts in deinem Programm.
    3. Du übergibst ein char, nämlich *text. Wie soll das funktionieren, die Funktion weiß erstens nicht, dass dieses Zeichen aus einem Array stammt, und zweitens nicht, woher.
    4. Dir ist sicher schon aufgefallen, dass es C++ nicht sonderlich interessiert, wo du deine Leerzeichen, Tabs und Zeilenumbrüche hinsetzt. Hier ist es genauso. Ansonsten gibt es zu der Frage endlose Debatten und sicherlich auch hier bald wieder.
    5. Ich gebe keine Empfehlungen zu sicherheitsrelevanten Aspekten. Wie man ein Passwort sicher ablegt ist eine Wissenschaft für sich. Für andere Texte: Der Unterschied hier ist, dass du einmal einen Zeiger auf ein konstantes statisches Array hast und einmal ein lokales Array. Da es dir nur darauf ankommt, ein konstantes Array zu haben, würde ich das erste empfehlen.


  • Ich habe keine Ahnung, was Serial.println() sein soll.

    Ein Array of char:

    char foo[42];
    

    Die Adresse eines Arrays of char:

    std::cout << foo << '\n';
    

    Ein Zeiger auf char:

    char *bar = foo;
    

  • Banned

    @Swordfish sagte in Wie funktionieren char-Zeiger?:

    Ich habe keine Ahnung, was Serial.println() sein soll.

    Denk mal ganz scharf nach ... 🐱



  • @Bashar sagte in Wie funktionieren char-Zeiger?:

    1. Weil die für char* passende Überladung von Serial.println so funktioniert. Gemäß der Konvention, die von C herrührt, dass man Strings als nullterminierte char-Arrays ablegt. Hätte man auch anders machen können, aber so wurde es wohl als sinnvoll angesehen.

    Kannst du diesen Punkt bitte noch einmal ausführlicher erklären? Der Begriff der Funktionsüberladung beschreibt die mehrfache Verwendung desselben Funktionsnamens für unterschiedliche Funktionen. Diese werden von C++ anhand der Signatur (Art und Reihenfolge der Argumente) auseinandergehalten. Ich verstehe deine Aussage nun so, dass der Zeiger text in C++ tatsächlich die Speicheradresse meines char-Arrays zurückgeben würde. Die Methode println der Klasse Serial interpretiert die Anweisung jedoch derart, dass direkt das char-Array ausgegeben wird.

    1. Es sollte die Adresse des Zeigers ausgegeben werden. Wie hast du denn ermittelt, dass das die Adresse des Arrays ist? Ich seh davon nichts in deinem Programm.

    Ich habe nichts ermittelt; ich wollte nur sicher gehen, dass es sich tatsächlich um die Adresse des Zeigers handelt.

    1. Ich gebe keine Empfehlungen zu sicherheitsrelevanten Aspekten. Wie man ein Passwort sicher ablegt ist eine Wissenschaft für sich. Für andere Texte: Der Unterschied hier ist, dass du einmal einen Zeiger auf ein konstantes statisches Array hast und einmal ein lokales Array. Da es dir nur darauf ankommt, ein konstantes Array zu haben, würde ich das erste empfehlen.

    Der Sicherheitsaspekt ist mir aktuell noch gleichgültig. Mir geht es um eine elegante Programmierung, die Fehlerquellen minimiert und Speicherplatz spart.
    Warum empfiehlst du ein konstantes statisches Array (const char* text)? Ein statisches char-Array bleibt auch zwischen den Funktionsaufrufen erhalten, kann jedoch im Gegensatz zu globalen Variablen nur lokal verwendet werden. Die Speicherklasse kommt doch letztlich nicht zum tragen, da der Geltungsbereich des Arrays die gesamte Programmdauer nicht verlassen wird.

    Grüße

    Jim





  • @Jimmy sagte in Wie funktionieren char-Zeiger?:

    Kannst du diesen Punkt bitte noch einmal ausführlicher erklären? Der Begriff der Funktionsüberladung beschreibt die mehrfache Verwendung desselben Funktionsnamens für unterschiedliche Funktionen. Diese werden von C++ anhand der Signatur (Art und Reihenfolge der Argumente) auseinandergehalten. Ich verstehe deine Aussage nun so, dass der Zeiger text in C++ tatsächlich die Speicheradresse meines char-Arrays zurückgeben würde. Die Methode println der Klasse Serial interpretiert die Anweisung jedoch derart, dass direkt das char-Array ausgegeben wird.

    Es gibt halt mehrere Überladungen für println, eine davon ist für char-Zeiger, und die gibt den dahinterliegenden String aus statt der Adresse.

    Das unter der Voraussetzung, dass das überhaupt C++ ist. Es sieht jedenfalls danach aus.

    Der Sicherheitsaspekt ist mir aktuell noch gleichgültig. Mir geht es um eine elegante Programmierung, die Fehlerquellen minimiert und Speicherplatz spart.
    Warum empfiehlst du ein konstantes statisches Array (const char* text)? Ein statisches char-Array bleibt auch zwischen den Funktionsaufrufen erhalten, kann jedoch im Gegensatz zu globalen Variablen nur lokal verwendet werden. Die Speicherklasse kommt doch letztlich nicht zum tragen, da der Geltungsbereich des Arrays die gesamte Programmdauer nicht verlassen wird.

    Weil ein nicht-statisches Array keinen Zusatznutzen bringt und es ineffizient ist, es bei jedem Aufruf der Funktion zu initialisieren. Ein statisches Array (static char text[] = "foo";) muss immerhin nur einmal initialisiert werden. Andererseits, lies den Link von @Zhavok ... ich rede hier nur von der C++-Seite, von Arduino weiß ich nichts. Wenn ich das richtig verstanden habe, werden Stringliterale dort ohnehin einmal umkopiert am Anfang ...



  • Die Überladungen der Methode print bzw. println findest du in der Klasser Print.h
    z.B. hier
    https://github.com/esp8266/Arduino/blob/02f54e85fdae5cc733bdf911afd72c6d49f25100/cores/esp8266/Print.h

    Wei du sehen kannst, wurde diese Methode für lauter verschiedene Datentypen überladen. Unter anderem eben auch für ein char Array. Die Arduino Methoden sind im großen und ganzen darauf ausgelegt, dass die Benutzung möglichst einfach ist, in der Regel leidet darunter die Effizient (siehe z.B. digitalWrite()).Und in einer einfachen Welt printet eine print Methode eben das Array aus, wenn man es ihm übergibt.Daher wurde das intern so realisiert. Wie @Swordfish verdeutlicht hat ist das nicht das Fall der C++ standard lib, dort wird die Adresse ausgegeben



  • Wobei ich an der Stelle nicht verstehe, wieso nicht const char* genommen wurde, da das IMHO viel mehr Sinn ergibt... Println soll ja nur ausgeben und nichts verändern an dem String.



  • Gut, die Funktionsweise der Überladung und ihre Folgen habe ich verstanden. Dennoch ist mir noch nicht ganz klar, warum ich einen char-Wert in einem Zeiger speichern kann. char *text ist ein Zeiger. Zeiger dienen dem Speichern von Adressen. char *text = "test" speichert jedoch ein char-Array in einem Zeiger. Meine Erwartung wäre

    const static char text[5] = "test";
    char *zeiger = &text;
    

    Stattdessen steht da

    char *zeiger= "test";
    

    Handelt es bei der zweiten Variante quasi um eine verkürzte Schreibweise, die mein char-Array unter der Speicheradresse speichert, welche der Zeiger zeiger enthält, ohne dabei jedoch noch eine Variable text zu erzeugen?

    Den Kommentar von Swordfish erachte ich außerdem für falsch, da mir der Code

    #include <iostream>
    using namespace std;
    
    int main(void) {
    	const char foo[12] = "Hallo Welt!";
    	cout << foo << '\n';
    	cin.get();
    	return 0;
    }
    

    in Visual Studio den Inhalt des char-Arrays ausgibt und nicht wie Swordfish schrieb: die Adresse.

    @Swordfish sagte in Wie funktionieren char-Zeiger?:

    Ich habe keine Ahnung, was Serial.println() sein soll.

    Ein Array of char:

    char foo[42];
    

    Die Adresse eines Arrays of char:

    std::cout << foo << '\n';
    

    Ein Zeiger auf char:

    char *bar = foo;
    

    Grüße Jim



  • @Jimmy

    const char* str = "Hello World"
    

    Legt im Daten Segment (Wobei kann glaub auch wo anders liegen, kommt auf die Umgebung an) Deines Programms "Hello World\0" ab. Die Variable str zeigt lediglich auf das char-Array innerhalb Deines Programms, enthält aber nicht den String selbst.

    char *zeiger= "test"; //  warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    

    Ist nicht korrekt, denn ein string-Literal ist vom Typ const char*, also immutable. Deshalb:

    const char *zeiger= "test";
    

    Bei dem Fall:

    #include <iostream>
    
    int main() {
        const char* s1 = "Hello ";
        char s2[] = {'W', 'o', 'r', 'l', 'd', '\0'};  // macht man natürlich normalerweise nicht so
        int i = 5;
        std::cout << s1 << s2 << '\n'; // Hello World
        std::cout << &i << '\n'; // 0x65fe3c
    }
    

    Wird bei char[] bzw. char* (hier s2) der ostream& operator<< (ostream& os, const char* s); aufgerufen, weil char* zu const char* konvertiert werden kann, und der ist so definiert, dass der Inhalt bis zum '\0' ausgegeben wird. Wobei im Fall eines anderen Zeigers (nicht auf einen char Typ, z. B. int*) wird die Adresse ausgegeben.

    Deshalb stimme ich Dir zu, bei operator<<(char[]) wird nicht die Adresse ausgegeben.

    Du kannst z. B. hier schauen, was für Assembler verschiedene Kompiler generieren.

    const char* str = "test";
    const char* str2 = "test";
    // ---> gcc
    .LC0:
            .string "test"
    str:
            .quad   .LC0
    str2:
            .quad   .LC0
    

    Wie Du siehst, ist in dem .LC0 Segment "test" abgelegt, und str und str2 zeigen lediglich darauf (die genauen Details sind nicht so wichtig, es geht um die Struktur).



  • @HarteWare Erstmal danke für deine Mühe. 😉

    const char* str = "Hello World"
    

    Legt im Daten Segment (Wobei kann glaub auch wo anders liegen, kommt auf die Umgebung an) Deines Programms "Hello World\0" ab. Die Variable str zeigt lediglich auf das char-Array innerhalb Deines Programms, enthält aber nicht den String selbst.

    Das deckt sich dann auch mit meinem Verständnis.

    char *zeiger= "test"; //  warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    

    Ist nicht korrekt, denn ein string-Literal ist vom Typ const char*, also immutable. Deshalb:

    const char *zeiger= "test";
    

    Das widerum ist mir nicht völlig klar. Ein const char* zeiger ist ein Zeiger auf ein const char. Das bedeutet, der Wert des Zeigers bzw. die Speicheradresse ist veränderbar, der Inhalt des Speichers bzw. das char-Array selbst ist nicht veränderbar (immutable). Versuche ich das char-Array dennoch zu ändern, erhalte ich eine Warnung des Compilers. Es gibt aber doch auch Anwendungsfälle, in denen das char-Array veränderbar sein soll. Daher verstehe ich den Grund nicht, der die Deklaration char* zeiger verbietet.

    Was ist ein Literal? Ein Literal ist nach meinem Verständnis ein konstanter Wert, der im Code direkt hineingeschrieben worden ist. Er kann aber jederzeit an einer anderen Stelle des Codes geändert werden.

    Bei dem Fall:

    #include <iostream>
    
    int main() {
        const char* s1 = "Hello ";
        char s2[] = {'W', 'o', 'r', 'l', 'd', '\0'};  // macht man natürlich normalerweise nicht so
        int i = 5;
        std::cout << s1 << s2 << '\n'; // Hello World
        std::cout << &i << '\n'; // 0x65fe3c
    }
    

    Wird bei char[] bzw. char* (hier s2) der ostream& operator<< (ostream& os, const char* s); aufgerufen, weil char* zu const char* konvertiert werden kann, und der ist so definiert, dass der Inhalt bis zum '\0' ausgegeben wird. Wobei im Fall eines anderen Zeigers (nicht auf einen char Typ, z. B. int*) wird die Adresse ausgegeben.

    Deshalb stimme ich Dir zu, bei operator<<(char[]) wird nicht die Adresse ausgegeben.

    Ist es demnach korrekt, wenn ich sage: Im Regelfall gibt der Operator <<, der auf einen Zeiger ohne vorangestellten Sternoperator angewendet wird, eine Speicheradresse zurück. Für Zeiger des Datentyps char existiert in der Definition des Operators << eine Ausnahme, welche zur Ausgabe des Speicherinhalts anstatt der Speicheradresse führt?

    Grüße

    Jim



  • @Jimmy sagte in Wie funktionieren char-Zeiger?:

    Daher verstehe ich den Grund nicht, der die Deklaration char* zeiger verbietet.

    Was ist ein Literal? Ein Literal ist nach meinem Verständnis ein konstanter Wert, der im Code direkt hineingeschrieben worden ist. Er kann aber jederzeit an einer anderen Stelle des Codes geändert werden.

    Mit Literal ist ein "String Literal" gemeint, also zwischen zwei "".

    In C++ sind diese eben immutable, und damit ist es undefiniertes Verhalten, wenn du einen char* darauf zeigen lässt und versuchst die Daten zu überschreiben. Um das Ganze logisch anzugehen: Wie in meinem assembler-Code oben siehst Du, dass der Compiler für mehrmalige Verwendungen des selben Stringliterals die selbe Speicherstelle im Programm verwendet hat. Wenn man diese nun aber ändern könnte, wäre dies nicht mehr möglich, denn dann könnte ich ja durch Ändern von str1 auch str2 beeinflussen, da diese auf den selben String zeigen.

    Es ist definiert, dass ein "String Literal" vom Typ const char* ist. Da mit char* rumhantieren ist undefiniertes Verhalten. Wie @Swordfish mal schön dazu sagte:

    Dein Compiler darf dir wenn er das sieht auch online eine Pizza bestellen ...

    Da du schon zur Kompilierzeit das Stringliteral angibst, brauchst Du ja nicht im Programm die Daten ändern, sondern kannst das von Hand direkt machen (die Größe ist sowieso fix, also würde das auch nicht so viel bringen, wenn man die überschreiben könnte). Und wenn doch, kannst Du den String in einen std::string kopieren, es gibt einen Konstruktor std::string(const char*):

    #include <string>
    const char* str = "hello World";
    std::string str2{str};
    str2[0] = 'H';
    std::cout << "str : " << str << '\n'; // "hello World"
    std::cout << "str2: " << str2 << '\n'; // "Hello World"
    

    Ist es demnach korrekt, wenn ich sage: Im Regelfall gibt der Operator <<, der auf einen Zeiger ohne vorangestellten Sternoperator angewendet wird, eine Speicheradresse zurück. Für Zeiger des Datentyps char existiert in der Definition des Operators << eine Ausnahme, welche zur Ausgabe des Speicherinhalts anstatt der Speicheradresse führt?

    Könnte man vermutlich so sagen, ja.



  • @Jimmy sagte in Wherever

    Den Kommentar von Swordfish erachte ich außerdem für falsch

    ja. Denk dir einen cast nach void * dazu.



  • @Jimmy Der Sternoperator beim Pointer wird auch Dereferenzierungsoperator genannt.

    Das heißt, du bekommst den Datentyp, auf den der Pointer verweist.

    wenn du bei const char *text ="Hallo"; später im Code *text verwendest, dann ist das kein Zeiger sondern ein char.

    Da es auch Zeiger auf Zeiger gibt, ist es mit Aussagen wie „mit * ist es ...“ nicht ganz so eindeutig.


Log in to reply