Resource icon

[C] Benutzereingaben oder Textdateien zeilenweise einlesen

Programmiersprache(n)
C
Betriebssystem(e)
unspezifisch
Q 1: Welche Funktionen bietet C an?
A: Die entsprechenden Funktionen finden sich im Header stdio.h und müssen in der Lage sein, an einem Zeilenumbruch abzubrechen (Zeichenkette lesen) oder einen Zeilenumbruch zu detektieren (zeichenweise lesen)

fgetc - Zeichen von einem Stream (Datei oder Benutzereingabe) lesen
fgets - Zeichenkette von einem Stream (Datei oder Benutzereingabe) lesen
getc - Zeichen von einem Stream (Datei oder Benutzereingabe) lesen
getchar - Zeichen von stdin (Benutzereingabe) lesen
gets - Zeichenkette von stdin (Benutzereingabe) lesen
fscanf - Formatierte Daten von einem Stream (Datei oder Benutzereingabe) lesen
scanf - Formatierte Daten von stdin (Benutzereingabe) lesen

Q 2: Welche Funktionen davon sollte ich NICHT verwenden?
A: Wir wissen nicht was ein Benutzer eingeben wird oder was in einer Textdatei zu finden ist. Auch wenn wir einen Benutzer bitten nur eine bestimmte Eingabe vorzunehmen oder wenn wir annehmen in einer Datei befinden sich nur Zeilen, die eine bestimmte Länge nicht überschreitet, können wir nicht sicherstellen, dass das auch tatsächlich so sein wird. Alles was passieren kann, wird passieren, es ist nur eine Frage der Zeit. Also ist es die Aufgabe des Programmierers, sein Programm so abzusichern, dass Fehler beim Lesen der Daten abgefangen werden und keinesfalls Speicherverletzungen vorkommen können, die einen Programmabsturz oder Schlimmeres verursachen könnten.
Wenn wir also davon ausgehen müssen, dass wir nicht wissen wie lang die eingelesene Zeichenkette sein wird, dürfen wir keinesfalls Funktionen verwenden, die die Eingabe nicht limitieren. Dies betrifft Funktionen, die eine Zeichenkette einlesen, ohne dass man ihnen die maximale Länge übergeben kann.

- char* gets(char* str)
Wie man sieht wird lediglich ein Pointer auf einen Speicherbereich übergeben. Wie groß dieser ist, kann die Funktion nicht feststellen. So etwas schreit regelrecht nach einem Pufferüberlauf. Diese Funktion ist also ein absolutes No Go.

- int fscanf(FILE* stream, const char* format, ... )
Formatierte Daten zu lesen, ist oft bequem. Für das Einlesen einer Zeichenkette (mit Typbezeichner %s) gilt hier aber das gleiche wie bei gets(). Es ist immerhin möglich im Typbezeichner die Länge der zu lesenden Zeichenkette anzugeben, diese muss der Puffergröße - 1 entsprechen. Für numerische Typen ist die Funktion ggf. verwendbar, da dort die die Größe des Speicherbereiches durch den Typ selbst bekannt ist. Wird die Typbreite durch die Eingabe überschritten, ist der konvertierte Wert jedoch falsch. Die Funktion selbst bietet keine Möglichkeit diesen Fehler zu erkennen. Es gibt außerdem keinen Rückschluss darauf, bei welchem Zeichen die Konvertierung abgebrochen wurde. Eine Analyse der eingelesenen bzw. im Stream verbleibenden Zeichen ist erforderlich.

- int scanf(const char* format, ... )
Hier gelten die gleiche Einschränkung, wie für fscanf(). Zusätzlich ist man lediglich in der Lage, vom stdin zu lesen.

Q 3: Dann bleibt also nur fgets, um eine Zeichenkette zu lesen?
A: Ja, wenn man eine Zeichenfolge als Block einlesen will.
- char* fgets (char *str, int num, FILE* stream)
Hier muss man den Parameter num angeben, der die Größe des Speicherbereichs angibt. Eine Speicherverletzung ist somit ausgeschlossen. Es gibt aber einiges zu beachten:
  • Der Speicherbereich muss groß genug sein, um die Zeile, den Zeilenumbruch (der immer mit eingelesen wird) und die Nullterminierung aufzunehmen.
  • Ist der Speicherbereich zu klein, werden num-1 Zeichen eingelesen und die Nullterminierung gesetzt. Die restlichen Zeichen und der Zeilenumbruch verbleiben im Eingabepuffer und warten darauf gelesen zu werden.

