Tom Seidel: Einige C-Fragen

Moin!

Ich versuche mir gerade C etwas näher zu bringen anhand des Galileo Openbooks "C von A bis Z". Beim aktuellen Kapitel (http://www.galileocomputing.de/openbook/c_von_a_bis_z/c_014_005.htm#RxxobKap014005040028BF1F0331A3) habe ich nun jedoch mal eine Frage:

Warum funktioniert das Programm:

#include <stdio.h>
#include <string.h>

char *myfunc(void) {
    char *output = "Output";
    //strcat(output, arg);
    return output;
}

int main(void) {
    char *result = NULL;
    result = myfunc();
    printf("Ergebnis der Funktion: %s\n", result);

return 0;

}

Folgt man der Argumentation im obigen Link, müsste doch ein Segmentation fault zu erwarten sein, da ja output ein lokaler Pointer ist und demnach nach Ausführung der Funktion vom Stack schon verschwunden sein muss.

Für entsprechende Aufklärung wäre ich dankbar!

  1. Da hätte ich jetzt noch glatt etwas vergessen.
    Angenommen ich habe ein String mit dem Format: NAME,ORT,ALTER

    Warum funktioniert dann folgendes nicht:

    char *string = "NAME,ORT,ALTER";
    char *name; // soll enthalten NAME
    char *ort; // soll enthalten ORT
    char *alter; // soll enthalten ALTER

    sscanf(string, "%s,%s,%s", name, ort, alter);

    Hier ist nun so, dass die variable name dann gleich NAME,ORT,ALTER enthält. Wie ist das möglich?

    1. Hallo,

      Da hätte ich jetzt noch glatt etwas vergessen.
      Angenommen ich habe ein String mit dem Format: NAME,ORT,ALTER

      Warum funktioniert dann folgendes nicht:

      char *string = "NAME,ORT,ALTER";
      char *name; // soll enthalten NAME
      char *ort; // soll enthalten ORT
      char *alter; // soll enthalten ALTER

      sscanf(string, "%s,%s,%s", name, ort, alter);

      Das ist GANZ BÖSE. EXTREM BÖSE. Aus zwei Gründen:

      1. Von scanf sollte man die Finger lassen. Die Funktion ist in meinen Augen die schlimmste Erfindung im C-Standard schlechthin. Zum einen ist sie ein potentielles Sicherheitsrisiko (siehe unten) und zum anderen gaukelt sie einem vor, das Umgekehrte von printf() zu sein, was sie aber nicht wirklich ist (und was auch nicht wirklich geht). %s hört bei scanf() nämlich entweder auf, wenn die maximallänge Erreicht ist (%20s) oder wenn ein Whitespace-Zeichen kommt (Leerzeichen, Neue Zeile, Tab).

      2. name, ort und alter sind bei Dir als Variablen nicht initialisiert. Sie zeigen also auf einen beliebigen Speicherbereich. Der wird dann mit dem scanf()-Ergebnis noch vollgeschrieben. Damit überschreibst Du beliebigen Speicher Deines Programms (u.U. halt auch den Stack und damit Rücksprungadressen - Hallo liebe Sicherheitslücke) oder das ganze segfaultet eben. Zudem kann man mit scanf() extrem leicht vergessen, eine Begrenzung für die Maximallänge des Puffers mit anzugeben (Du tust es hier ja auch nicht). Keine gute Idee.

      Wenn Du den obigen String richtig parsen willst, dann mache das lieber manuell, dann weißt Du nämlich auch, was Du tust.

      Hier gibt es zwei Varianten:

      1. Du allozierst immer automatisch auf dem Heap Puffer mit malloc(), die groß genug sind, den Teilstring zu halten. Du solltest diese Puffer nach Verwendung halt wieder per free() loswerden.

      2. Du legst auf dem Stack Puffer an, die eine feste Größe (zur Compilezeit) haben und kopierst halt maximal so viel hinein, wie der Puffer groß ist.

      Zu Variante 1:

      Mittels strstr() kannst Du die Position eines Substrings in einem String bestimmen. Damit kannst Du sehen, wo die "," vorkommen.

      Das folgende Beispiel verdeutlicht beide Varianten. Es ist nicht extrem schön und man kann das ganze mit ordentlich Pointerarithmetik noch viel eleganter machen, ich hoffe jedoch, dass es die Herangehensweise an so ein Problem in C verdeutlicht und verständlich macht, wo die Fallstricke liegen.

      #include <stdio.h>  
      #include <stdlib.h>  
      #include <string.h>  
        
      // So gewählt, dass NAME und ORT reinpassen, ALTER aber nicht mehr  
      #define PUFFER_GROESSE 5  
        
      const char *daten = "NAME,ORT,ALTER";  
        
      void ausgabe (char *name, char *ort, char *alter) {  
       printf ("Name: '%s', Ort: '%s', Alter: '%s'\n", name, ort, alter);  
      }  
        
      void fehler (void) {  
       printf ("Fehler!\n");  
      }  
        
      void heap(void) {  
       char *name, *ort, *alter;  
       const char *pos1, *pos2;  
       pos1 = daten;  
       pos2 = strstr (pos1, ",");  
       // , nicht gefunden  
       if (!pos2) {  
        fehler ();  
        return;  
       }  
       // richtige größe allozieren; pos2 - pos1 ist die länge des Strings vor dem  
       // Komma, +1 wegen 0-Byte am Ende  
       // calloc verwenden, damit der Speicher mit 0-Bytes initialisiert ist  
       // (alternativ: malloc() und letztes Byte auf 0 setzen)  
       name = calloc(1, (size_t)(pos2 - pos1 + 1));  
       // Daten kopieren  
       strncpy (name, pos1, (size_t)(pos2 - pos1));  
       // Nächste position bestimmen  
       pos1 = pos2 + 1;  
       pos2 = strstr (pos1, ",");  
       if (!pos2) {  
        fehler ();  
        return;  
       }  
       ort = calloc(1, (size_t)(pos2 - pos1 + 1));  
       strncpy (ort, pos1, (size_t)(pos2 - pos1));  
        
       pos1 = pos2 + 1;  
       // Wir nehmen an, dass es kein weiteres Komma gibt, daher suchen wir nicht  
       // danach, sondern kopieren einfach den Rest des Strings  
       alter = calloc(1, strlen(pos1) + 1);  
       // der Puffer ist groß genug, also können wir durchaus strcpy() verwenden  
       // das ANSONSTEN aber böse ist  
       strcpy (alter, pos1);  
        
       // Fertig  
       ausgabe (name, ort, alter);  
        
       // Puffer wieder loswerden  
       free (name);  
       free (ort);  
       free (alter);  
      }  
        
      void puffer(void) {  
       char name[PUFFER_GROESSE], ort[PUFFER_GROESSE], alter[PUFFER_GROESSE];  
       const char *pos1, *pos2;  
       size_t laenge;  
        
       // Alle Puffer mit 0-Bytes initialisieren  
       memset (name, 0, sizeof(name));  
       memset (ort, 0, sizeof(ort));  
       memset (alter, 0, sizeof(alter));  
        
       pos1 = daten;  
       pos2 = strstr (pos1, ",");  
       // , nicht gefunden  
       if (!pos2) {  
        fehler ();  
        return;  
       }  
       // Daten kopieren (maximal aber Puffergröße)  
       laenge = (size_t)(pos2 - pos1);  
       if (laenge > PUFFER_GROESSE - 1) {  
        laenge = PUFFER_GROESSE - 1;  
       }  
       strncpy (name, pos1, laenge);  
       // Nächste position bestimmen  
       pos1 = pos2 + 1;  
       pos2 = strstr (pos1, ",");  
       if (!pos2) {  
        fehler ();  
        return;  
       }  
       laenge = (size_t)(pos2 - pos1);  
       if (laenge > PUFFER_GROESSE - 1) {  
        laenge = PUFFER_GROESSE - 1;  
       }  
       strncpy (ort, pos1, laenge);  
        
       pos1 = pos2 + 1;  
       // Hier müssen wir wieder strncpy verwenden weil wir den Puffer nicht  
       // dynamisch angelegt haben  
       laenge = strlen (pos1);  
       if (laenge > PUFFER_GROESSE - 1) {  
        laenge = PUFFER_GROESSE - 1;  
       }  
       strncpy (alter, pos1, laenge);  
        
       // Fertig  
       ausgabe (name, ort, alter);  
        
       // Puffer existieren auf dem Stack, müssen nicht aufgeräumt werden  
      }  
        
      int main (int argc, char **argv) {  
       heap ();  
       puffer ();  
       return 0;  
      }
      

      Allgemein kann man sagen: Wenn man mit reinem C Strings manipulieren will, sucht man sich am besten eine Bibliothek, die einem die ganze "Drecksarbeit" abnimmt. Die C-Standardbibliothek ist in der Hinsicht eher schlecht als recht und sehr viele Stringfunktionen sind in irgend einer Hinsicht böse, auch wenn es nicht den Anschein hat (Threadsicherheit und Reentrancy sind auch gerne nochmal ein Problem, ich denke da an strtok() o.ä.). Hinzu kommt noch, dass man mit 8-Bit-Strings nicht mehr alles abdecken kann und dass die ganze C-Standardbibliothek nicht die leiseste Ahnung von Unicode etc. hat.

      Insofern: Wenn Du viel mit Strings machen willst, dann Suche Dir entweder eine brauchbare Bibliothek, die Dir die Drecksarbeit abnimmt oder nimm eine andere Programmiersprache als C. Bei allem anderen schießt Du Dir nur selbst in die Knie (siehst ja an meinem Beispiel, dass es nicht ganz einfach ist, einen derartigen String richtig zu parsen).

      Viele Grüße,
      Christian

      1. Hallo Christian!

        1. Von scanf sollte man die Finger lassen. Die Funktion ist in meinen Augen die schlimmste Erfindung im C-Standard schlechthin. Zum einen ist sie ein potentielles Sicherheitsrisiko (siehe unten) und zum anderen gaukelt sie einem vor, das Umgekehrte von printf() zu sein, was sie aber nicht wirklich ist (und was auch nicht wirklich geht). %s hört bei scanf() nämlich entweder auf, wenn die maximallänge Erreicht ist (%20s) oder wenn ein Whitespace-Zeichen kommt (Leerzeichen, Neue Zeile, Tab).

        Wie kann denn von diesem Beispiel ausgehend eine Sicherheitslücke entstehen? Klar, das Programm kann abstürzen, aber wie genau kann man dies so ausnutzen, dass das Programm z.B. etwas was macht, was es gar nicht soll?

        1. name, ort und alter sind bei Dir als Variablen nicht initialisiert. Sie zeigen also auf einen beliebigen Speicherbereich. Der wird dann mit dem scanf()-Ergebnis noch vollgeschrieben. Damit überschreibst Du beliebigen Speicher Deines Programms (u.U. halt auch den Stack und damit Rücksprungadressen - Hallo liebe Sicherheitslücke) oder das ganze segfaultet eben. Zudem kann man mit scanf() extrem leicht vergessen, eine Begrenzung für die Maximallänge des Puffers mit anzugeben (Du tust es hier ja auch nicht). Keine gute Idee.

        Wäre als initialisierung ein *name = NULL ausreichend?

        Wenn Du den obigen String richtig parsen willst, dann mache das lieber manuell, dann weißt Du nämlich auch, was Du tust.

        Hier gibt es zwei Varianten:

        1. Du allozierst immer automatisch auf dem Heap Puffer mit malloc(), die groß genug sind, den Teilstring zu halten. Du solltest diese Puffer nach Verwendung halt wieder per free() loswerden.

        2. Du legst auf dem Stack Puffer an, die eine feste Größe (zur Compilezeit) haben und kopierst halt maximal so viel hinein, wie der Puffer groß ist.

        Zu Variante 1:

        Mittels strstr() kannst Du die Position eines Substrings in einem String bestimmen. Damit kannst Du sehen, wo die "," vorkommen.

        Das folgende Beispiel verdeutlicht beide Varianten. Es ist nicht extrem schön und man kann das ganze mit ordentlich Pointerarithmetik noch viel eleganter machen, ich hoffe jedoch, dass es die Herangehensweise an so ein Problem in C verdeutlicht und verständlich macht, wo die Fallstricke liegen.

        Wie würde die Version mit Pointerarithmetik aussehen? Irgendwie lerne ich durch Beispielcode immer am besten. Wenn du Zeit und Lust hast mir dies nochmals dadurch zu zeigen, wäre mir sehr geholfen :-)

        #include <stdio.h>

        #include <stdlib.h>
        #include <string.h>

        // So gewählt, dass NAME und ORT reinpassen, ALTER aber nicht mehr
        #define PUFFER_GROESSE 5

        const char *daten = "NAME,ORT,ALTER";

        void ausgabe (char *name, char *ort, char *alter) {
        printf ("Name: '%s', Ort: '%s', Alter: '%s'\n", name, ort, alter);
        }

        void fehler (void) {
        printf ("Fehler!\n");
        }

        void heap(void) {
        char *name, *ort, *alter;
        const char *pos1, *pos2;
        pos1 = daten;
        pos2 = strstr (pos1, ",");
        // , nicht gefunden
        if (!pos2) {
          fehler ();
          return;
        }
        // richtige größe allozieren; pos2 - pos1 ist die länge des Strings vor dem
        // Komma, +1 wegen 0-Byte am Ende
        // calloc verwenden, damit der Speicher mit 0-Bytes initialisiert ist
        // (alternativ: malloc() und letztes Byte auf 0 setzen)
        name = calloc(1, (size_t)(pos2 - pos1 + 1))

        Wie würde dann das Beispiel mit malloc() genau aussehen?

        name = (char *) malloc((size_t)(pos2 - pos1 + 1));

        WIe kann ich nun bei einem Pointer das letzte Zeichen ansprechen? Wäre name vom Typ char[] ginge das ja mit name[strlen(name)-1]. Warum ist ein Cast auf size_t notwendig?

        // Daten kopieren
        strncpy (name, pos1, (size_t)(pos2 - pos1));
        // Nächste position bestimmen
        pos1 = pos2 + 1;
        pos2 = strstr (pos1, ",");
        if (!pos2) {
          fehler ();
          return;
        }
        ort = calloc(1, (size_t)(pos2 - pos1 + 1));
        strncpy (ort, pos1, (size_t)(pos2 - pos1));

        pos1 = pos2 + 1;
        // Wir nehmen an, dass es kein weiteres Komma gibt, daher suchen wir nicht
        // danach, sondern kopieren einfach den Rest des Strings
        alter = calloc(1, strlen(pos1) + 1);
        // der Puffer ist groß genug, also können wir durchaus strcpy() verwenden
        // das ANSONSTEN aber böse ist
        strcpy (alter, pos1);

        Warum ist strcpy "böse" - auch wieder weil keine Bereichsüberprüfung erfolgt?

        // Fertig
        ausgabe (name, ort, alter);

        // Puffer wieder loswerden
        free (name);
        free (ort);
        free (alter);
        }

        void puffer(void) {
        char name[PUFFER_GROESSE], ort[PUFFER_GROESSE], alter[PUFFER_GROESSE];
        const char *pos1, *pos2;
        size_t laenge;

        // Alle Puffer mit 0-Bytes initialisieren
        memset (name, 0, sizeof(name));
        memset (ort, 0, sizeof(ort));
        memset (alter, 0, sizeof(alter));

        Ist also eine Initialisierung also grundsätzlich notwendig, oder?

        pos1 = daten;
        pos2 = strstr (pos1, ",");
        // , nicht gefunden
        if (!pos2) {
          fehler ();
          return;
        }
        // Daten kopieren (maximal aber Puffergröße)
        laenge = (size_t)(pos2 - pos1);
        if (laenge > PUFFER_GROESSE - 1) {
          laenge = PUFFER_GROESSE - 1;
        }
        strncpy (name, pos1, laenge);
        // Nächste position bestimmen
        pos1 = pos2 + 1;
        pos2 = strstr (pos1, ",");
        if (!pos2) {
          fehler ();
          return;
        }
        laenge = (size_t)(pos2 - pos1);
        if (laenge > PUFFER_GROESSE - 1) {
          laenge = PUFFER_GROESSE - 1;
        }
        strncpy (ort, pos1, laenge);

        pos1 = pos2 + 1;
        // Hier müssen wir wieder strncpy verwenden weil wir den Puffer nicht
        // dynamisch angelegt haben
        laenge = strlen (pos1);
        if (laenge > PUFFER_GROESSE - 1) {
          laenge = PUFFER_GROESSE - 1;
        }
        strncpy (alter, pos1, laenge);

        // Fertig
        ausgabe (name, ort, alter);

        // Puffer existieren auf dem Stack, müssen nicht aufgeräumt werden
        }

        int main (int argc, char **argv) {
        heap ();
        puffer ();
        return 0;
        }

        
        >   
        > Allgemein kann man sagen: Wenn man mit reinem C Strings manipulieren will, sucht man sich am besten eine Bibliothek, die einem die ganze "Drecksarbeit" abnimmt. Die C-Standardbibliothek ist in der Hinsicht eher schlecht als recht und sehr viele Stringfunktionen sind in irgend einer Hinsicht böse, auch wenn es nicht den Anschein hat (Threadsicherheit und Reentrancy sind auch gerne nochmal ein Problem, ich denke da an strtok() o.ä.). Hinzu kommt noch, dass man mit 8-Bit-Strings nicht mehr alles abdecken kann und dass die ganze C-Standardbibliothek nicht die leiseste Ahnung von Unicode etc. hat.  
        >   
        > Insofern: Wenn Du viel mit Strings machen willst, dann Suche Dir entweder eine brauchbare Bibliothek, die Dir die Drecksarbeit abnimmt oder nimm eine andere Programmiersprache als C. Bei allem anderen schießt Du Dir nur selbst in die Knie (siehst ja an meinem Beispiel, dass es nicht ganz einfach ist, einen derartigen String richtig zu parsen).  
          
        Könnte an dieser Stelle eine Empfehlung für eine bestimmte C-Bibliothek erfolgen?  
        
        
        1. Hallo!

          1. Von scanf sollte man die Finger lassen. Die Funktion ist in meinen Augen die schlimmste Erfindung im C-Standard schlechthin. Zum einen ist sie ein potentielles Sicherheitsrisiko (siehe unten) und zum anderen gaukelt sie einem vor, das Umgekehrte von printf() zu sein, was sie aber nicht wirklich ist (und was auch nicht wirklich geht). %s hört bei scanf() nämlich entweder auf, wenn die maximallänge Erreicht ist (%20s) oder wenn ein Whitespace-Zeichen kommt (Leerzeichen, Neue Zeile, Tab).

          Wie kann denn von diesem Beispiel ausgehend eine Sicherheitslücke entstehen? Klar, das Programm kann abstürzen, aber wie genau kann man dies so ausnutzen, dass das Programm z.B. etwas was macht, was es gar nicht soll?

          Naja, im Prinzip kannst Du Dir das so überlegen:

          void tuWas (void) {
            char buf[20]);
            scanf("%s", buf);
          }

          Wenn der eingegebene String nun mehr als 19 Zeichen hat (0-Byte muss man ja berücksichtigen), dann wird weiter in den Stack hineingeschrieben. Irgendwann kommt im Stack dann die Rücksprungadresse (die wird dort gespeichert, damit die Funktion wieder zurückkehren kann). Wenn Du diese Rücksprungadresse auch überschreibst, kannst Du den Prozessor beim Verlassen der Funktion an eine beliebige Codestelle springen lassen - und damit z.B. auch an den Anfang von buf (dem Puffer), wo Du dann Deinen eigenen kompilierten Code hingetan hast.

          1. name, ort und alter sind bei Dir als Variablen nicht initialisiert. Sie zeigen also auf einen beliebigen Speicherbereich. Der wird dann mit dem scanf()-Ergebnis noch vollgeschrieben. Damit überschreibst Du beliebigen Speicher Deines Programms (u.U. halt auch den Stack und damit Rücksprungadressen - Hallo liebe Sicherheitslücke) oder das ganze segfaultet eben. Zudem kann man mit scanf() extrem leicht vergessen, eine Begrenzung für die Maximallänge des Puffers mit anzugeben (Du tust es hier ja auch nicht). Keine gute Idee.

          Wäre als initialisierung ein *name = NULL ausreichend?

          Nein, das würde gar nicht funktionieren. Dann würde die Variable name nämlich auf NULL zeigen und scanf() würde sofort abstürzen. scanf() kann die Variable name selbst nämlich nicht ändern (wie auch, bei Funktionsparametern), scanf() ändert nur den Speicherbereich auf den die Variable zeigt! Und wenn der NULL ist, dann schmiert's Dir halt ab.

          Der Witz ist: Initialisieren heißt hier, dass die Variable name auf einen Speicherbereich zeigen muss, der genug Platz für die Daten, die scanf() reinschreiben will hat. Die Standard-C-Funktionen allozieren in der Regel nicht von alleine.

          Das folgende Beispiel verdeutlicht beide Varianten. Es ist nicht extrem schön und man kann das ganze mit ordentlich Pointerarithmetik noch viel eleganter machen, ich hoffe jedoch, dass es die Herangehensweise an so ein Problem in C verdeutlicht und verständlich macht, wo die Fallstricke liegen.

          Wie würde die Version mit Pointerarithmetik aussehen? Irgendwie lerne ich durch Beispielcode immer am besten.

          Die Variante mit dem Heap kann man nicht sonderlich optimieren durch Pointerarithmetik (weil die ja erst wissen muss, wie groß der Puffer sein soll, bevor sie den Puffer füllt), daher muss man immer zuerst nach dem Komma suchen, dann den Puffer anlegen und dann erst kopieren. Ob Du nun das strstr() in einer Schleife selbst nachbaust oder nicht gibt sich nicht viel.

          Bei der Variante mit der festen Puffergröße kann man damit die Logik dagegen schon etwas ändern:

          void puffer(void) {  
           char name[PUFFER_GROESSE], ort[PUFFER_GROESSE], alter[PUFFER_GROESSE];  
           const char *pos1, *pos2;  
           char *ptr;  
            
           // Alle Puffer mit 0-Bytes initialisieren  
           memset (name, 0, sizeof(name));  
           memset (ort, 0, sizeof(ort));  
           memset (alter, 0, sizeof(alter));  
            
           pos1 = daten;  
           for (pos2 = pos1, ptr = name; *pos2 && *pos2 != ','; pos2++, ptr++) {  
            if (ptr - name < PUFFER_GROESSE - 1) {  
              *ptr = *pos2;  
            }  
           }  
           // , nicht gefunden  
           if (!*pos2) {  
            fehler ();  
            return;  
           }  
           // Nächste position bestimmen  
           pos1 = pos2 + 1;  
           for (pos2 = pos1, ptr = ort; *pos2 && *pos2 != ','; pos2++, ptr++) {  
            if (ptr - ort < PUFFER_GROESSE - 1) {  
              *ptr = *pos2;  
            }  
           }  
           // , nicht gefunden  
           if (!*pos2) {  
            fehler ();  
            return;  
           }  
            
           pos1 = pos2 + 1;  
           for (pos2 = pos1, ptr = alter; *pos2 && *pos2 != ','; pos2++, ptr++) {  
            if (ptr - alter < PUFFER_GROESSE - 1) {  
              *ptr = *pos2;  
            }  
           }  
            
           // Fertig  
           ausgabe (name, ort, alter);  
            
           // Puffer existieren auf dem Stack, müssen nicht aufgeräumt werden  
          }
          

          // Komma, +1 wegen 0-Byte am Ende
          // calloc verwenden, damit der Speicher mit 0-Bytes initialisiert ist
          // (alternativ: malloc() und letztes Byte auf 0 setzen)
          name = calloc(1, (size_t)(pos2 - pos1 + 1))

          Wie würde dann das Beispiel mit malloc() genau aussehen?

          name = (char *) malloc((size_t)(pos2 - pos1 + 1));

          Ja.

          WIe kann ich nun bei einem Pointer das letzte Zeichen ansprechen? Wäre name vom Typ char[] ginge das ja mit name[strlen(name)-1].

          Nein! Bzw. beim Ansprechen ist es egal ob es char[] oder char * ist - allerdings musst Du zwei Dinge beachten:

          1) strlen() sucht nach dem 0-Byte. Wenn Du's noch nicht eingefügt hast,
              dann fährst Du strlen() damit gegen die Wand!
           2) strncpy() terminiert den Zielstring nur dann mit einem 0-Byte, wenn
              der Quellstring zuende ist. Das ist hier aber nicht der Fall, da der
              Quellstring an der Stelle, wo wir aufhören wollen, ein Komma hat.

          Aus dem Grund muss man den 0-String selbst terminieren, wenn man nicht schon den kompletten Puffer vorher auf 0 initialisiert hat (deswegen habe ich calloc() verwendet):

          strncpy (name, pos1, (size_t)(pos2 - pos1));  
          name[(size_t)(pos2 - pos1)] = '\0';
          

          (Wichtig: name hat hier (pos2 - pos1 + 1) Elemente, siehe den alloc-Aufruf, und nur (pos2 - pos1) davon sind gefüllt, d.h. das (pos2 - pos1 + 1)te Element wird mit der zweiten Zeile auf 0 gesetzt.)

          In der Puffer-Variante ohne Zeigerarithmetik:

          strncpy (name, pos1, laenge);  
          name[laenge] = '\0';
          

          In der Puffer-Variante mit Zeigerarithmetik:

           for (pos2 = pos1, ptr = name; *pos2 && *pos2 != ','; pos2++) {  
            if (ptr - name < PUFFER_GROESSE - 1) {  
              *ptr = *pos2;  
              ptr++;  
            }  
           }  
          *ptr = '\0';
          

          (Hier muss man sich halt die letzte gültige Position im name-Array merken, d.h. ptr nur inkrementiren, solange er noch gültig ist.)

          (In der Puffer-Variante bräuchte es dann das memset() nicht mehr.)

          Warum ist ein Cast auf size_t notwendig?

          Ist es nicht, aber es ist IMHO besserer Stil, size_t ist der für alle Größen im Speicher verantwortliche Typ.

          // der Puffer ist groß genug, also können wir durchaus strcpy() verwenden
          // das ANSONSTEN aber böse ist
          strcpy (alter, pos1);

          Warum ist strcpy "böse" - auch wieder weil keine Bereichsüberprüfung erfolgt?

          Ja. Hier haben wir allerdings den Zielpuffer (alter) vorher alloziert in einer Größe, die den String garantiert halten kann (wir haben ja strlen() reingesteckt), daher ist das hier OK. Wenn man so eine Situation allerdings nicht hat (was viel häufiger der Fall ist), ist strcpy() böse, weil das einfach immer weiterschreibt.

          // Alle Puffer mit 0-Bytes initialisieren
          memset (name, 0, sizeof(name));
          memset (ort, 0, sizeof(ort));
          memset (alter, 0, sizeof(alter));

          Ist also eine Initialisierung also grundsätzlich notwendig, oder?

          Nein, wenn man das 0-Byte manuell setzt, nicht, aber in meinen Augen spart eine vorige Initialisierung des gesamten Puffers mit 0-Bytes eine Menge Ärger später, weil man sich dann nicht von Hand drum kümmern muss. Ist natürlich eine Frage, wie viel Performance man bereit ist, einzubüßen. Bei normalen Strings spielt das keine Rolle, wenn die Daten aber sehr groß werden (Megabytes), sollte man dann doch besser das 0-Byte direkt setzen, dann macht sich das bemerkbar.

          Insofern: Wenn Du viel mit Strings machen willst, dann Suche Dir entweder eine brauchbare Bibliothek, die Dir die Drecksarbeit abnimmt oder nimm eine andere Programmiersprache als C. Bei allem anderen schießt Du Dir nur selbst in die Knie (siehst ja an meinem Beispiel, dass es nicht ganz einfach ist, einen derartigen String richtig zu parsen).

          Könnte an dieser Stelle eine Empfehlung für eine bestimmte C-Bibliothek erfolgen?

          Nein, leider nicht. Ich habe "da draußen" einige in meinen Augen brauchbare Dinge gesehen, aber wirklich begeistert war ich noch von keiner Geschichte.

          Viele Grüße,
          Christian

    2. Da hätte ich jetzt noch glatt etwas vergessen.
      Angenommen ich habe ein String mit dem Format: NAME,ORT,ALTER

      Warum funktioniert dann folgendes nicht:

      char *string = "NAME,ORT,ALTER";
      char *name; // soll enthalten NAME
      char *ort; // soll enthalten ORT
      char *alter; // soll enthalten ALTER

      Die Zeiger "name", "ort" und "alter" sind nicht initialisiert!

      Ein einfaches Beispiel:

        
      #include <stdio.h>  
      #include <stdlib.h>  
      #include <string.h>  
        
      int main(void) {  
        
         int iRetCode = 0;  
         const char* pszString = "NAME,ORT,ALTER";  
        
         char* pszName  = (char*) calloc(strlen(pszString) /* + 1 fuer die terminierende \0 */ + 1, sizeof(char));  
         char* pszOrt   = (char*) calloc(strlen(pszString) + 1, sizeof(char));  
         char* pszAlter = (char*) calloc(strlen(pszString) + 1, sizeof(char));  
        
         if ((NULL == pszName) || (NULL == pszOrt) || (NULL == pszName)) {  
        
            /*  
             *    iRetVal setzen,  
             *    weitere Fehlerbehandlung  
             */  
         } else {  
        
            if ((int)strlen(pszString) >  
                  /*  
                   * sscanf liest bei %s bis zu Whitespaces; z:B. " ";  
                   * bei anderen Separatoren brauchst Du %[]  
                   */  
                  sscanf(pszString, "%[^,],%[^,],%[^,]", pszName, pszOrt, pszAlter)) {  
        
               /*  
                *    String wurde nicht vollstaendig konsumiert;  
                *    iRetVal setzen,  
                *    weitere Fehlerbehandlung  
                */  
            } else {  
        
               /*  
                  Strings verarbeiten  
                */  
            };  
          }  
        
         free(pszName); /* free(NULL) ist immer ok! */  
         free(pszOrt);  
         free(pszAlter);  
        
         return iRetCode;  
      }  
        
      
      

      Hier ist nun so, dass die variable name dann gleich NAME,ORT,ALTER enthält. Wie ist das möglich?

      sscanf hat in Deiner Version den gesamten String schon mit dem ERSTEN Format-Spezifizierer konsumiert da er kein "Whitespace" fand. Siehe Code.

      ---

      Es gibt in diesem Thread einige Stellungnahmen zu sscanf etc; aus Diskussionen rund um Für und Wider der Standardfunktionen für reines "C" halte ich mich i.A. heraus, allerdings würde ich ein C++-Programm nicht die Qualitätssicherung passieren lassen, wenn solche Funktionen darin direkt verwandt würden.

      Aber C++ ist ein anderes Thema.

      Sinnvoll könnte es hier aber sein, sich über UNICODE zumindest Gedanken zu machen; bei den meisten Code-Beispielen für Einsteiger wird darauf nicht geachtet, aber UNICDOE spielt in der C/C++-Praxis eine erhebliche Rolle.

      Ferner sind bei der Standardbibliothek Aspekte der Nebenläufigkeit(aka "Concurrency", "Multi-Threading") zu bedenken wenn die Anwendungen komplexer werden.

      Grüsse

      Solkar

      P.S.: Ungarische Notation, z.B. "psz<Irgendwas>" oder i<Irgendwas> ist natürlich nicht Pflicht und auch sehr unumstritten im Praxiseinsatz, aber ich habe die Erfahrung gemacht, dass dies Einsteigern hilft, die Übersicht zu behalten.

  2. Hallo,

    Warum funktioniert das Programm:

    #include <stdio.h>
    #include <string.h>

    char *myfunc(void) {
        char *output = "Output";
        //strcat(output, arg);
        return output;
    }

    int main(void) {
        char *result = NULL;
        result = myfunc();
        printf("Ergebnis der Funktion: %s\n", result);

    return 0;

    }

    Folgt man der Argumentation im obigen Link, müsste doch ein Segmentation fault zu erwarten sein, da ja output ein lokaler Pointer ist und demnach nach Ausführung der Funktion vom Stack schon verschwunden sein muss.

    Nein, der Speicherbereich, auf den output zeigt, wird hier *nicht* auf dem Stack alloziert. Der String "Output" ist nämlich (weil's ein String ist, der 1:1 in der Source-Datei vorkommt) schon in der ausführbaren Datei, die Du erstellst, enthalten und wird beim Laden des Programms in den Speicher geladen (genau so wie alle anderen Strings die Du direkt in den Source schreibst). Auf diesen (im Programm immer verfügbaren) Speicherbereich zeigt dann output, das Du zurückgibst.

    Etwas anderes wäre es, wenn Du folgendes machen würdest:

    char output[] = "Output";

    Dann wird nämlich auf dem Stack ein Array der Größe 7 (6 Zeichen + 0-Byte) angelegt und dort dann "Output" reingeschrieben und das zurückgegeben. Wenn Du das dann ausführst, dann bekommst Du je nach Zufall entweder eine Segfault oder zumindest Datenmüll als Ausgabe.

    Viele Grüße,
    Christian

    1. Hallo Christian,

      Nein, der Speicherbereich, auf den output zeigt, wird hier *nicht* auf dem Stack alloziert. Der String "Output" ist nämlich (weil's ein String ist, der 1:1 in der Source-Datei vorkommt) schon in der ausführbaren Datei, die Du erstellst, enthalten und wird beim Laden des Programms in den Speicher geladen (genau so wie alle anderen Strings die Du direkt in den Source schreibst). Auf diesen (im Programm immer verfügbaren) Speicherbereich zeigt dann output, das Du zurückgibst.

      guter Hinweis, daran habe ich nicht gedacht.
      Das *kann* sein, muss aber nicht. Ob konstante Strings direkt im Code gespeichert und direkt referenziert werden, ist nämlich vom Compiler (bzw. dessen Optionen) abhängig.

      char output[] = "Output";
      Dann wird nämlich auf dem Stack ein Array der Größe 7 (6 Zeichen + 0-Byte) angelegt und dort dann "Output" reingeschrieben und das zurückgegeben.

      Ja, hier ist es eindeutig.

      Schönes Wochenende noch,
       Martin

      --
      Okay, Alkohol ist keine Antwort.
      Aber manchmal vergisst man beim Trinken wenigstens die Frage.
      1. Hallo Martin,

        Nein, der Speicherbereich, auf den output zeigt, wird hier *nicht* auf dem Stack alloziert. Der String "Output" ist nämlich (weil's ein String ist, der 1:1 in der Source-Datei vorkommt) schon in der ausführbaren Datei, die Du erstellst, enthalten und wird beim Laden des Programms in den Speicher geladen (genau so wie alle anderen Strings die Du direkt in den Source schreibst). Auf diesen (im Programm immer verfügbaren) Speicherbereich zeigt dann output, das Du zurückgibst.

        guter Hinweis, daran habe ich nicht gedacht.
        Das *kann* sein, muss aber nicht. Ob konstante Strings direkt im Code gespeichert und direkt referenziert werden, ist nämlich vom Compiler (bzw. dessen Optionen) abhängig.

        Nein, ISO C 99 (PDF, 3.6 MiB) sagt ganz klar (§ 6.5.2.5, Seite 77):

        | EXAMPLE 5      The following three expressions have different meanings:
        |          "/tmp/fileXXXXXX"
        |          (char []){"/tmp/fileXXXXXX"}
        |          (const char []){"/tmp/fileXXXXXX"}
        | The first always has static storage duration and has type array of char,
        | but need not be modifiable; the last two have automatic storage duration
        | when they occur within the body of a function, and the first of these two
        | is modifiable.

        Betonung liegt hierbei auf dem Satz: "The first always has static storage". Zu "static storage" sagt der Standard in § 6.2.4, Seite 32:

        | An object whose identifier is declared with external or internal linkage,
        | or with the storage-class specifier static has static storage duration. Its
        | lifetime is the entire execution of the program and its stored value is
        | initialized only once, prior to program startup.

        Das heißt: Eine Funktion, die return "foo"; oder char *p = "foo"; return foo; macht, wird nach dem C-Standard immer funktionieren und niemals Probleme machen.

        Zudem: Es wäre für den Compiler ziemlich unsinnig, bei char *p = "foo" extra noch Code zu generieren (!), der den Inhalt von "foo" extra auf dem Stack abglegt - müsste bei jedem Betreten der Funktion ja geschehen.

        Viele Grüße,
        Christian

        1. Hallo,

          Zu "static storage" sagt der Standard in § 6.2.4, Seite 32:

          | An object whose identifier is declared with external or internal linkage,
          | or with the storage-class specifier static has static storage duration. Its
          | lifetime is the entire execution of the program and its stored value is
          | initialized only once, prior to program startup.

          Das heißt: Eine Funktion, die return "foo"; oder char *p = "foo"; return foo; macht, wird nach dem C-Standard immer funktionieren und niemals Probleme machen.

          ja, ich habe etwas verwechselt. Nämlich dass manche Compiler per Option die Auswahl bieten, konstante Daten (also Initialisierungswerte) wahlweise im Datensegment oder im Codesegment zu speichern.

          Zudem: Es wäre für den Compiler ziemlich unsinnig, bei char *p = "foo" extra noch Code zu generieren (!), der den Inhalt von "foo" extra auf dem Stack abglegt - müsste bei jedem Betreten der Funktion ja geschehen.

          Ja, stimmt. :-)

          Ciao,
           Martin

          --
          Einer aktuellen Erhebung zufolge sind zehn von neun Ehefrauen eifersüchtig auf ihren Mann.
  3. Hallo Tom,

    Ich versuche mir gerade C etwas näher zu bringen ...

    das ist in unserem Metier nie ein Fehler. :-)

    char *myfunc(void) {
        char *output = "Output";
        //strcat(output, arg);
        return output;
    }

    Was macht diese Funktion? Sie legt einen String (also ein char-Array) als lokale Variable auf dem Stack an, und liefert einen Zeiger darauf zurück.

    char *result = NULL;
        result = myfunc();
        printf("Ergebnis der Funktion: %s\n", result);

    Folgt man der Argumentation im obigen Link, müsste doch ein Segmentation fault zu erwarten sein, da ja output ein lokaler Pointer ist und demnach nach Ausführung der Funktion vom Stack schon verschwunden sein muss.

    Nein. "Verschwunden" ist erstmal nichts. Der Zeiger auf die auf dem Stack abgelegte Zeichenkette wird ja durch das return nach oben durchgereicht. Dann zeigt result also immer noch auf denselben Speicherbereich auf dem Stack wie der lokale Zeiger output innerhalb der Funktion. Da ein Programm ohne Einschränkung auf seinen eigenen Stack zugreifen darf, produziert das erstmal keinen Fehler.

    Problematisch wird's allerdings, wenn nach der Rückkehr von myfunc() eine weitere Funktion aufgerufen wird. Die würde nämlich die noch auf dem Stack liegende Zeichenkette mit ihren eigenen lokalen Variablen überschreiben. Das Programm läuft dann zwar immer noch "fehlerfrei", aber deine Daten werden sich immer wieder auf unerklärliche Weise verändern.

    char *string = "NAME,ORT,ALTER";
    char *name; // soll enthalten NAME
    char *ort; // soll enthalten ORT
    char *alter; // soll enthalten ALTER
    sscanf(string, "%s,%s,%s", name, ort, alter);

    Hier ist nun so, dass die variable name dann gleich NAME,ORT,ALTER enthält. Wie ist das möglich?

    Das ist mir auch schleierhaft. Denn *dieses* Beispiel müsste tatsächlich einen Segmentation Fault erzeugen. ;-)
    Die oben deklarierten Zeiger name, ort, alter sind nicht initialisiert. Sie zeigen daher auf zufällige Speicherbereiche, und sscanf() kopiert dann Zeichen für Zeichen aus dem Eingabestring in die durch name, ort, alter referenzierten Bereiche - also buchstäblich "irgendwo" hin.

    Ich würde aber grundsätzlich von der Verwendung der scanf-Funktionen abraten. Denn sie haben keine "eingebaute" Bereichsprüfung, können also nicht feststellen, ob der Puffer, auf den die übergebenen Zeiger vereisen, auch wirklich groß genug für das Ergebnis ist (bzw. ob es überhaupt ein gültiger Bereich ist). Funktionen, die eine Eingabe untersuchen und auswerten, schreibe ich mir lieber selbst - und zwar immer so, dass sie auch eine Bereichsüberprüfung vornehmen können oder alternativ die maximal zulässige Anzahl Zeichen von der aufrufenden Funktion übermittelt bekommen.

    So long,
     Martin

    --
    Der Bäcker schlägt die Fliegen tot
    Und macht daraus Rosinenbrot.
    1. Moin Martin!

      char *string = "NAME,ORT,ALTER";
      char *name; // soll enthalten NAME
      char *ort; // soll enthalten ORT
      char *alter; // soll enthalten ALTER
      sscanf(string, "%s,%s,%s", name, ort, alter);

      Hier ist nun so, dass die variable name dann gleich NAME,ORT,ALTER enthält. Wie ist das möglich?

      Das ist mir auch schleierhaft. Denn *dieses* Beispiel müsste tatsächlich einen Segmentation Fault erzeugen. ;-)
      Die oben deklarierten Zeiger name, ort, alter sind nicht initialisiert. Sie zeigen daher auf zufällige Speicherbereiche, und sscanf() kopiert dann Zeichen für Zeichen aus dem Eingabestring in die durch name, ort, alter referenzierten Bereiche - also buchstäblich "irgendwo" hin.

      Ich würde aber grundsätzlich von der Verwendung der scanf-Funktionen abraten. Denn sie haben keine "eingebaute" Bereichsprüfung, können also nicht feststellen, ob der Puffer, auf den die übergebenen Zeiger vereisen, auch wirklich groß genug für das Ergebnis ist (bzw. ob es überhaupt ein gültiger Bereich ist). Funktionen, die eine Eingabe untersuchen und auswerten, schreibe ich mir lieber selbst - und zwar immer so, dass sie auch eine Bereichsüberprüfung vornehmen können oder alternativ die maximal zulässige Anzahl Zeichen von der aufrufenden Funktion übermittelt bekommen.

      Erstmal danke für die Antwort - leider kann ich mir als Anfänger noch nicht auf alles einen Reim machen. Könntest du die letzte Aussage vielleicht etwas konkretisieren - wie würde man mein Ziel praktisch am besten erreichen?

      Danke für die Mühe!

    2. Hallo Martin,

      Ich würde aber grundsätzlich von der Verwendung der scanf-Funktionen abraten.

      Denn sie haben keine "eingebaute" Bereichsprüfung, können also nicht feststellen, ob der Puffer, auf den die übergebenen Zeiger vereisen, auch wirklich groß genug für das Ergebnis ist (bzw. ob es überhaupt ein gültiger Bereich ist).

      Naja, man kann schon %20s machen, tu scanf() nicht *zu* sehr unrecht. ;-) Bei %20s vs. %s vergisst man das 20 halt mal gerne, bei einer Funktion, die die Puffergröße explizit als Parameter erwartet passiert das dagegen nicht, da meckert der Compiler, wenn man den vergisst.

      Funktionen, die eine Eingabe untersuchen und auswerten, schreibe ich mir lieber selbst - und zwar immer so, dass sie auch eine Bereichsüberprüfung vornehmen können oder alternativ die maximal zulässige Anzahl Zeichen von der aufrufenden Funktion übermittelt bekommen.

      Daher auch hier: Volle Zustimmung.

      Viele Grüße,
      Christian

      1. Hallo nochmal,

        Ich würde aber grundsätzlich von der Verwendung der scanf-Funktionen abraten.

        Nach diesem Zitat ist irgendwie mein "FULL ACK" flöten gegangen. ;-)

        Viele Grüße,
        Christian

  4. Hallo!

    Moin!

    Ich versuche mir gerade C etwas näher zu bringen anhand des Galileo Openbooks "C von A bis Z". Beim aktuellen Kapitel (http://www.galileocomputing.de/openbook/c_von_a_bis_z/c_014_005.htm#RxxobKap014005040028BF1F0331A3) habe ich nun jedoch mal eine Frage:

    Warum funktioniert das Programm:

    #include <stdio.h>
    #include <string.h>

    char *myfunc(void) {
        char *output = "Output";
        //strcat(output, arg);
        return output;
    }

    ; Das Kompilat dazu sieht so aus:

    4:    char *myfunc(void) {
    00401020   push        ebp
    00401021   mov         ebp,esp
    00401023   push        ecx

    ; Hier

    5:        char *output = "Output";
    00401024   mov         dword ptr [output],offset ___xt_z(0x00410a30)+10Ch

    ; wird nichts alloziert, sondern an die (Stack) Variable die Adresse einer
    ; Zeichenkette, irgendwo "fest" im Speicher liegt, zugewiesen

    6:        //strcat(output, arg);

    ; Hier

    7:        return output;
    0040102B   mov         eax,dword ptr [output]

    ; wird dann der WERT der Variablen nach EAX gerettet,

    8:    }
    0040102E   mov         esp,ebp

    ; und ier wird dann lediglich der Stackrahmen freigegeben,
    ; die Stack-VARIABLE "output" ist danach ungültig; aber
    ; ihr ehemaliger WERT ist immer noch eine gültige feste Speicheradresse
    ; die später weiterverwandt wird

    00401030   pop         ebp
    00401031   ret
    9:

    In dem Registerdump

    EAX = 00420250 EBX = 7FFDF000
    ECX = 00000001 EDX = 004202D0
    ESI = 02ADF80C EDI = 7C9100E0
    EIP = 00401024 ESP = 0012FF6C
    EBP = 0012FF70 EFL = 00000216

    CS = 001B DS = 0023 ES = 0023
    SS = 0023 FS = 003B GS = 0000 OV=0
    UP=0 EI=1 PL=0 ZR=0 AC=1 PE=1 CY=0

    dazu kannst Du sehen, dass SS(STACK), ES(HEAP) und DS(DATA) alle die gleichen Selektoren haben; deshalb werden hier nur kurze Offset-Zeiger benutzt.

    Wenn Du dir mal einen Hexdump Deines Programms anschaust, wirst Du darin auch die - feste - Zeichenkette "Output" entdecken; der UNIX-Befehl "strings" zeigt sie Dir auch an.

    Allerdings ändert das nichts an der Grundregel, dass man Adressen lokal allozierter Variablen nicht zurückgeben darf; hier geht es zwar mal, aber  guter Stil wäre sowas nicht.

    Grüsse

    Solkar

  5. Beim Durchschauen einiger Beispiele bin ich wiederum auf folgendes gestoßen:

    Gegeben ist folgendes:

    char *ptr;
    char str[] = "ARR.AY";
    printf("Alt: %s", str);
    ptr = strrchr(str, '.');
    *ptr = '\0';
    printf("Neu: %s", str);

    Man sucht also nach einem bestimmten Zeichen ('.') und ersetzt dieses durch ein 0-Byte, um den String zu terminieren, obwohl ja noch die anderen Zeichen existieren.
    Was steckt hier dahinter - warum macht man sowas?

    1. Hallo,

      Man sucht also nach einem bestimmten Zeichen ('.') und ersetzt dieses durch ein 0-Byte, um den String zu terminieren, obwohl ja noch die anderen Zeichen existieren.
      Was steckt hier dahinter - warum macht man sowas?

      um den String zu kürzen - wozu sonst?
      Dass hinter dem abschließenden Nullbyte noch ein paar Zeichen im Speicher rumlungern, der ja ohnehin reserviert ist, spielt dabei doch keine Rolle.

      So long,
       Martin

      --
      Das einzige Problem beim Nichtstun: Man weiß nie, wann man damit fertig ist.
      1. Moin Martin!

        Man sucht also nach einem bestimmten Zeichen ('.') und ersetzt dieses durch ein 0-Byte, um den String zu terminieren, obwohl ja noch die anderen Zeichen existieren.
        Was steckt hier dahinter - warum macht man sowas?

        um den String zu kürzen - wozu sonst?
        Dass hinter dem abschließenden Nullbyte noch ein paar Zeichen im Speicher rumlungern, der ja ohnehin reserviert ist, spielt dabei doch keine Rolle.

        Achso ok. Mir erschien es nur irgendwie komisch, die restlichen Zeichen einfach stehen zu lassen - immerhin belegen die ja unnötig Speicher im Prinzip.

        Jetzt hätte ich noch eine Frage: Ich habe eine Funktion deren Signatur folgendermaßen aussieht

        void func(int arg1, char **output)
            output[0] = sprintf("Ergebnis: %d", 1 * arg1);
            output[1] = sprintf("Ergebnis: %d", 2 * arg1);
            output[2] = sprintf("Ergebnis: %d", 3 * arg1);
        }

        Wie initialisiere ich nun char** output vor der Übergabe an die Funktion richtig, damit ich keinen Speicherzugriffsfehler erhalte?

        1. Hallo Thomas,

          Was steckt hier dahinter - warum macht man sowas?
          um den String zu kürzen - wozu sonst?
          Achso ok. Mir erschien es nur irgendwie komisch, die restlichen Zeichen einfach stehen zu lassen - immerhin belegen die ja unnötig Speicher im Prinzip.

          ja schon, aber wie gesagt: Der Speicherbereich ist dem Programm bzw. diesem Zweig des Programms ja sowieso für diesen Zweck zugeteilt. Wenn du Besuch hast und nicht möchtest, dass die Leute in dein Schlafzimmer stolpern, machst du ja auch einfach nur die Tür zu (Nullbyte, hier Ende), anstatt das Schlafzimmer abzureißen oder auszuräumen. ;-)

          Jetzt hätte ich noch eine Frage: Ich habe eine Funktion deren Signatur folgendermaßen aussieht

          Den Begriff "Signatur" habe ich in dem Zusammenhang noch nicht gehört. Deklaration oder Prototyp ist mir geläufig, auch Funktionskopf habe ich schon oft gehört. Signatur finde ich nicht ganz so intuitiv. Naja, egal.

          Wie initialisiere ich nun char** output vor der Übergabe an die Funktion richtig, damit ich keinen Speicherzugriffsfehler erhalte?

          void func(int arg1, char **output)
              output[0] = sprintf("Ergebnis: %d", 1 * arg1);
              output[1] = sprintf("Ergebnis: %d", 2 * arg1);
              output[2] = sprintf("Ergebnis: %d", 3 * arg1);
          }

          Übrigens fehlt deiner Funktion die öffnende geschweifte Klammer. ;-)
          Sooo - output ist also ein Zeiger auf einen Zeiger auf char. Da du in der Funktion über einen Index auf drei Elemente zugreifst, muss es ein Zeiger auf mindestens drei Zeiger sein, also etwa so:

          char *ergebnis[3];
            ...
            func(foo, ergebnis);

          Dein Beispiel klemmt aber noch an einer anderen Stelle: Der Ergebnistyp von sprintf ist int, denn sprintf gibt die Anzahl der geschriebenen Zeichen zurück, nicht den erzeugten String. Dafür fehlt in deinem Aufruf von sprintf der erste Parameter, nämlich der Puffer, wo sprintf den erzeugten String hinschreiben soll. Das ist ein wesentlicher Unterschied zur sprintf-Implementierung z.B. in PHP.

          char str[60];
            int anzahl;

          anzahl = sprintf(str, "Konstante: %u", 400);

          Dieser Codeausschnitt erzeugt in str[] die Zeichenkette "400" und ein abschließendes Nullbyte (die restlichen 56 Zeichen bleiben ungenutzt), und gibt den Wert 3 an anzahl zurück. Beachte den ersten Parameter, der angibt, wo sprintf den String hinschreiben soll.

          Schönen Sonntag noch,
           Martin

          --
          "Drogen machen gleichgültig."
           - "Na und? Mir doch egal."
        2. Hallo!

          Jetzt hätte ich noch eine Frage: Ich habe eine Funktion deren Signatur folgendermaßen aussieht

          void func(int arg1, char **output)
              output[0] = sprintf("Ergebnis: %d", 1 * arg1);
              output[1] = sprintf("Ergebnis: %d", 2 * arg1);
              output[2] = sprintf("Ergebnis: %d", 3 * arg1);
          }

          Aua, das ist GANZ BÖSE! sprintf() hat folgende Signatur:

          int sprintf (char *puffer, const char *format, ...);

          Das heißt: Du gibst als ersten Parameter Deinen Puffer hinein und danach alle Parameter, die printf() selbst erwartet. [1] Als Rückgabewert erhälst Du die Anzahl an geschriebenen Zeichen.

          sprintf() ist in C aber auch böse, weil auch hier wieder keine Bereichsprüfung stattfindet. Du möchtest hier folgendes machen:

          void func(int arg1, char **output, size_t buflen) {  
            snprintf(output[0], buf, "Ergebnis: %s", 1 * arg1);  
            snprintf(output[1], buf, "Ergebnis: %s", 2 * arg1);  
            snprintf(output[2], buf, "Ergebnis: %s", 3 * arg1);  
          }
          

          Der zusätzliche Parameter buflen würde hier dann angeben, wie groß der Puffer ingesamt ist, damit er nicht überschrieben wird. snprintf() prüft im Gegensatz zu printf() wieder die Puffergröße.

          Wie initialisiere ich nun char** output vor der Übergabe an die Funktion richtig, damit ich keinen Speicherzugriffsfehler erhalte?

          Du willst ein Array mit 3 Elementen, die eine bestimmte Puffergröße haben:

          #define PUFFERGROESSE 200  
            
          // ...  
            
          // Ein Array mit 3 char*-Pointern allozieren  
          char **output = (char **)malloc(sizeof(char *)*3);  
          int i;  
          // Nun im Array 3 Puffer anlegen  
          for (i = 0; i < 3; i++) {  
            output[i] = (char *)malloc(PUFFERGROESSE);  
          }  
          // Nun übergeben  
          func (15, output, PUFFERGROESSE);  
            
          // nun irgendwas mit output machen  
            
          // Nun wieder loswerden  
          for (i = 0; i < 3; i++) {  
            free (output[i]);  
          }  
          free (output);
          

          Viele Grüße,
          Christian

          [1] In PHP (Scriptsprache!) gibt's ein sprintf(), das eine Zeichenkette *zurückgibt*, das ist aber in der internen C-Implementierung relativ komplex gebaut (und es gibt in der C-Standardbibliothek kein Äquivalent dafür), verwechselst Du das evlt. damit?

          1. Moin!

            [...]

            Danke für die vorherigen Erklärungen.

            #define PUFFERGROESSE 200

            // ...

            // Ein Array mit 3 char*-Pointern allozieren
            char **output = (char **)malloc(sizeof(char *)*3);
            int i;
            // Nun im Array 3 Puffer anlegen
            for (i = 0; i < 3; i++) {
              output[i] = (char *)malloc(PUFFERGROESSE);
            }
            // Nun übergeben
            func (15, output, PUFFERGROESSE);

            // nun irgendwas mit output machen

            // Nun wieder loswerden
            for (i = 0; i < 3; i++) {
              free (output[i]);
            }
            free (output);

              
            Mit diesen Beispielcode habe ich es nun geschafft. Über einen kleinen Punkt bin ich aber noch hängengeblieben:  
              
            Ich wollte folgendes machen:  
              
            output[0] = name; // name ist dabei vom Typ char \*  
              
            Dies führt aber stets zu komischen Ergebnissen. Mit strcpy(output[0], name) hat es dagegen funktioniert. Wann muss man also strcpy benutzen und kann nicht direkt die Strings einanderzuweisen?  
              
            
            > [1] In PHP (Scriptsprache!) gibt's ein sprintf(), das eine Zeichenkette \*zurückgibt\*, das ist aber in der internen C-Implementierung relativ komplex gebaut (und es gibt in der C-Standardbibliothek kein Äquivalent dafür), verwechselst Du das evlt. damit?  
              
            Ja, das war der Fehler u.a.  
            
            
            1. Hallo,

              Mit diesen Beispielcode habe ich es nun geschafft. Über einen kleinen Punkt bin ich aber noch hängengeblieben:

              Ich wollte folgendes machen:

              output[0] = name; // name ist dabei vom Typ char *

              Dies führt aber stets zu komischen Ergebnissen.

              Ja klar. Wenn Du Dir die Variable »output« mal aufmalst, dann sieht die so aus:

              +--------+         +-----------+                  +------------------+
              | output |-------->| output[0] |----------------->|                  |
              +--------+         | output[1] |---               +------------------+
                                 | output[2] |-  \              +------------------+
                                 +-----------+ \  ------------>|                  |
                                                \               +------------------+
                                                 \              +------------------+
                                                  ------------>|                  |
                                                                +------------------+

              +------+                  +------------------+
                                      | name |----------------->|                  |
                                      +------+                  +------------------+

              Die leeren Kästchen sind alles Speicherbereiche, in denen Strings zu finden sind.

              Wenn Du nun strcpy machst, dann wird einfach der Inhalt von dem Puffer, auf den die Variable name zeigt, in den Puffer output[0] kopiert. [1] Die Zeiger selbst ändern sich nicht.

              Wenn Du dagegen output[0] = name machst, dann sieht das ganze so aus:

              +--------+         +-----------+                  +------------------+
              | output |-------->| output[0] |-------           |                  |
              +--------+         | output[1] |---    \          +------------------+
                                 | output[2] |-  \    |         +------------------+
                                 +-----------+ \  ---|-------->|                  |
                                                \     |         +------------------+
                                                 \    |         +------------------+
                                                  ---|-------->|                  |
                                                       \        +------------------+
                                                        \                         +------+           \      +------------------+
                                      | name |------------+---->|                  |
                                      +------+                  +------------------+

              Das heißt: Sowohl output[0] und name zeigen jetzt auf den gleichen Speicherbereich. Jetzt gibt es damit schon drei potentielle Probleme:

              1. Ich weiß nicht, woher bei Dir die Variable name jetzt kommt. Wenn das ein lokaler Puffer ist, hast Du das Problem, dass der Stack ja wieder weg ist, sobald die aufgerufene Funktion endet - (die gleiche Problematik wie wenn Du das direkt per return zurückgibst!).

              2. Da (zumindest wenn Du meinem Beispiel folgst) die Elemente von output alle per malloc() alloziert werden, werden sie auch hinterher mit free() wieder freigegeben. Wenn Du free() aber auf einen Puffer anwendest, der nicht per malloc() alloziert wurde, dann hat das böse Konsequenzen. Und selbst wenn (!) der Puffer von name bereits per malloc() alloziert wurde, würdest Du name selbst ja vmtl. auch an anderer Stelle per free() wieder loswerden und zweimal free() auf die gleiche Variable ist genauso schlecht.

              3. Der Puffer, auf den output[0] urpsrünglich zeigte, wird nirgends mehr referenziert und bleibt daher im Speicher bestehen. Er wird erst freigegeben, sobald das Programm sich beendet. Bei länger laufenden Programmen führt das zu sogenannten "Speicherlecks", d.h. die Programme verwenden immer mehr Speicher, selbst wenn sie ihn nicht mehr benötigten.

              Ich habe mir das Buch, mit dem Du C lernen wolltest, jetzt nicht angesehen, aber anscheinend scheint es gerade auf die Subtilitäten bei Zeigern nicht gut genug einzugehen. Ich empfehle Dir daher mal das Standardwerk Programmieren in C von Kernighan/Ritchie.

              Viele Grüße,
              Christian

              [1] Übrigens: Auch hier wieder: strcpy() ist so wie Du es verwenden willst vermutlich böse, weil keine Bereichsprüfung für den Zielpuffer durchgeführt wird.