-3

I want to learn how to load multiple structures (many students: name, surname, index, address...) from a text file looking like:

Achilles, 9999
Hector, 9998
Menelaos, 9997
... and so on

Struct can be like:

struct student_t {
    char *name;
    int index;
}

My attempt (doesn't work; I'm not even sure if fgets+sscanf is a considerable option here):

int numStudents=3; //to simplify... I'd need a function to count num of lines, I imagine
int x, y=1000, err_code=1;

FILE *pfile = fopen("file.txt", "r");
if(pfile==0) {return 2;}

STUDENT* students = malloc(numStudents * sizeof *students);

char buffer[1024];
char *ptr[numStudents];
for (x = 0; x < numStudents; x++){ //loop for each student
    students[x].name=malloc(100); //allocation of each *name field 
    fgets(buffer, 100, pfile); //reads 1 line containing data of 1 student, to buffer
    if(x==0) *ptr[x] = strtok(buffer, ",");//cuts buffer into tokens: ptr[x] for *name
    else *ptr[x] = strtok(NULL, ","); //cuts next part of buffer
    sscanf(ptr[x], "%19s", students[x].name); //loads the token to struct field
    *ptr[y] = strtok(NULL, ","); //cuts next part of the buffer
    students[y].index = (int)strtol(ptr[y], NULL, 10); //loads int token to struct field
    *buffer='\0';//resets buffer to the beginning for the next line from x++ fgets...
    y++;//the idea with y=1000 is that I need another pointer to each struct field right?
}

for (x = 0; x < numStudents; x++)
    printf("first name: %s, index: %d\n",students[x].name, students[x].index);

return students;

Then printf it to see what was loaded. (to simplify my real structure that has 6 fields). I know a nice method to load 1 student from user input...(How to scanf commas, but with commas not assigned to a structure? C) however to load multiple, I have this idea but I'm not sure if it's too clumsy to work or just terrybly written.

Later I'd try to sort students by name , and perhaps even try to do a realloc buffer that increases it's size along with new students being loaded to buffer... and then to sort what'd been loaded... but I imagine that first I need to load it from the file to buffer and from buffer to fill structure, to be able to sort it then?...

Thanks A LOT for all the help!

trincot
  • 317,000
  • 35
  • 244
  • 286