Q 3.1: Woher weiß ich, ob der Speicherbereich groß genug gewählt ist.
A: Wie bei der Antwort auf Q2 schon angemerkt, du kannst es nicht wissen. Es gibt drei Herangehensweisen:
  1. Je größer der Puffer gewählt wird, desto geringer ist die Wahrscheinlichkeit dass dessen Grenzen erreicht werden. Wenn man nur kurze Eingaben, wie "ja" oder "nein" erwartet, ist ein Puffer für 100 Zeichen sicher ausreichend groß gewählt.
  2. Wenn Eingaben, die eine bestimmte Länge überschreiten, bei der nachfolgenden Verarbeitung sowieso zum Fehler führen würden, muss man den Puffer nicht unnötig groß wählen. Soll die Eingabe bspw. zu einem int konvertiert werden, so benötigt man maximal 10 Stellen für die Ziffern + 1 Stelle für ein eventuelles Vorzeichen + 1 Stelle für den Zeilenumbruch + 1 Stelle für die Nullterminierung (wenn man von einer Implementierung eines int mit einer Breite von 32 Bit ausgeht).
  3. Zum Teil hat das Betriebssystem bereits Limits, die man nicht überschreiten kann. Bspw. kann eine Benutzereingabe in das Windows Konsolefenster max. 4094 Zeichen lang sein. Weitere Tastatureingaben werden ignoriert, lediglich der Abschluss der Zeile mit Enter wird noch akzeptiert. Somit ist eine Puffergröße von 4096 Zeichen (wegen Zeilenumbruch und Nullterminierung) ausreichend.
Q 3.2: Falls die Größe des Speicherbereichs überschritten wird, wie werde ich die verbleibenden Zeichen im Eingabepuffer los?
A: Man ließt immer wieder, dass man dies mit fflush(stdin) erledigen könnte. Das ist nicht korrekt und ist nur für Windows garantiert. Der C Standard geht hier von undefiniertem Verhalten aus. Man sollte also einen Weg wählen, der plattformunabhängig funktioniert. Z.B.:
while ((int ch = fgetc(pFile)) != '\n' && ch != EOF);

Q 4: Was ist nun aber, wenn ich wirklich nicht abschätzen kann, wie lang die Zeile sein wird?
A: Dann bleibt dir immer noch das zeichenweise Einlesen. Speicher kannst du bspw. mit malloc() und realloc() dynamisch reservieren. Sobald ein Zeilenumbruch gelesen wird oder das Streamende erreicht ist, brichst du das Einlesen ab.
- int fgetc(FILE* stream) und int getc(FILE* stream)
Beide Funktionen sind völlig äquivalent. Es wird jeweils ein Zeichen aus dem Stream gelesen und dessen ASCII Wert zurück gegeben.
- int getchar(void)
Eine weitere Möglichkeit, es wird aber lediglich vom stdin gelesen.

Q 5: Wenn die Eingabefunktionen für formatierte Daten nicht alle Fehler abfangen, wie soll ich dann meine Eingaben zu einem numerischen Wert konvertieren?
A: Der Header stdlib.h bietet die strto...() Funktionen (z.B. long int strtol(const char* str, char** endptr, int base) für die Konvertierung zu einem long int). Mit diesen Funktionen ist man in der Lage zu erkennen, ob die gesamte Zeichenkette konvertierbar war oder vorher abgebrochen wurde (endptr). Außerdem wird errno gesetzt, wodurch indiziert wird, ob die Typbreite überschritten wurde.
Verwende niemals die ato...() Funktionen. Damit ist dies alles nicht möglich, nicht einmal die Unterscheidung, ob der Rückgabewert 0 ist, weil die Eingabe zu 0 konvertiert wurde oder weil ein Fehler aufgetreten ist.

