Problem mit Zeiger -> Programm stürzt ab



  • Hallo zusammen,

    in meinen Übungsunterlagen habe ich folgende Lösung (Auszug):

    #include <stdio.h>
    #include <string.h>
    void main(void)
    {
        FILE *quelle,*ziel;
        char *name, *nameneu="am_";
    
        printf("Dateiname: ");
        gets(name);
    
        quelle=fopen(name,"r");
        ziel=fopen(strcat(nameneu,name),"w");
    }
    

    Wenn ich das Programm ausführe (mit Code::Blocks) stürzt das Programm ab.

    Wenn ich die Zeile 6 in

    char name[255], nameneu[100]="am_";
    

    ändere, funktioniert es.

    Kann mir das jemand erklären?
    Status: anfänger

    Grüße und Danke, Felted



  • Versteh ich das richtig? Das ist eine vorgegebene Musterlösung?



  • Ja.


  • Mod

    felted schrieb:

    in meinen Übungsunterlagen habe ich folgende Lösung (Auszug):

    Ist das eine bewusst falsche Lösung? Da ist nämlich fast jede Zeile ein WTF. Falls das eine ernsthafte Musterlösung sein sollte: Kurs abbrechen, Autodidakt werden. Sonst verdirbt der Kurs dich total. Dies ist kein Scherz. Nichts ist schlimmer, als Falsches zu lernen, egal welches Fach. Das wird man nie wieder los.

    Kann mir das jemand erklären?

    Ja, aber vermutlich nicht mit Worten, die du bei solch einem Lehrer verstehst 😞 . Die Zeiger im ersten Beispiel zeigen allesamt ins Nichts. Wenn du versuchst, da etwas hin zu schreiben, kracht es. Bei deiner Verbesserung ersetzt du die Zeiger durch konkrete Arrays, in die darfst du natürlich schreiben.

    Die Fehler und ungewöhnlichen Techniken im ersten Beispiel:
    Zeile 3: Nicht-standardkonforme Signatur der main.
    Zeile 6: Zeiger/Zeichenketten nicht verstanden. Literale nicht verstanden.
    Zeile 8: Ungewöhnliche Ausgabemethode für Zeichenkettenliterale.
    Zeile 9: gets is dangerous and should not be used.
    Zeile 12: Zeiger nicht verstanden, Zeichenketten nicht verstanden, Literale nicht verstanden, strcat nicht verstanden.

    Das ist selbst für einen Anfänger ein ganz gravierendes Zeugnis, aber für einen Lehrer? 😮



  • Gehen wir das mal durch. Erstmal eine Formalie: Der Rückgabetyp von main muss int sein, nicht void. Jetzt die grausamen Sachen:

    • der Zeiger name ist nicht initialisiert, zeigt also an irgendeine zufällige Speicherstelle, gets(name) schreibt also irgendwohin -- das führt in der Regel schon direkt zum Absturz, weil eine zufällige Speicherstelle in der Regel nicht deinem Programm gehört
    • gets ist eine Funktion, die prinzipiell nicht benutzt werden sollte, da sie keine Möglichkeit besitzt, die Länge der Eingabe zu begrenzen
    • nameneu zeigt auf ein Stringliteral, also ein konstantes Array. strcat(nameneu, name) versucht, dieses zu verändern, was undefiniertes Verhalten hat -- von Absturz bis zu seltsamen Effekten (oder gar nichts) ist alles drin


  • Kleine Verbesserungen:

    #include <stdio.h>
    #include <string.h>
    int main(void)
    {
        FILE *quelle,*ziel;
        //char *name, *nameneu="am_";
        char name[255], nameneu[100]="am_"; // Können char's auch mit dynamischer Länge initialisiert werden?
        printf("Dateiname: ");
        //gets(name); // gefährlich
        fgets(name, 255, stdin); // Besser? Aber Datei kann nicht geöffnet werden...
        quelle=fopen(name,"r");
        ziel=fopen(strcat(nameneu,name),"w");
    
        if(!quelle){
            printf("Datei kann nicht geoeffnet werden");
        }
    }
    

    Fragen zu:

    Zeile 8: Ungewöhnliche Ausgabemethode für Zeichenkettenliterale: Wieso ungewöhnlich?
    Zeile 9: gets is dangerous and should not be used: fgets ist nach Recherche besser. Aber Datei kann jetzt nicht mehr geöffnet werden...
    Zeile 12: Zeiger nicht verstanden, Zeichenketten nicht verstanden, Literale nicht verstanden, strcat nicht verstanden: Wie würdest Du/Ihr es lösen?

    Kann man char's dynamisch initialisieren, wenn man im Vorfeld nicht weiß, wieviele Zeichen zugewiesen werden sollen?

    Grüße und Danke, Felted



  • fgets liest das abschließenden '\n' von der Enter-Taste mit ein.
    Kannst du sehen, wenn du

    if(!quelle){
            printf("Datei <%s> kann nicht geoeffnet werden", name);
        }
    

    nimmst. Dann steht da z.B.

    Datei <test.txt
    > kann nicht geoeffnet werden
    

    Achte auf dei Position der < >

    Es hat alles Vor- und Nachteile. gets ist einfach aber unsicher, fgets dagegen sicher aber schwierig (und zuviel Schreibarbeit 😃 )

    printf erwartet als ersten Parameter den Formatstring.

    Eleganter wäre puts , das hängt aber automatisch ein '\n' hinten dran, womit man dann wieder bei fputs landet, was dies nicht tut.

    Solange du ein festes Stringliteral hast, geht das m.M. nach so mit dem printf in Ordnung.
    Solltest du aber bei printf als ersten Parameter eine Benutzereingabe nehmen, wird es gefährlich. Denn der kann ja jederzeit einen Formatstring eingeben.



  • felted schrieb:

    Kann man char's dynamisch initialisieren, wenn man im Vorfeld nicht weiß, wieviele Zeichen zugewiesen werden sollen?

    Wenn du die Größe zur Laufzeit meinst: Nein
    Arrays kann man nach der Definition nicht mehr in der Größe ändern.
    Du kannst mit malloc/realloc arbeiten.

    Wenn du nicht immer die Zeichen bei der initialisierung zählen willst, kannst du die Größe auch weg lassen, dann wird das Array ausreichen groß (für den Inhalt bei der Definition) gemacht.

    char text[] = "Hallo Welt!"; // Platz für 12 char inkl '\0'
    

    Falls du hier auf den Dateinamen anspielst, so gibt es irgendwo in den Systemheadern auch eine Angabe dazu.
    Es gibt da eine maximale Dateinamenlänge und auch eine für einen voll qualifizierten Dateinamen (mit Laufwerk und Pfad).


  • Mod

    felted schrieb:

    Wie würdest Du/Ihr es lösen?

    Kommt auf das Nieveau an. Auf Anfängerniveau beispielsweise so (geht aber besser, siehe nächste Frage):

    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
      FILE *source, *destination;  // Programmieren auf Englisch!
      char name[1234], new_name[1237] = "am_";
    
      puts("Dateiname?");
      scanf(" %1233[^\n]", name);
    
      source=fopen(name, "r");
      destination=fopen(strcat(new_name, name), "w");
    
      if(!source)
        {
          puts("Datei kann nicht geoeffnet werden.");
          return 1;
        }
    
      printf("Demo, dass strcat nun funktioniert: new_name = \"%s\"\n", new_name);
      return 0;
    }
    

    Kann man char's dynamisch initialisieren, wenn man im Vorfeld nicht weiß, wieviele Zeichen zugewiesen werden sollen?

    Ja, ist in C aber einigermaßen umständlich, weil es mit ziemlich viel Handarbeit verbunden wäre (weshalb ich es hier nicht zeige). Was in mir übrigens den Verdacht aufbringt, dass die Originallösung (bei der angenommen wird, dass die char-Arrays sich magisch vergrößern) vielleicht einfach nur eine schlechte Übersetzung aus einer anderen Sprache ist. In vielen anderen Sprachen, mit spezieller Behandlung für Zeichenketten, ginge das nämlich. Zum Beispiel C++:

    #include <iostream>  // Übersetzungsbuch für Dummies: iostream = stdio.h
    #include <string>    // Übersetzungsbuch für Dummies: string = string.h
    
    void main(void)      // In C++ auch falsch, aber wenn's der Compiler schluckt, stört das den Lehrer nicht
    {
        using namespace std; // Übersetzungsbuch für Dummies: weglassen
        fstream quelle,ziel;    // Übersetzungsbuch für Dummies: fstream = FILE*
        string name, nameneu="am_";  // Übersetzungsbuch für Dummies: string = char*
    
        cout << "Dateiname: ";  // Übersetzungsbuch für Dummies: cout = printf
        getline(cin, name);     // Übersetzungsbuch für Dummies: getline = gets
    
        quelle.open(name, ios::in); // Übersetzungsbuch für Dummies: open = fopen
        ziel=fopen(nameneu + name, ios::out); // Übersetzungsbuch für Dummies: string::ooperator+ = strcat
    }
    

    Und schon hätte man aus diesem stilistisch schlechten, aber funktionierendem, C++-Programm durch eine naive Übersetzung ein nicht-funktionierendes, aber richtig aussehendes, C-Programm gemacht. Wahrscheinlich ist die Ursprungssprache eine andere (denn wer C nicht kann, der kann erst recht kein C++), vielleicht Pascal oder Python, eventuell Java. Aber ich kann mir gut vorstellen, dass der obige Unsinn so oder ähnlich zustande gekommen ist.



  • SeppJ schrieb:

    char name[1234], new_name[1237] = "am_";
    

    Unsinnig. Dateinamen haben ein Limit, unter Windows und Linux liegt das glaub ich bei 255 Zeichen.



  • überzeugter Marxist schrieb:

    SeppJ schrieb:

    char name[1234], new_name[1237] = "am_";
    

    Unsinnig. Dateinamen haben ein Limit, unter Windows und Linux liegt das glaub ich bei 255 Zeichen.

    Schon mal daran gedacht, dass man auch den kompletten Pfad mit eingeben muss.
    Und dafür gibt es auch ein Limit. Das ist aber größer als 255.

    (Das Problem gab es aber auch schon bei den 8.3 Dateinamen von DOS/Windows)


  • Mod

    DirkB schrieb:

    Und dafür gibt es auch ein Limit. Das ist aber größer als 255.

    Nicht einmal das. Die meisten Dateisysteme können beliebig tief verschachtelt werden (oder zumindest sehr, sehr tief, bis ihnen der Platz für Verwaltungsdaten ausgeht). Bloß die System-APIs setzen ein paar Obergrenzen, sofern man diese benutzt. Da ist es dann recht kunterbunt und systemabhängig. Da nehme ich mir heraus, irgendeine Magic Number zu wählen, die voraussichtlich niemals vorkommen wird. Aber 255 kann ich mir schon vorstellen, dass das leicht mal vorkommt. Selbst die ganz, ganz alte Windows-API konnte angeblich schon 260 Zeichen Pfadlänge. Neuerdings angeblich 32767 Unicode-Zeichen (was immer das genau heißen mag. Jedenfalls ist es deutlich mehr als 255.)



  • Also das ist cool:

    scanf(" %1000[^\n]", name);
    

    In meinen Unterlagen steht (sinngemäß):

    ...kann nur einen Wert, bis zum nächsten Leerzeichen (Whitespace) einlesen...

    Mit [^\n] kann man diesen Umstand offenbar umgehen.

    Steht das irgendwo geschrieben, oder ist das ein „Insidertrick“

    Grüße, Felted



  • SeppJ schrieb:

    destination=fopen(strcat(new_name, name), "w");
    
      if(!source)
        {
          puts("Datei kann nicht geoeffnet werden.");
          return 1;
        }
    
      printf("Demo, dass strcat nun funktioniert: new_name = \"%s\"\n", new_name);
      return 0;
    }
    

    Ist es Absicht, dass du kein fclose(destination) aufrufst? Mag zwar formal korrekt sein, aber für einen Anfänger hätte ich das lieber explizit hingeschrieben.


  • Mod

    Klar, es gibt zu allen Funktionen der Standardbibliothek Referenzen. Zunächst mal natürlich im ISO-Standard selbst, aber besser aufbereitet zum Beispiel hier:
    http://www.cplusplus.com/reference/clibrary/

    Das ist zwar eigentlich eine C++-Referenz, aber der Teil über die C-Standardbibliothek (die zu großen Teilen in C++ vorhanden ist), ist so geschrieben, dass die Beispiele auch für C funktionieren.

    Da findet man:
    http://http://www.cplusplus.com/reference/cstdio/scanf/

    Somit entziffern wir, was ich da überhaupt gemacht habe:

    scanf(" %1000[^\n]", name);
    

    Zunächst einmal sehen wir, dass das Leerzeichen am Anfang durchaus eine Bedeutung hat:

    Whitespace character: the function will read and ignore any whitespace characters encountered before the next non-whitespace character (whitespace characters include spaces, newline and tab characters -- see isspace). A single whitespace in the format string validates any quantity of whitespace characters extracted from the stream (including none).

    Dann kommt ein Prozentzeichen, welches einläütet, dass die Eingabe irgendwie interpretiert werden soll. Auf das Prozentzeichen folgt eine Zahl, das muss also ein width-Feld sein:

    Specifies the maximum number of characters to be read in the current reading operation (optional)

    Erst danach kommt das Zeichen, welches bestimmt, was überhaupt gelesen werden soll. Das ist hier kein 's', für das deine Bemerkung bezüglich Leerzeichen (allgemeiner: Whitespace) richtig wäre, sondern '[', gefolgt von '^', welches ganz etwas anderes bedeutet:

    [^characters] Negated scanset: Any number of characters none of them specified as characters between the brackets.

    Somit ist auch der Rest klar: '\n' ist das einzige Zeichen vor der schließenden Klammer, ist also das Zeichen, bei dem abgebrochen werden soll. '\n' ist ein Zeilenumbruch.

    Insgesamt übergeht der Ausdruck also erst alle Whitespace-Zeichen, die eventuell am Anfang eingegeben werden. Dann wird alles gelesen, bis ein Zeilenumbruch erreicht wird oder 1000 Zeichen gelesen wurden. Der Zeilenumbruch wird zwar eingelesen, aber nicht mit abgespeichert.

    1234567 schrieb:

    Ist es Absicht, dass du kein fclose(destination) aufrufst? Mag zwar formal korrekt sein, aber für einen Anfänger hätte ich das lieber explizit hingeschrieben.

    Nein, das ist falsch aus dem Original übernommen. Und da ich sonst hauptsächlich C++ mache, fällt mir ein fehlendes close nicht auf, mir fiel eher im Gegenteil das explizite open auf 🙂 .



  • Wow, super Hilfe. Vielen Dank!
    Felted



  • SeppJ schrieb:

    scanf(" %1000[^\n]", name);
    

    Zunächst einmal sehen wir, dass das Leerzeichen am Anfang durchaus eine Bedeutung hat

    Der Haken daran ist, dass du mit %[] Whitespaces einschließt, mit " " aber führende Whitespaces in der Eingabe ausschließt.
    Also entweder ganz zulassen oder ganz ausschließen.


  • Mod

    Wutz schrieb:

    SeppJ schrieb:

    scanf(" %1000[^\n]", name);
    

    Zunächst einmal sehen wir, dass das Leerzeichen am Anfang durchaus eine Bedeutung hat

    Der Haken daran ist, dass du mit %[] Whitespaces einschließt, mit " " aber führende Whitespaces in der Eingabe ausschließt.

    Genau das möçhte ich aber.

    Also entweder ganz zulassen oder ganz ausschließen.

    Wieso? Das ist doch genau das gewünschte: Dateinamen können nicht mit Leerzeichen anfangen (oder können sie doch?), wohl aber welche enthalten.

    P.S.: Anscheinend können Dateinamen doch mit Leerzeichen beginnen. Cool 🕶 . Dann war die Aktion mit dem Überspringen der Leerzeichen unnötig, sogar hinderlich. Habe diese Regel intuitiv angenommen, hätte vorher mal nachschlagen sollen, was wirklich richtig ist.



  • Hallo,

    habe noch eine Frage zu strcat.

    So stürzt das Programm ab:

    // Programm stürzt ab
        char titel[] = "Buch";
        char zusatz[] = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    }
    

    Das liegt wohl daran, dass das Array titel[] nur genau so groß ist, um den String Buch aufnehmen zu können. Dementsprechend kann die Funktion strcat das Array nicht mehr erweitern und es kommt zum Absturz. Richtig?

    Frage: Wieso stürzt hier das Programm nicht ab?

    // Programm stürzt nicht ab
        char titel[] = "Buch";
        char zusatz[1000] = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    }
    

    Hier ist das Array titel[] doch nach wie vor zu klein, um das Array zusatz[1000] aufnehmen zu können, oder?

    Und noch ein paar Anfängerfragen.
    In meinen Unterlagen zu strcat steht:

    *char *strcat (char *str1, const char str2);

    Das Sternchen(*) steht ja für einen Zeiger.

    Die Parameter str1 und str2 müssen aber keine Zeiger sein, damit es funktioniert.
    Frage: Wieso wird dann bei der Beschreibung der Funktion dies mit einem * dargestellt?

    Frage: Wieso funktioniert:

    // Funktioniert
        char titel[1000] = "Buch";
        char *zusatz = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    

    aber

    // Programm stürzt ab
        char *titel = "Handbuch ";
        char zusatz[1000] = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    

    nicht?

    Grüße und Danke, Felted



  • felted schrieb:

    Das liegt wohl daran, dass das Array titel[] nur genau so groß ist, um den String Buch aufnehmen zu können.

    Richtig.

    Frage: Wieso stürzt hier das Programm nicht ab?

    // Programm stürzt nicht ab
        char titel[] = "Buch";
        char zusatz[1000] = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    }
    

    Hier ist das Array titel[] doch nach wie vor zu klein, um das Array zusatz[1000] aufnehmen zu können, oder?

    Es wäre zwar schön, wenn jedes falsche Programm einen Absturz verursachen würde, aber leider ist das nicht so. Du verursachst hier (und auch bei dem ersten Programm) undefiniertes Verhalten, das heißt, dass im Prinzip alles passsieren kann, der Sprachstandard legt kein Verhalten fest. Von nichts bis Absturz ist alles drin.
    Die konkrete Erklärung geht über das Speicherlayout, also an welcher Stelle im Speicher die Arrays liegen, aber das kann sich von Compiler zu Compiler, auch mit unterschiedlichen Optimierungsleveln oder Debugeinstellungen ändern und taugt daher nicht wirklich zum Verständnis.

    Und noch ein paar Anfängerfragen.
    In meinen Unterlagen zu strcat steht:

    *char *strcat (char *str1, const char str2);

    Das Sternchen(*) steht ja für einen Zeiger.

    Ja.

    Die Parameter str1 und str2 müssen aber keine Zeiger sein, damit es funktioniert.

    Doch, eigentlich schon. Wenn die Argumente keine Zeiger sind, müssen sie wenigstens in Zeiger umwandelbar sein.

    Arrays sind immer in einen Zeiger auf ihr erstes Element konvertierbar.

    Frage: Wieso funktioniert:

    // Funktioniert
        char titel[1000] = "Buch";
        char *zusatz = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    

    Weil titel genug Platz hat.

    // Programm stürzt ab
        char *titel = "Handbuch ";
        char zusatz[1000] = " fuer Anfaenger";
        strcat (titel, zusatz);
        printf("%s \n", titel);
        getch();
    

    nicht?

    Weil titel auf ein (konstantes!) Zeichenkettenliteral zeigt. Veränderungen von Zeichenkettenliteralen verursachen undefiniertes Verhalten.



  • "Programm stürzt ab" ist vom Standard nicht spezifiziert.
    Spezifiziert ist undefiniertes Verhalten, so auch in deinem Fall:
    Das Beschreiben von undefiniertem Speicher führt zu undefiniertem Verhalten gemäß C-Standard, das Programm zur Laufzeit kann abstürzen, muss aber nicht.

    felted schrieb:

    *char *strcat (char *str1, const char str2);

    Das Sternchen(*) steht ja für einen Zeiger.

    Die Parameter str1 und str2 müssen aber keine Zeiger sein, damit es funktioniert.

    Wieso nicht?

    char titel[1000] = "Buch";
        char *zusatz = " fuer Anfaenger"; /* Zeiger auf einen Speicherbereich, der mit dem Inhalt des Stringliterals befüllt ist und beim Ändern einzelner Zeichen zu undefiniertem Verhalten führt */
    

    d.h. also wenn du <zusatz> beschreibst oder es indirekt durch eine Funktion machen lässt, liegt undefiniertes Verhalten vor, genauso (wie oben erwähnt) wie wenn du in undefinierte Speicherbereiche reinschreibst/reinschreiben lässt.
    C-Funktionen prüfen zur Laufzeit nicht, ob der übergebene Zeiger auch auf ausreichenden oder beschreibbaren Speicher verweist.
    Das Programm kann dann abstürzen muss es aber nicht.

    Deshalb gibt es den Qualifizierer const, der anzeigt, dass dieser Speicherbereich nicht beschreibbar ist, viele Compiler warnen hier, wenn sie es merken.

    char *s = "irgendwas";
    /* ist immer gleichbedeutend mit: */
    const char *s = "irgendwas";
    

    d.h. der referenzierte Speicherbereich, auf den s verweist, darf niemals beschrieben werden, sonst UB.


Anmelden zum Antworten