Follow the stream lifecycle
Plan the program as create or open, check, write, reposition or reopen for reading, read, inspect the result and close. The example uses tmpfile so the exercise does not overwrite a named document. For a persistent file, choose an explicit path and review the fopen mode first; opening an existing file with w can truncate it.
Make the first run reproducible
Save the example as file_io.c and compile with gcc -std=c11 -Wall -Wextra -Werror file_io.c -o file_io. Run ./file_io and compare the output below. The supplied records are intentionally shorter than the 64-byte buffer. Keep this first run unchanged so unexpected output can be separated from changes you introduce during practice.
Test an empty stream deliberately
Run ./file_io empty. The program creates the stream but skips writing records, so the documented result is chunks read: 0. This is a normal end-of-input case, not a read failure. Compare that outcome with tmpfile returning NULL or ferror reporting a read problem, both of which make the program return a failure status.
Understand bounded reading
fgets reads into the buffer within the supplied capacity and terminates a successfully read string. A newline that is read stays in the string. One successful call does not necessarily represent a whole logical line: a longer record may need several calls. The counter therefore says chunks rather than lines. Do not claim this small demonstration parses arbitrary CSV or binary files.
Check state changes and cleanup
fseek returns the update stream to its beginning before reading; its result is checked. The example also checks writing, reading and closing outcomes. A single cleanup label ensures that the stream is closed after an error once it has been created. The temporary file is removed when closed; this example is for practice, not persistent storage.
Extend the exercise in controlled steps
First change one record and confirm both content and chunk count. Next make a record longer than the buffer and predict how many fgets calls it needs, including any remaining newline. Then build a separate named-file version in a disposable folder, using r to read existing content or a to append. Never point an introductory w-mode exercise at files you need to keep.
Explain what the example does not test
The empty option is tested, but it does not simulate a storage failure. To investigate actual I/O failures, use an isolated test environment and describe the failing operation and return value. Do not assume a NULL read result always means normal end-of-file, and do not continue using a FILE pointer after fclose.
Complete file_io.c example
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
int empty = argc == 2 && strcmp(argv[1], "empty") == 0;
if (argc > 2 || (argc == 2 && !empty)) {
fputs("usage: file_io [empty]\n", stderr);
return 2;
}
FILE *stream = tmpfile();
if (stream == NULL) {
perror("tmpfile");
return 1;
}
int status = 0;
if (!empty && fputs("Java practice\nC file handling\n", stream) == EOF) {
perror("write");
status = 1;
goto cleanup;
}
if (fseek(stream, 0L, SEEK_SET) != 0) {
perror("seek");
status = 1;
goto cleanup;
}
char buffer[64];
unsigned int chunks = 0;
while (fgets(buffer, sizeof buffer, stream) != NULL) {
if (fputs(buffer, stdout) == EOF) {
status = 1;
goto cleanup;
}
++chunks;
}
if (ferror(stream)) {
fputs("read failed\n", stderr);
status = 1;
goto cleanup;
}
if (printf("chunks read: %u\n", chunks) < 0) {
status = 1;
}
cleanup:
if (fclose(stream) == EOF) {
perror("close");
status = 1;
}
return status;
}
Expected result
Normal run: Java practice C file handling chunks read: 2 Run with empty argument: chunks read: 0
Further reading
Check your progress
Keep your working files, test cases and notes together. Explain one design decision, demonstrate a failure case and list what you would improve next. These concrete outputs make it easier to identify the next skill to practice.