Q 6: Alle Eingabefunktionen benötigen Enter oder einen Zeilenumbruch um zurück zu geben. Gibt es auch eine Funktion, die bei einem beliebigen Tastendruck den ASCII Wert sofort zurück gibt?
A: Grundsätzlich nein. Keine der Standardfunktionen zeigt so ein Verhalten. Die getch() Funktion wird aber verbreitet angewendet. Sie findet sich unter Windows im Header conio.h und bei *nixoiden Betriebssystemen im (n)curses.h.

Q 7: Gibt es weitere plattform- oder compilerspezifische Funktionen, die Anwendung finden könnten?
A: Natürlich. Beispielhaft:
- Die getline() Funktion als GNU-Erweiterung, die speziell zu diesem Zweck entwickelt wurde.
- Unter Windows gibt es spezifische Funktionen mit Anhang _s (z.B. gets_s() als Pendant zur gets() Standardfunktion). Diese Funktionen haben eine veränderte Syntax, um Speicherverletzungen zu verhindern, werden bislang aber nur vom VC (MS Visual Studio) unterstützt. Zu finden sind sie im MSDN als Verlinkung in den Referenzen der Standardfunktionen.

Q 8: Viel Theorie. Wie würde denn die Umsetzung in einem Beispielcode aussehen?
A: Wie man die Umsetzung angeht, hängt davon ab, auf welche Fehlermöglichkeiten man eingehen will, ob man performancekritischen Code schreibt etc. Das Folgende ist also wirklich nur als Beispiel und nicht als Nonplusultra anzusehen.
C:
#include <stdio.h>  // ReadLineCopy,  ReadLineAlloc
#include <stdlib.h> // ReadLineAlloc, StringToLong
#include <string.h> // ReadLineCopy,  StringToLong
#include <ctype.h>  // StringToLong
#include <errno.h>  // StringToLong


/** \brief  Read one line out of a file stream and save it in the passed buffer.
*  \param  file_stream  Input stream either of a file opened with reading access or the stdin stream.
*  \param  buffer       Pointer to the first element of a buffer array that receives the line read.
*  \param  buffer_size  Size of the buffer array in number of chars.
*  \param  p_length     Pointer to a variable of type size_t that receives the length of the line read. This parameter can be NULL.
*  \return Value passed to buffer, pointing to the zero-terminated string that contains the line read. NULL if the function failed. */
char *ReadLineCopy(FILE *const file_stream, char *const buffer, const size_t buffer_size, size_t *const p_length)
{
  int    read = EOF; // character read
  size_t len  =  0u; // length of the line read

  if (buffer == NULL
      || buffer_size == 0u
      || file_stream == NULL
      || ferror(file_stream) != 0
      || feof(file_stream) != 0
      || fgets(buffer, buffer_size, file_stream) == NULL // read not more than buffer_size - 1 characters including the line feed character if present
      || (len = strlen(buffer)) == 0u) // determine the length and make sure it was greater than zero
    return NULL;

  if (buffer[len - 1u] != '\n') // that is, either the buffer did not have enough capacity or the end of the file stream was reached
  {
    if (p_length != NULL)
      *p_length = len;

    if ((read = fgetc(file_stream)) == '\n' || read == EOF) // it's okay as long as the very next character read is either carriage return or we were at EOF
      return buffer;

    while (ferror(file_stream) == 0 && (read = fgetc(file_stream)) != '\n' && read != EOF); // otherwise read the remaining characters that would have been belonging to the line
    return NULL;
  }

  buffer[--len] = 0; // replace the cariage return character with a terminating zero

  if (p_length != NULL)
    *p_length = len;

  return buffer;
}


