How to deal with it?
There is a lot packed into that question. Obviously, the simple answer is just to choose one location where you will allocate for each new student added to your collection, but I fear that answer would fall well short of providing you with much help at all.
So let's fix things -- starting from the beginning. I will move fairly quickly from a description standpoint as the code is commented reasonably well.
First glaring problem, 15 characters is not nearly enough characters to handle all reasonably anticipated names. Double that at least (but be mindful your struct will contains static arrays and each will require storage for the full size of each array) A reasonable start, eliminating your Magic Numbers and hardcoded filesnames could be:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define MAXGRP 16 /* if you need a constant, #define one (or more) */
#define MAXNM 32 /* 15 is inadequate for many names */
#define MAXC 1024
typedef struct student { /* typedef for convenience */
char name[MAXNM];
char lastname[MAXNM];
int course;
char group[MAXGRP];
} student;
Next since you will be reading character strings and integers for every student entered, go ahead a write a function to read and validate string and integer input. You can provide an optional prompt to be output before each input (or provide NULL in its place and no prompt is output). To read character strings, or integer input read a complete line of user-input with fgets(). So what remains in stdin doesn't impact subsequent inputs.
Use a temporary buffer (array) to hold the user input (don't skimp on buffer size) and then validate the string will fit in your struct before copying the string:
char *getstring (char *str, size_t maxlen, char *prompt)
{
char buf[MAXC]; /* buffer to read each user-input */
size_t len; /* length of string read */
for (;;) { /* loop continually */
if (prompt) /* display prompt if not NULL */
fputs (prompt, stdout);
if (!fgets (buf, MAXC, stdin)) { /* get string, handle manual EOF */
puts ("(input canceled)");
return NULL;
}
buf[(len = strcspn(buf,"\n"))] = 0; /* trim \n, save length */
if (len < maxlen) /* validate string fits */
break;
fputs ("error: input length exceeds storage.\n", stderr);
}
return memcpy (str, buf, len + 1); /* copy buf to str, return ptr to str */
}
For integer input, do the exact same thing except this time, attempt a conversion to int using sscanf() and in the event of a matching failure, since you took the input using fgets(), there is no need to clear to end of line to remove the offending characters before your next attempted read, e.g.
int getint (int *val, char *prompt)
{
char buf[MAXC]; /* buffer to read each user-input */
for (;;) { /* loop continually */
if (prompt) /* display prompt if not NULL */
fputs (prompt, stdout);
if (!fgets (buf, MAXC, stdin)) { /* get string, handle manual EOF */
puts ("(input canceled)");
return 0;
}
if (sscanf (buf, "%d", val) == 1) /* convert to int / validate */
break;
fputs ("error: invalid integer input.\n", stderr);
}
return 1; /* return success */
}
Now let's change your getstudent() function to only be responsible from filling a struct student and move all memory allocation to an addstudent() function later that will only allocate memory if student data has been successfully stored in a temporary stuct. Your getstudent() read would take a pointer to the temporary stuct to fill and return a pointer to that stuct on success or NULL on failure, e.g.:
student *getstudent (student *s)
{
putchar ('\n'); /* provide empty line before each set of input */
if (!getstring (s->name, MAXNM, "Enter name: ")) /* read/validate first */
return NULL;
if (!getstring (s->lastname, MAXNM, "Enter lastname: ")) /* read/validate last */
return NULL;
if (!getint (&s->course, "Enter course: ")) /* read/validate course */
return NULL;
if (!getstring (s->group, MAXGRP, "Enter group: ")) /* read/validate group */
return NULL;
return s; /* return pointer to filled struct */
}
(note: all of your functions that can either succeed or fail should return a type that can be used to validate success or failure of the function)
The logic is kept much clearner by offloading all the validation code to your getstring() and getint() functions.
Now an addstudent() function simply needs to call getstudent() and if that succeeds, than allocate (realloc()) memory and store the new student in that new memory in your collection. It will also update the total number of students in your collection by taking a pointer to the total number and then incrementing the value at that address, e.g.
student *addstudent (student **students, size_t *nstudents)
{
student s = {.name = ""}; /* declare temporary struct */
if (!getstudent (&s)) /* fill/validate temporary struct */
return NULL;
/* allocate / validate memory to hold struct */
void *tmp = realloc (*students, (*nstudents + 1) * sizeof **students);
if (!tmp) {
perror ("addstudent-tmp");
return NULL;
}
*students = tmp; /* assign reallocated block to students */
(*students)[*nstudents] = s; /* fill new mem with temp struct */
(*nstudents)++; /* increment student count */
return *students; /* return pointer to students */
}
Your showdata function can be greatly simplified to simply iterate over each struct in your collection, outputting data in your chosen format, e.g.:
void showdata (student *s, size_t nstudents)
{
if (!nstudents) { /* if no students, so indicate */
puts ("no student data.");
return;
}
puts ("\nname\tlastname\tcourse\tgroup"); /* output heading */
for (size_t i = 0; i < nstudents; i++) /* loop outputting each student */
printf ("%s\t%-8s\t%d\t%s\n", s[i].name, s[i].lastname, s[i].course, s[i].group);
}
Now to load students from a file into your collection, you will use an approach similar to addstudent() you will read from the file into a temporary struct and then only on a successful read do you allocate storage from that struct and add it to your collection.
NOTE this is not in practice how you write and read struct data to/from a file. Why? The compiler is free to insert padding bytes in between or after any of the members of your struct (but not before the first) and there is nothing in the C standard that mandates where and how much padding a compiler will add. That means that writing a series of stuct to a binary file is completely non-portable between compilers (but for learning purposes it should work fine on the same computer -- the same compiler should at least be consistent in what it does) In practice you will serialize the data before writing it out in binary, so you can read the same data back in in a portable manner.
At this point, just be away you can't write arrays of structs to a binary file and have that file read back in by another compiler due to potential differences in padding.
With that said, on your same computer, you can read the collection of struct student into memory with the following:
student *loadstudents (int fd, student **students, size_t *nstudents)
{
for (;;) { /* loop continually reading students from file */
student s; /* temporary struct */
ssize_t nbytes; /* no. of bytes read */
errno = 0; /* reset errno */
nbytes = read (fd, &s, sizeof **students); /* read student into temp struct */
if (nbytes == -1) { /* validate no error */
perror ("loadstudents-read");
return NULL;
}
if (nbytes != sizeof **students) { /* validate no. of bytes read */
if (nbytes != 0)
fputs ("error: less than student no. of bytes read.\n", stderr);
break;
}
/* allocate / validate memory to hold struct */
void *tmp = realloc (*students, (*nstudents + 1) * sizeof **students);
if (!tmp) {
perror ("loadstudents-tmp");
return NULL;
}
*students = tmp; /* assign reallocated block to students */
(*students)[*nstudents] = s; /* fill new mem with temp struct */
(*nstudents)++; /* increment student count */
}
return *students; /* return pointer to students */
}
Writing the collection of struct student to the file in binary in a non-poratable way is trivial. You know how many students you have, and you know how many bytes per-struct your compiler provides for each struct, so you just write number * sizeof (struct student) bytes to the file, e.g.
int writestudents (int fd, student *students, size_t nstudents)
{
ssize_t nbytes = sizeof *students * nstudents; /* no. of bytes to write */
return write (fd, students, nbytes) == nbytes; /* write/validate bytes written */
}
Those are your functions and their uses in a nutshell. Now just write a main() function that will read what is already present in your datafile (which you provide the name of as an argument to main() and do NOT hardcode it -- that's what int argc, char **argv are for). You add any student in the file to your collection saving the number of students loaded from file so you don't need to worry about writing them out again since you have your file opened O_RDWR. (it would be simpler to open it O_RDONLY, read all the students into your collection in memory, close the file and when you are ready to write the file, open it O_WRONLY and write all the students out)
After loading all the students from your file, you call addstudent() and add any additional students you like (or none at all if you generate a manual EOF at a data entry prompt)
To write your collection out at the end (instead of writing each new struct out piecemeal) you simply write all new structs you have added at the end and then close the file (checking the return of close() to catch any flush or stream errors not present when the last struct was written) and then free your data and your done.
You can do that many, many ways, but one approach is:
int main (int argc, char **argv) {
char *file = NULL, buf[MAXC]; /* pointer to filename & temp buffer (y/n) */
int fd; /* file descriptor */
size_t nstudents = 0, loaded = 0; /* total students, no. loaded from file */
student *students = NULL; /* pointer to students */
mode_t mode = S_IRWXU | S_IRGRP | S_IROTH; /* open mode */
if (argc < 2) { /* validate one argument given for filename */
fprintf (stderr, "error: insufficient arguments provided\n"
"usage: %s filename\n", argv[0]);
return 1;
}
file = argv[1]; /* assign filename to pointer */
fd = open (file, O_CREAT | O_APPEND | O_RDWR, mode); /* open file */
if (fd == -1) {
perror ("open-file");
return 1;
}
loadstudents (fd, &students, &loaded);
if (errno)
return 1;
nstudents = loaded; /* assign no. of students loaded (saving loaded) */
if (nstudents) /* if students read from file, output number */
printf ("%zu students read from file: %s\n", nstudents, file);
do { /* loop adding students to collection */
if (!addstudent (&students, &nstudents))
break;
fputs ("\nenter another student (y/n)? ", stdout); /* prompt to enter more? */
if (!fgets (buf, sizeof buf, stdin))
break;
} while (*buf != 'n' && *buf != 'N'); /* while 1st char in buf now 'n/N' */
showdata (students, nstudents); /* output all in students */
if (nstudents - loaded) { /* if new students added since file loaded */
/* write new students added to file */
if (writestudents (fd, students + loaded, nstudents - loaded))
printf ("\n%zu students written to file: %s\n", nstudents, file);
else
perror ("writestudents");
}
if (close (fd) == -1) /* always validate close after write */
perror ("close-file");
if (nstudents) /* free allocated memory */
free (students);
}
That's it, you can just copy/paste all the pieces above together, compile and test. If you look at the code, most of the additions to the code are validating the return of every user-input function and every allocation. That is mandatory. Type an "a100" for the course in your code and see what happens...
You validate every allocation because it isn't a matter of if an allocation can fail, it is a matter of when an allocation fails. That is why I originally said simply moving all your allocations to a single location would likely not provide a meaningful answer for all of the problems that needed to be addressed in your code.
Example Use/Output
Starting wth no data file present, let's add two students and write them to your file:
$ ./bin/student_input dat/student_input.bin
Enter name: John
Enter lastname: Doe
Enter course: 100
Enter group: 1000
enter another student (y/n)? y
Enter name: Mickey
Enter lastname: Mouse
Enter course: 101
Enter group: 1002
enter another student (y/n)? n
name lastname course group
John Doe 100 1000
Mickey Mouse 101 1002
2 students written to file: dat/student_input.bin
Now verify that they are written to your binary file:
$ hexdump -Cv dat/student_input.bin
00000000 4a 6f 68 6e 00 00 00 00 00 00 00 00 00 00 00 00 |John............|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 44 6f 65 00 00 00 00 00 00 00 00 00 00 00 00 00 |Doe.............|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000040 64 00 00 00 31 30 30 30 00 00 00 00 00 00 00 00 |d...1000........|
00000050 00 00 00 00 4d 69 63 6b 65 79 00 00 00 00 00 00 |....Mickey......|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000070 00 00 00 00 4d 6f 75 73 65 00 00 00 00 00 00 00 |....Mouse.......|
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000090 00 00 00 00 65 00 00 00 31 30 30 32 00 00 00 00 |....e...1002....|
000000a0 00 00 00 00 00 00 00 00 |........|
000000a8
Looks good, now let's run the program again and add another student:
$ ./bin/student_input dat/student_input.bin
2 students read from file: dat/student_input.bin
Enter name: Minnie
Enter lastname: Mouse
Enter course: 101
Enter group: 1004
enter another student (y/n)? n
name lastname course group
John Doe 100 1000
Mickey Mouse 101 1002
Minnie Mouse 101 1004
3 students written to file: dat/student_input.bin
Now verify your file contains all three:
$ hexdump -Cv dat/student_input.bin
00000000 4a 6f 68 6e 00 00 00 00 00 00 00 00 00 00 00 00 |John............|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 44 6f 65 00 00 00 00 00 00 00 00 00 00 00 00 00 |Doe.............|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000040 64 00 00 00 31 30 30 30 00 00 00 00 00 00 00 00 |d...1000........|
00000050 00 00 00 00 4d 69 63 6b 65 79 00 00 00 00 00 00 |....Mickey......|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000070 00 00 00 00 4d 6f 75 73 65 00 00 00 00 00 00 00 |....Mouse.......|
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000090 00 00 00 00 65 00 00 00 31 30 30 32 00 00 00 00 |....e...1002....|
000000a0 00 00 00 00 00 00 00 00 4d 69 6e 6e 69 65 00 00 |........Minnie..|
000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000000c0 00 00 00 00 00 00 00 00 4d 6f 75 73 65 00 00 00 |........Mouse...|
000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000000e0 00 00 00 00 00 00 00 00 65 00 00 00 31 30 30 34 |........e...1004|
000000f0 00 00 00 00 00 00 00 00 00 00 00 00 |............|
000000fc
All there.
Granted this ended up longer than originally planned, but there really isn't any way to provide a 1/2 answer and not leave you with just as many problems as you came with (sans one or two). Big takeaways - take all user-input using fgets() (or POSIX getline()) so what remains in stdin is not conditioned on a scanf() conversion, or a matching failure, always validate, validate, validate, consider using the stdio file-stream functions instead of the low-level syscalls (read, write, etc..) and don't try and cram all data entry and validation and allocation and counting in a single function -- break it up into smaller, more easily maintained functions -- it will help keep your logic clear.
Look it over, digest it, and let me know if you have further questions.
sp = (struct student *)malloc(sizeof(struct student));followed bysp = input_student();is a memory-leak because the pointer to the 1st allocated block is overwritten by the return frominput_student();struct sp studentand to use the structure.fprintfto write text formatted data onto the file.fopenfunction.