(gcc) GPF bei Pointerzugriff -- läuft mit Array



  • Hi,

    für die Uni galt es eine kleine Übungsaufgabe zu implementieren, einzelne Zeichen in einem String per Funktion zu ersetzen.
    Ok, nicht weiter schwer. Das komische ist, daß folgender Code zwar funktioniert. Wenn man allerdings die Stringdeklaration austauscht (auskommentierte Zeile stattdessen verwendet), dann klappt es nicht mehr. Zwar kompiliert der gcc den Code weiterhin, aber es gibt einen GPF. Im disassemblierten Code bleibt die Ausführung bei einem 'mov'-Befehl hängen. Ich kann kein Assembler, bin aber sicher, daß es sich dabei um die Zuweisung in der Schleife handelt.
    Warum tritt dieser Fehler nur auf, wenn ich den String als Zeiger statt als Array deklariere?
    Ein Freund sagte mir außerdem, daß der Fehler mit dem MS-Compiler nicht auftritt. Mein Professor konnte sich den Fehler auch nicht erklären, meinte lediglich, daß meine Parameterdeklaration nicht von allen Compilern akzeptiert würde.

    #include <iostream>
    #include <stdlib.h>
    
    using namespace std;
    
    void replace_letter(
      char * const szString,
      const char Letter,
      const char NewLetter
    );
    
    int main(void)
    {
        char SomeString[]   = "Hello, World!";
        //char * SomeString   = "Hello, World!";
        const char Search   = 'l';
        const char Replace  = '_';
    
        cout << "Before: \t" << SomeString << endl;
        replace_letter(SomeString, Search, Replace);
        cout << "After: \t" << SomeString << endl;
    
        system("PAUSE");
        return EXIT_SUCCESS;
    }
    
    void replace_letter(
      char * const szString,
      const char Letter,
      const char NewLetter
    ){
        for (int i = 0; szString[i]; i++)
            if (szString[i] == Letter)
                szString[i] = NewLetter;
    }
    

    that's it.

    Grüße,
    Konrad -



  • String-Literale dürfen nicht beschrieben werden. Das Array stellt eine lokale Kopie des Literals dar, darin darfst du nach Lust und Laune schalten und walten. Deklarierst du aber nur einen Zeiger, so ist das ein Verweis auf das String-Literal. Wenn du danach über den Zeiger eine Schreiboperation machst, kann es sein, dass das in die Hose geht.

    Mich wundert, dass dein Professor das nicht wußte. Oder vielleicht wunderts mich auch nicht 😉



  • Hi,

    erst mal danke für die Antwort.

    Bashar schrieb:

    String-Literale dürfen nicht beschrieben werden. Das Array stellt eine lokale Kopie des Literals dar, darin darfst du nach Lust und Laune schalten und walten. Deklarierst du aber nur einen Zeiger, so ist das ein Verweis auf das String-Literal. Wenn du danach über den Zeiger eine Schreiboperation machst, kann es sein, dass das in die Hose geht.

    Ich bin nicht ganz sicher, daß ich damit etwas anfangen kann: wenn ich z.B. per "new" explizit den Platz für den String reseviere und dann den Pointer darauf zeigen lasse, wieso kann ich nicht per Pointer auf den reservierten Platz schreiben? Ich meine, ich habe ihn doch selbst reserviert?!? (das ist wahrscheinlich die die-hard Anfängerfrage aber was soll's)

    Mich wundert, dass dein Professor das nicht wußte. Oder vielleicht wunderts mich auch nicht 😉

    oh, ich habe den Eindruck, daß der Prof trotzdem einer der Sorte "crack" ist, also einiges von Programmierung versteht. Zumindest entsteht dieser Eindruck, wenn er die Unterschiede der diversen Compilers runterrattert.



  • Konrad Rudolph schrieb:

    Ich bin nicht ganz sicher, daß ich damit etwas anfangen kann: wenn ich z.B. per "new" explizit den Platz für den String reseviere und dann den Pointer darauf zeigen lasse, wieso kann ich nicht per Pointer auf den reservierten Platz schreiben? Ich meine, ich habe ihn doch selbst reserviert?!? (das ist wahrscheinlich die die-hard Anfängerfrage aber was soll's)

    Du meinst so:

    char * ptr = new char[50];
    ptr = "Hallo welt";
    *ptr = "x";
    

    ?

    Einfach mal Schritt für Schritt durchgehen, das hilft manchmal, wenn man den Wald vor Bäumen nicht sieht:

    1. Wir besorgen Platz für 50 chars, und bekommen einen Pointer darauf.
    2. Wir initialisieren ptr mit diesem Pointer. ptr zeigt also jetzt auf 50 Bytes, die wir zur freien Verfügung haben.
    3. nächste Anweisung: ptr wird eine andere Adresse zugewiesen. ptr zeigt jetzt auf ein (nicht beschreibbares) Zeichenkettenliteral.
    4. wir machen einen Schreibzugriff auf die erste Stelle dieses Literals -> BÄNG
      Der vorher reservierte Speicher ist übrigens verloren. Wir haben keinen Pointer mehr, der darauf zeigt, so dass wir auch kein delete mehr ausführen können -> Speicherleck.

    Ich hoffe die Bäume sind jetzt weg 😉

    oh, ich habe den Eindruck, daß der Prof trotzdem einer der Sorte "crack" ist, also einiges von Programmierung versteht. Zumindest entsteht dieser Eindruck, wenn er die Unterschiede der diversen Compilers runterrattert.

    Dann halt ihm mal den Standard unter die Nase. Abschnitt 2.13.4 Absatz 2 sagt The effect of attempting to modify a string literal is undefined.
    Den Final Draft dieses Kapitels gibts z.B. hier: http://www.kuzbass.ru:8086/docs/isocpp/lex.html

    Diverse C++-Einsteigerbücher (von denen ich leider im Moment keins zur Hand habe) formulieren das ganze eventuell etwas anschaulicher.



  • Bashar schrieb:

    Du meinst so:

    char * ptr = new char[50];
    ptr = "Hallo welt";
    *ptr = "x";
    

    nein, leider nicht. Ich meine das:

    char * ptr = new char[50];
    ptr = "Hallo welt";
    ptr[0] = 'x';
    

    Denn das ist ja im Prinzip das, was in meinem Code passiert, oder irre ich mich da?



  • Die beiden Codes tun exakt das gleiche, keine Sorge. *ptr und ptr[0] sind zwei Schreibweisen für das gleiche. Tatsächlich ist der Ausdruck A[B] definiert als *(A + B).



  • Ok, danke. Die Bäume waren plötzlich weg 😉

    Nur noch eine praktische Frage: wenn ich also mit dynamischen Strings arbeiten will, die zur Laufzeit manipulierbar sind, darf ich ihnen keine String-Literals zuweisen. Das heißt also, daß man stattdessen immer den String Zeichen für Zeichen in einen reservierten Speicher kopieren muß (vorausgesetzt ich will keine vorhandenen Stringfunktionen verwenden)?



  • #include "stdio.h"
    
    char a[]="Hallo";
    
    int main()
    {
      a[1]='e';
      printf("%s\n", a);
      a[1]='i'; a[2]='\0';
      printf("%s\n", a);
      a[1]='a'; a[2]='l';
      printf("%s\n", a);
      return 0;
    }
    

    Der Speicher für das Stringliteral "Hallo" wird angelegt, und Hallo + '\0' dort hineingeschrieben - die Reservierung des Speichers wird in diesem Fall auch während des gesamten Programms in gleicher Grösse beibehalten.
    char a[] = ... weist dessen Adresse eben nur an a zu.

    Manipulierbar sind die Einträge trotzdem, Du darfst nur nicht über das Ziel hinausschiessen. Ein Stringende-'\0' ist für den Compiler ein Zeichen, wie jedes andere auch, nur die Stringfunktionen gehen damit gesondert um.
    Was Du beachten musst, ist die Maximallänge nach der Initialisierung, und eben die Lebensdauer. Die kann, wie in einem Aufruf strcmp(mystr, "abc") so kurz sein, dass Du an das Stringliteral gar nicht rankommst.

    Vielleicht hilft Dir das kleine Beispiel, um ein besseres Gefühl dafür zu bekommen, was der Compiler macht.



  • Hi Bitsy,

    danke für dein Beispiel, aber du verwendest ja ein char-Array, d.h. (soweit ich das verstanden habe), wird hier das Stringliteral bei der Initialisierung ins Array *kopiert*. Wenn man stattdessen einen char-Pointer deklariert und initialisieren hätte, würde dieser lediglich aufs Stringliteral zeigen (?).

    Was ich wissen wollte: wenn man eine Variable als "char*" deklariert und ihr ein Stringliteral zuweisen will (was später manipulierbar sein soll), muß ich dann statt der einfachen Zuweisung immer folgendes schreiben?

    #include "stdio.h" 
    #include <strings.h>
    
    int main() 
    { 
      char * szString = NULL;
      // jetzt will ich diesem String "Hallo" zuweisen, aber so,
      // daß ich das später manipuliern kann.
      // szString = "Hallo" klappt ja nicht.
      // Muß ich immer folgendes machen:
    
      szString = new char[6];
      strcpy(szString, "Hallo");
    
      printf("%s\n", szString);
    
      delete [] szString;
      return 0; 
    }
    


  • Probier's mal aus, auch an dieser Stelle geht
    char* szString = "Hallo";
    Aber schau Dir folgendes Beispiel an:

    #include <stdio.h>
    
    int func(void)
    {  
      char* szString = "Hallo";
      printf("%s\n", szString); 
      szString[1]='e';
      printf("%s\n", szString); 
      return 0;  
    }
    
    int main()
    {
      func();
      func();
      return 0;
    }
    

    Beim ersten Durchlauf wird szString auf "Hallo" gestellt, und dann "Hello" draus gemacht. Beim zweiten Durchlauf findet die Neuinitialisierung nicht statt! Das alte "Hello" bleibt erhalten. Streng genommen passiert hier einfach was Falsches! Denn normal würde man vom Compiler genau das erwarten, was Dein letztes main() macht. Macht er aber nicht (oder Dein Compiler vielleicht doch?)
    Wir kommen hier in eine Grauzone rein.

    Deswegen ist Dein letztes main ein bedeutend sicherer Weg den String aufzubauen und zu initialisieren.
    Ich würde Stringinitialisierungen wirklich *nur* global machen, denn dann ist man sicher, dass es nur einmal passiert.
    Ferner ist es riskant, mit [6] zu arbeiten.
    Mach lieber

    #include <stdio.h>
    #include <strings.h> 
    
    #define STR_HALLO "Hallo"
    
    int main()  
    {  
      char* szString = new char[sizeof(STR_HALLO)];
      strcpy(szString, STR_HALLO);
      printf("%s\n", szString); 
      delete [] szString; 
      return 0;  
    }
    


  • Bitsy schrieb:

    Wir kommen hier in eine Grauzone rein.

    Nein, die ist ganz schwarz. Siehe Standard-Zitat weiter oben, zweites Posting von mir.



  • Bitsy schrieb:

    Probier's mal aus, auch an dieser Stelle geht
    char* szString = "Hallo";
    Aber schau Dir folgendes Beispiel an:

    #include <stdio.h>
    
    int func(void)
    {  
      char* szString = "Hallo";
      printf("%s\n", szString); 
      szString[1]='e';
      printf("%s\n", szString); 
      return 0;  
    }
    
    int main()
    {
      func();
      func();
      return 0;
    }
    

    Beim ersten Durchlauf wird szString auf "Hallo" gestellt, und dann "Hello" draus gemacht. Beim zweiten Durchlauf findet die Neuinitialisierung nicht statt! Das alte "Hello" bleibt erhalten.

    Sorry, das kann ich nicht nachvollziehen: obiger Code ist ja genau der, der bei mir einen Fehler liefert (ich habe mal dein Beispiel auch ausprobiert, vielleicht hatte ich ja doch etwas nicht verstanden): Zugriffsfehler, und zwar in der Zeile "szString[1]='e';" (afaik).
    Daher ja überhaupt meine Frage ... so wie ich das jetzt verstanden habe, kann man sowas schlicht und ergreifend nicht machen sondern muß erst das String Literal kopieren (z.B: per strcpy()).


Anmelden zum Antworten