/** \brief  Read one line out of a file stream and save it in an allocated memory space.
*  \param  file_stream  Input stream either of a file opened with reading access or the stdin stream.
*  \param  p_length     Pointer to a variable of type size_t that receives the length of the line read. This parameter can be NULL.
*  \return Zero-terminated string that contains the line read. NULL if the function failed.
*          The returned pointer has to be released using free(). */
char *ReadLineAlloc(FILE *const file_stream, size_t *const p_length)
{
  int      read     =  EOF; // character read
  char   * buffer   = NULL, // buffer for the characters read
         * cur      = NULL; // cursor pointing to the current position in the buffer
  size_t   len      =   0u, // counter for the characters read
           capacity = 128u; // size of the allocated buffer;
                            // 128 should be reasonable to start with, but you could initialize the variable with
                            // another power of two in case you already know that the lines are usually longer

  if (file_stream == NULL
      || ferror(file_stream) != 0
      || feof(file_stream) != 0
      || (buffer = cur = (char*)malloc(capacity)) == NULL)
    return NULL;

  while ((read = fgetc(file_stream)) != '\n' && read != EOF) // read character-wise
  {
    if (ferror(file_stream) != 0)
    {
      free(buffer);
      return NULL;
    }

    *cur++ = read;

    if (++len == capacity) // not even enough free space for the terminating zero
    {
      char *tmp = (char*)realloc(buffer, (capacity <<= 1)); // double the buffer size
      if (tmp == NULL)
      {
        free(buffer);
        return NULL;
      }

      buffer = tmp;
      cur = buffer + len; // the cursor has to be moved to the new memory space if realloc returned a different pointer value
    }
  }

  if (p_length != NULL)
    *p_length = len;

  *cur = 0; // terminating zero
  return buffer;
}


/** \brief Conversion of a string to long int using the strtol() function.
*  \param  str      String to be converted.
*  \param  base     Numerical base to be used.
*  \param  p_num    Pointer to a long int that receives the converted value.
*  \param  end_ptr  Pointer to a pointer to char that indicates the character where the conversion stopped. This parameter can be NULL.
*  \return unsigned int  Return value as a bitset
*                        0 if No error occurred. Elsewise one or more of the following flags might be set:
*                        2^0 - Whitespaces found at the beginning of the string,
*                        2^1 - End of the string not numerical,
*                        2^2 - not convertable or type overflow,
*                        2^3 - Fatal error. */
unsigned StringToLong(const char *const str, const int base, long *const p_num, char **const end_ptr)
{
  const char  * truncated =  str; // Points to the first character in the string that is not a whitespace.
  char        * end       = NULL; // Pointer to the character where strtol() stopped the conversion.
  unsigned      flags     =   0u; // Returned bitset.

  if (p_num != NULL)
    *p_num = 0;

  if (end_ptr != NULL)
    *end_ptr = NULL;

  if (str == NULL || p_num == NULL || base < 0 || base > 36 || base == 1)
    return 8;

  while (isspace(*truncated) != 0)
    ++truncated; // Skip leading whitespaces.

  errno = 0;
  *p_num = strtol(str, &end, base);

  if (ERANGE == errno)
    flags |= 4u; // Type width overflow.

  if ( // Figure out whether strtol() returned zero because '0' was read or because the string was not convertable  - E.g.: "0$" okay, "$" failure
    *p_num == 0
    && !( // Don't miss the ! at this point.
      truncated[0] == '0'
      || (
        (truncated[0] == '+' || truncated[0] == '-')
        && truncated[1] == '0'
      )
    )
  )
  {
    flags |= 4u;
  }

  if (*end != '\0')
    flags |= 2u;

  if (isspace(*str) != 0)
    flags |= 1u;

  if (end_ptr)
    *end_ptr = end;

  return flags;
}


// extra kurz, um Fehler generieren zu können
#define BUFCOUNT 10
// wenn StringToLong() eine Zahl von 1 bis 3 zurückgibt, war strtol() in der Lage einen Long Integer vom Stringanfang zu extrahieren
#define OK  0
#define ERR 4

