1

I have an assignment and I need to use scanf to read the input from a text file and temporarily store the inputs into a structure and finally print using printf. We aren't allowed to use an array of structures.

Here's the text file:

1010  Jose Manalo      3.5
2003  Luisa Santos     2.5
1008  Andres Espinosa  4.0

Here are the structures that were given (String20 is an array of 20 characters):

struct nameTag {
    String20 first;
    String20 last;
};

struct studentTag {
    int ID;
    struct nameTag name;
    float grade;
};

My initial code looked like this (studentInfo is my structure variable for studentTag):

scanf("%d %s %s %f", &studentInfo.ID, studentInfo.name.first, studentInfo.name.last, &studentInfo.grade);
printf("%d %s %s %.1f", studentInfo.ID, studentInfo.name.first, studentInfo.name.last, studentInfo.grade);

This of course only reads and prints the first line

Edit (Solved): Putting scanf into a loop until it reached EOF or NULL worked perfectly. Here's what I did:

while(scanf("%d %s %s %f", &studentInfo.ID, studentInfo.name.first, studentInfo.name.last, &studentInfo.grade)!=NULL)
    printf("%d %s %s %.1f\n", studentInfo.ID, studentInfo.name.first, studentInfo.name.last, studentInfo.grade);
  • Does this answer your question? [C read file line by line](https://stackoverflow.com/questions/3501338/c-read-file-line-by-line) – zubergu Mar 14 '20 at 14:47
  • 2
    You use `fgets()` to read the entire line into a buffer (sufficiently sized character array or allocated block of memory) and then use `sscanf()` to parse the needed information from the buffer **validating** the return of `sscanf` to ensure the number of conversions requested took place. – David C. Rankin Mar 14 '20 at 14:50
  • What you are looking for is **fscanf**.Alternatively you can read the whole file with **fgets**. – alex01011 Mar 14 '20 at 14:52
  • I was only allowed to use scanf strictly for the program but I see the problems and limitations of using scanf now. I'll use these answers for future references so I can make better programs, thank you all so much. – John Sarmiento Mar 14 '20 at 16:24

2 Answers2

3

When you are reading a line-at-a-time, use a line-oriented input function such as fgets() or POSIX getline(). This avoids the many pitfalls for new C programmers who attempt to read from the file with scanf() alone.

The problems with using scanf() alone are many, but boil down to what is left in the input stream after an attempted read. For example, when using scanf() to attempt to read an integer value, if a non-digit is encountered before a digit, a matching-failure occurs and character extraction from the input stream ceases at that point leaving all offending characters in the input stream unread -- just waiting to bite you again on your next attempted read unless you manually empty the characters from your input stream. After reading a float as the last value, the '\n' is left in the input-stream unread and if on your next read, your format specifier doesn't ignore leading whitespace, the whitespace will be taken as your input. (this is just the tip of the iceberg...)

So use fgets() to read an entire line-at-a-time into an appropriately sized character array. That way you are consuming an entire line at a time. You then parse what you need from your line of data using sscanf(). Regardless of the success/failure of the parse, you do not impact your read of the next line of data from the file. The read of data and parse of value are no longer coupled in a single operation.

Putting the pieces together, in your case you can do something similar to:

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

#define MAXC  1024
#define MAXNM   20

typedef char String20[MAXNM];   /* your typedef */

struct nameTag {
    String20 first;
    String20 last;
};

struct studentTag {
    int ID;
    struct nameTag name;
    float grade;
};

int main (int argc, char **argv) {

    char buf[MAXC];     /* buffer to hold each line */
    size_t n = 0;       /* counter for number of students */
    struct studentTag std[MAXNM] = {{ .ID = 0 }};
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }

    /* read each line -- until array full of EOF reached */
    while (n < MAXNM && fgets (buf, MAXC, fp)) {
        struct studentTag tmp = { .ID = 0 };    /* temporary struct to fill */
        if (sscanf (buf, "%d %19s %19s %f",     /* parse value from line */
                    &tmp.ID, tmp.name.first, tmp.name.last, &tmp.grade) == 4)
            std[n++] = tmp; /* assign temp struct to array on success */
        else    /* on error */
            fprintf (stderr, "error: line format - %s", buf);
    }
    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);

    for (size_t i = 0; i < n; i++)  /* output all stored student data */
        printf ("\nID: %d\nFirst: %s\nLast: %s\nGrade: %f\n",
                std[i].ID, std[i].name.first, std[i].name.last, std[i].grade);

    return 0;
}

(note: you always validate every user-input and every parse of values by checking the return, see Henry Spencer's 10 Commandments for C Programmers - No. 6 "Ye be warned...")

Example Use/Output

With your data in dat/studenttag.txt, you would provide the filename as the first argument and receive:

$ ./bin/readstudenttag dat/studenttag.txt

ID: 1010
First: Jose
Last: Manalo
Grade: 3.500000

ID: 2003
First: Luisa
Last: Santos
Grade: 2.500000

ID: 1008
First: Andres
Last: Espinosa
Grade: 4.000000

Look things over and let me know if you have further questions.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
1

scanf is used to read input streams, from stdin, that is, the most aproximate function you can use is fscanf, similar to scanf, but you can specify the file from which to read:

#include <stdio.h>

typedef char String20[20];

struct nameTag {
    String20 first;
    String20 last;
};

struct studentTag {
    int ID;
    struct nameTag name;
    float grade;
};

int main()
{
   FILE* f;
   if(!(f = fopen("file.txt", "r"))) {
       return 1;
   }

   struct studentTag studentInfo; 

   while(fscanf(f, "%d %19s %19s %f", &studentInfo.ID, studentInfo.name.first, studentInfo.name.last, &studentInfo.grade) == 4)
        printf("%d %s %s %.1f\n", studentInfo.ID, studentInfo.name.first, studentInfo.name.last, studentInfo.grade);
}

Live sample

Using the return of fscanf to verify if the correct number of fields was read and using it as stop argument.

Note that for char array reading it's important to specify the length of the string to be read, for instance, for a char[20] character array container you can use "%s19" specifier, to avoid stack smashing.

anastaciu
  • 23,467
  • 7
  • 28
  • 53