Immo
  • 19
  • 6
  • Read documentation of [C I/O functions](https://en.cppreference.com/w/c/io). – Basile Starynkevitch Sep 05 '18 at 17:02
  • With `students[x].name=(char*)malloc(sizeof(char*)); ` you allocate ONE size of a pointer, which is not enough storage for the string. – Paul Ogilvie Sep 05 '18 at 17:02
  • In sscanf, `&students[x].name` is wrong and should just be `students[x].name` – Paul Ogilvie Sep 05 '18 at 17:03
  • `strtok` just chops `buffer` into tokens, it does not allocate memory. – Paul Ogilvie Sep 05 '18 at 17:04
  • How can I name what I want to do in other way, that would allow me to google it? Before I ask a question, I spend at least 20 hours on a problem. What I know how to do is: reading from stdin to struct fields, from bin file where I know length of fields, but it's all regarding 1 student. – Immo Sep 05 '18 at 17:20
  • There was no topic about reading multiple structures from file, so I asked this question. I thought that if someone does not want to answer, skips this question. Now when I'm downvoted to oblivion, I'd need to spend another 200 hours with this 1 simple task, but my holidays are over. – Immo Sep 05 '18 at 17:36
  • I don't know how to name it. But assuming, in main(): `struct student_t *p=malloc(100);`, how can I write `p[x].name` using `->`? Is it just `p[x]->name`? – Immo Sep 05 '18 at 17:38
  • It looks like your `printf()` call is already properly formatted and it should work correctly. You don't need to use the `->` operator in this case. The easiest way to do random access with a pointer is to use the array syntax, as you did. If you wanted to use pointer arithmetic syntax as an exercise, it would look like this: `printf("first name: %s, index: %d\n",(students+x)->name, (students+x)->index);` – Tim Randall Sep 05 '18 at 17:43
  • Try to write function `student_t * read_one_student(FILE * file)` which allocates memory for one student and reads it from file (you said that you know how to do this). Then create an array of pointers to students and fill it in a loop using this function. – magras Sep 05 '18 at 17:45
  • @magras array of pointers like: `struct student_t **ptr=malloc(number_of_students);`? (I mean, is it a double pointer here?) – Immo Sep 05 '18 at 17:50
  • I can't use fread function (which I used for a .bin file containing lengths) because I don't know the length of words in the file, is it correct? – Immo Sep 05 '18 at 17:55
  • You can use `fread` like: read one character, check if it's comma, if it isn't realloc the name +1, copy the character that you read, then fread one character again. If you encounter a comma, realloc the name +1, add a null delimeter and then proceed with scanning for index. – KamilCuk Sep 05 '18 at 18:00
  • @Immo almost `struct student_t **ptr=malloc(number_of_students * sizeof(void*));` Malloc accepts size in bytes. You need to store `number_of_students` pointers each of which has size `sizeof(void*)`. Or you can follow accepted solution and allocate array of students like `malloc(number_of_students * sizeof(student_t)` and pass pointer where new student should be written into function like that: `void read_one_student(student_t * write_pointer, FILE * file)`. – magras Sep 06 '18 at 12:31

1 Answers1

2

C is a little harsh. I use GNU getline below, which may be not portable, which you might end up implementing yourself. I use stdin for input FILE * just for simplicity.
The program reads the students list into the students array. Then I sort the students by comparing indexes, then by name, each time with printing out.
Your code is a bit of a mishmash - try to write a separate function for loading a single student, you don't need char ptr[students] just a single char *ptr for strtok function. strtok is a little mixy, I prefer using just strchr mutliple times. I used memcpy to just copy the name from the string and remember to null delimeter it.

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

struct student_s {
    char *name;
    int index;
};

static int students_name_cmp(const void *a, const void *b)
{
    const struct student_s *s1 = a;
    const struct student_s *s2 = b;
    return strcmp(s1->name, s2->name);
}

static int students_index_cmp(const void *a, const void *b)
{
    const struct student_s *s1 = a;
    const struct student_s *s2 = b;
    return s1->index - s2->index;
}

int main()
{
    struct student_s *students = NULL;
    size_t students_cnt = 0;
    FILE *fp = stdin;
    size_t read;
    char *line = NULL;
    size_t len = 0;

    // for each line
    while ((read = getline(&line, &len, fp)) != -1) {

        // resize students!
        students = realloc(students, (students_cnt + 1) * sizeof(*students));
        // handle erros            
        if (students == NULL) {
            fprintf(stderr, "ERROR allocating students!\n");
            exit(-1);
        }

        // find the comma in the line
        const const char * const commapos = strchr(line, ',');
        if (commapos == NULL) {
            fprintf(stderr, "ERROR file is badly formatted!\n");
            exit(-1);
        }
        // student has the neme between the start to the comma adding null delimeter
        const size_t namelen = (commapos - line) + 1;
        // alloc memory for the name and copy it and null delimeter it
        students[students_cnt].name = malloc(namelen * sizeof(char));
        // handle errors
        if (students[students_cnt].name == NULL) {
             fprintf(stderr, "ERROR allocating students name!\n");
             exit(-1);
        }
        memcpy(students[students_cnt].name, line, namelen - 1);
        students[students_cnt].name[namelen] = '\0';

        // convert the string after the comma to the number
        // strtol (sadly) discards whitespaces before it, but in this case it's lucky
        // we can start after the comma
        errno = 0;
        char *endptr;
        const long int tmp = strtol(&line[namelen], &endptr, 10);
        // handle strtol errors
        if (errno) {
            fprintf(stderr, "ERROR converting student index into number\n");
            exit(-1);
        }
        // handle out of range values, I use INT_MIN/MAX cause index is int, no better idea, depends on application
        if (tmp <= INT_MIN || INT_MAX <= tmp) {
            fprintf(stderr, "ERROR index number is out of allowed range\n");
            exit(-1);
        }
        students[students_cnt].index = tmp;

        // handle the case when the line consist of any more characters then a string and a number
        if (*endptr != '\n' && *endptr != '\0') {
            fprintf(stderr, "ERROR there are some rabbish characters after the index!");
            exit(-1);
        }

        // finnally, increment students count
        students_cnt++;
    }
    if (line) {
        free(line);
    }

    // sort by index
    qsort(students, students_cnt, sizeof(*students), students_index_cmp);

    // print students out sorted by index
    printf("Students sorted by index:\n");
    for (size_t i = 0; i < students_cnt; ++i) {
        printf("student[%zu] = '%s', %d\n", i, students[i].name, students[i].index);
    }

    // now we have students. We can sort them.
    qsort(students, students_cnt, sizeof(*students), students_name_cmp);

    // print students out sorted by name
    printf("Students sorted by name:\n");
    for (size_t i = 0; i < students_cnt; ++i) {
        printf("student[%zu] = '%s', %d\n", i, students[i].name, students[i].index);
    }

    // free students, lucky them!
    for (size_t i = 0; i < students_cnt; ++i) {
        free(students[i].name);
    }
    free(students);

    return 0;
}

For the following input on stdin:

Achilles, 9999
Hector, 9998
Menelaos, 9997

the program outputs:

Students sorted by index:
student[0] = 'Menelaos', 9997
student[1] = 'Hector', 9998
student[2] = 'Achilles', 9999
Students sorted by name:
student[0] = 'Achilles', 9999
student[1] = 'Hector', 9998
student[2] = 'Menelaos', 9997

A test version available here on onlinegdb.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • 1
    Couple of notes: (1) `realloc(students, (students_cnt + 1)..` reallocating by `1` each time is highly inefficient. Better to allocate an initial block of `students` (say `alloc=2, 4 or 8;`) and then when `student_cnt == alloc`, realloc `2 x alloc` and update `alloc *= 2;` (2) `if (commapos == NULL)` don't exit, just skip the line with `continue`. (3) Why `memcpy`? You can `strcpy (students[students_cnt].name, commapos + 1);` and then remove the `'\n'` (either way, it's up to you). (4) compare `&line[namelen] == endptr && tmp == 0` to fully validate the `strtol` conversion. (good effort `:)` – David C. Rankin Sep 05 '18 at 18:12
  • @David Thanks! (1) Good idea for optimization. But, nowadays, I stopped caring about malloc overhead. (2) Well, I decided not to skip the line as part of error handling (3) memcpy is faster, I know the string length, so no need to check if each char is zero (4) Thanks! I feel really lost as to when strtol returns errored, so I just check errno and hope for implementation that sets EINVAL ;) I feel like I should `(errno == ERANGE && (tmp == LONG_MAX || tmp == LONG_MIN) || (errno != 0 && tmp == 0) || endptr == &line[namelen]` to fully check for conversion errors ;) but hell, I just check errno! – KamilCuk Sep 05 '18 at 18:27
  • 1
    Just remember there are 2 validations for `strtol` (1) check whether any digits were converted. `strtol` will return `0` and `ptr == endptr` if no digits were found (and `errno` is NOT set). (2) checking `errno` covers all other cases (underflow/overflow, etc..). It's a toss up here whether parsing with `strchr` and `strtol` wouldn't be better as `sscanf`. Same result either way, but `sscanf` (to a fixed buf for `name`) and validating 2 conversions took place followed by a `malloc` & `strcpy` (or `strdup`) may be a bit shorter `:)` – David C. Rankin Sep 05 '18 at 19:15