int main(void)
{
  char buffer[BUFCOUNT] = {0}, *str = NULL, *remaining = NULL;
  size_t length = 0;

  long num = 0;
  unsigned flags = -1, i = 0;
  const char *const err_msg[4] = {"vorangestellte Whitespaces", "Abbruch vor Stringende", "nicht als Long Integer interpretiertbar", "Anwender-/Verarbeitungsfehler"};
  const char *const ack_msg[4] = {"keine vorangestellten Whitespaces", "Abbruch am Stringende", "als Long Integer interpretiertbar", "kein Anwender-/Verarbeitungsfehler"};

/* ReadLineCopy */
  printf("~~~~~~~~~~~~~~~~~~~~~~~~~~\nEingabe: ");
  if (ReadLineCopy(stdin, buffer, BUFCOUNT, &length) != NULL) // Benutzereingabe einholen und auf Verarbeitungsfehler prüfen.
    printf("\nReadLineCopy - OK:\n Laenge: %u\n String: '%s'\n", (unsigned)length, buffer); // Die Eingabe passt in den Puffer.
  else if (*buffer)
    fprintf(stderr, "\nReadLineCopy - Fehler:\n Laenge: %u\n String: '%s'\n", (unsigned)length, buffer);  // Die Eingabe passt nicht in den Puffer.
  else
    fprintf(stderr, "\nReadLineCopy - Fataler Fehler\n"); // Syntaxfehler, ungültiger Stream o.Ä.

/* ReadLineAlloc */
  printf("\n~~~~~~~~~~~~~~~~~~~~~~~~~~\nEingabe: ");
  if ((str = ReadLineAlloc(stdin, &length)) != NULL) // Benutzereingabe einholen und auf Verarbeitungsfehler prüfen.
  {

    printf("\nReadLineAlloc:\n Laenge: %u\n String: '%s'\n", (unsigned)length, str); // Anzeigen, was wir bekommen haben.

/* StringToLong */
    flags = StringToLong(str, 10, &num, &remaining); // Versuch die Benutzereingabe als Long Integer zu interpretieren.

    // Beispiel, wie man auf die einzelnen Flags zugreifen könnte.
    puts("\n~~~~~~~~~~~~~~~~~~~~~~~~~~\nStringToLong:\n Fehlerflags");
    for (; i < 4; ++i)
    {
      if ((flags >> i) & 1)
        fprintf(stderr, "  1 - %s\n", err_msg[i]);
      else
        printf("  0 - OK (%s)\n", ack_msg[i]);
    }

    if (flags & 2) // Ausgabe des Strings, an dem die Konvertierung abgebrochen wurde.
      printf(" Restlicher String:\n  '%s'\n", remaining); // Hier muss str noch gültig sein, da remaining auf einen Teilbereich des dafür allozierten Speichers zeigt.

    free(str); // Speicher (reserviert in ReadLineAlloc()) freigeben, da str nicht länger benötigt wird!

    // Beispiel, wie man den Rückgabewert weiterhin interpretieren kann.
    if (flags == OK)
    {
      printf(" Gesamter String konnte als Long Integer interpretiert werden:\n  %ld\n", num);
      return EXIT_SUCCESS;
    }
    else if (flags < ERR)
    {
      printf(" Long Integer wurde aus Teilstring extrahiert:\n  %ld\n", num);
      return EXIT_SUCCESS;
    }
    else
    {
      fprintf(stderr, " String/-anfang konnte nicht als Long Integer interpretiert werden.\n");
      return EXIT_FAILURE;
    }
  }

  fprintf(stderr, "ReadLineAlloc: Verarbeitungsfehler.\n");
  return EXIT_FAILURE;
}

Addendum:
Auch wenn ich oben etwas von Zeichenketten, Zeichenfolgen oder Strings geschrieben habe - einen Stringtyp kennt C leider nicht. asc hat zu diesem Zweck einen String Buffer entwickelt, der dabei hilft mit Zeichenketten zu arbeiten. Ich habe den Code bereits auch für Windows adaptiert und würde ihn bei Bedarf hinzufügen. // EDIT: asc hat ihn in Version 0.3 mit eingebracht.

BTW: Für Kritik und Ergänzungen bin ich natürlich offen ...
Autor
German
First release
Last update
Bewertung
5,00 Stern(e) 3 Bewertungen

Latest updates

  1. Code review.

    Ungarische Notation zugunsten lesbarer Variablennamen entfernt. Keine Reallokation für jedes...
  2. [C] Benutzereingaben oder Textdateien zeilenweise einlesen (v. 2)

    Antwort zu Q2 betr. fscanf Funktion konkretisiert.

Latest reviews

Sehr hilfreich
Damit sollte das Thema dann ja geklärt sein :)
Sehr schönes Tutorial. Der Frage-Antwort Stil gefällt mir sehr!
Oben