File Handling

Everything so far has lived only in memory, vanishing the instant the program ends. File I/O — through FILE pointers and the fopen/fread/fwrite/fclose family — lets a C program read and write data that persists after it exits.

Every variable, array, and dynamically allocated block used up to this point has shared one limitation: the moment the program that created it exits, the memory holding it is handed back to the operating system and everything in it is gone. A file changes that. Data written to a file on disk survives past the run that wrote it, so a later run of the same program — or a completely different program — can pick that data back up. This page walks through the whole file-handling toolkit in <stdio.h> in one pass: the FILE type, opening and closing streams, every opening mode and exactly what each one does to existing content, text-mode formatted and character I/O, the file position indicator, binary I/O with fread/fwrite, buffering, the standard streams, and the handful of mistakes that trip up almost everyone the first time they touch file I/O.

Why File Handling Matters

An ordinary variable's lifetime ends with the program. int score = 95; is completely gone the instant main() returns — there is no way for a later run to ask the program what score was last time. Dynamically allocated memory from malloc() is no better off in this respect: it outlives a single function call, but not the process itself.

A file solves exactly this problem. If a program writes student records to a file named score.txt, that data sits on persistent storage — disk, flash, whatever the underlying device is — completely independent of whether the program that wrote it is still running. A later execution of the same program, run tomorrow or next year, can open score.txt and read the records straight back.

Nothing about ordinary variables or heap memory can bridge the gap between "Run 1" and "Run 2" above -- only something written to persistent storage can.

// Run 1 of the program
write_student_record("Rina", 95);   // stored in score.txt
// program exits -- "Rina", 95 no longer exist as variables

// Run 2 of the program, possibly days later
read_student_records();             // "Rina", 95 come back from score.txt

The <stdio.h> File Toolkit

Almost every file operation used in C comes from one header, #include <stdio.h>. It supplies the FILE type itself along with the whole family of functions this page covers: fopen() and fclose() to open and close a stream, fprintf()/fscanf() for formatted text, fgetc()/fputc() for single characters, fgets()/fputs() for lines and strings, fread()/fwrite() for raw binary data, fseek()/ftell()/rewind() to move around inside a file, feof()/ferror() to check a stream's status, fflush() to force buffered output out, and rename()/remove() to manage files on disk.

Every one of these functions operates on a stream — an abstraction <stdio.h> provides over whatever the underlying operating system actually does to read and write files. You will not need any OS-specific calls for the material on this page; <stdio.h> alone is enough.

The FILE Type

C represents an open file through an object of type FILE, almost always accessed via a pointer: FILE *file;. Think of a FILE * as a handle — a small ticket your program holds that lets it ask the library to read from or write to a particular open file, without your code ever needing to know how that file is actually represented on disk.

The actual internal layout of a FILE object is implementation-defined and deliberately hidden from you -- you only ever touch it through a FILE * and the standard library functions, never by reaching inside it yourself.

FILE *file;   // file will hold a handle to an open stream, once fopen() succeeds

Opening a File with fopen()

fopen() is how every file interaction begins. It takes a filename and a mode string describing what you intend to do with the file, and returns a FILE * you then pass to every other file function.

"data.txt" is the file to open; "r" is the mode string -- here, open for reading. The modes are covered in full in the next few sections.

FILE *fopen(const char *filename, const char *mode);

// Example
FILE *file = fopen("data.txt", "r");

Checking fopen() for Failure

Opening a file can fail for reasons entirely outside your program's control: the file doesn't exist, the process lacks permission, the path is invalid, the storage device is full or disconnected, or the process already has too many files open. When fopen() fails, it returns NULL rather than a usable handle.

Checking for NULL immediately after fopen() is not optional defensive programming -- it is the only way to know whether the handle you are about to use is actually valid.

Common mistake: skipping the NULL check and passing the result of fopen() straight into fprintf(), fgetc(), or any other file function. If fopen() failed, that pointer is NULL, and dereferencing it through those functions is undefined behavior -- typically a crash, but not guaranteed to fail loudly. Always check file == NULL before doing anything else with the handle.

It is also worth treating a filename or path itself as untrusted external input, exactly like data typed in by a user: a path that worked yesterday can fail today because a file moved, was deleted, or a permission changed, so the NULL check has to be a real part of your program's control flow, not an afterthought.

FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    printf("Could not open file.\n");
    return 1;
}

Closing a File with fclose()

Every stream successfully opened with fopen() should eventually be closed with fclose(file);. Closing releases the operating system resources tied up by the open file and, importantly, performs any output flushing still pending for that stream.

Common mistake: forgetting fclose(). Beyond leaking a file descriptor for as long as the program keeps running, a stream opened for output that is never closed can leave data you wrote sitting in an internal buffer and never actually reaching the file, because the flush that normally happens on close never occurs.

FILE *file = fopen("data.txt", "w");
if (file == NULL) {
    return 1;
}

/* ... use file ... */

fclose(file);

File Opening Modes at a Glance

The mode string passed to fopen() is what determines whether the file must already exist, whether existing content is kept or destroyed, and whether reading, writing, or both are allowed. The six core text modes are:

"r"    read an existing file (fails if it doesn't exist)
"w"    write -- create the file if needed, TRUNCATE it if it exists
"a"    append -- create the file if needed, write only at the end
"r+"   read and write an EXISTING file, content preserved
"w+"   read and write -- create if needed, TRUNCATE if it exists
"a+"   read and append -- create if needed, writes always go to the end

"r" -- Read an Existing File

Mode "r" opens a file strictly for reading. The file must already exist; if it does not, fopen() returns NULL rather than creating one. Existing contents are left completely untouched.

FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    printf("data.txt does not exist.\n");
    return 1;
}

"w" -- Write, and Why It Truncates

Mode "w" opens a file for writing. If the file does not exist, it is created. If it does exist, its existing contents are immediately discarded -- the file is truncated to zero length the moment fopen() succeeds, before your program has written a single byte.

The truncation happens as a side effect of the fopen() call itself, not because of anything written afterward -- opening in "w" mode and then deciding not to write anything still leaves the file empty.

Common mistake: opening a file with "w" when you actually meant to preserve and add to its existing content. This silently destroys everything the file previously held. If old data needs to survive, use "a" (append) or "r+" (read/write without truncation) instead of "w".

FILE *file = fopen("data.txt", "w");
// If data.txt already had content, it is gone now -- the file is empty
fprintf(file, "New content\n");
fclose(file);

"a" -- Append Without Erasing

Mode "a" opens a file for appending. It creates the file if it does not exist, exactly like "w" -- but if the file already exists, its contents are left alone and every write goes to the end of the file, after whatever was already there.

FILE *file = fopen("log.txt", "a");
if (file == NULL) {
    return 1;
}
fprintf(file, "New log entry\n");   // added after existing content, nothing erased
fclose(file);

The Combined Modes: "r+", "w+", "a+"

Adding a "+" allows both reading and writing on the same stream, and the base letter still controls what happens to existing content and whether the file must already exist.

"r+" is the mode to reach for when you need to both read and modify an existing file in place without losing what is already there -- "w+" still carries the same truncation risk as plain "w" and should be used with the same care.

"r+"  file must already exist; content preserved; both reading and writing allowed
"w+"  creates the file if needed; TRUNCATES an existing file; both reading and writing allowed
"a+"  creates the file if needed; reading allowed anywhere; all writes still go to the end

Text Mode vs Binary Mode

Every mode string can carry an extra "b" to open the stream in binary mode instead of text mode: "rb", "wb", "ab", "r+b", "w+b", "a+b" mirror the six text modes exactly, but tell the library not to perform any translation on the bytes read or written.

The distinction matters because on some systems -- Windows in particular -- text mode can perform implementation-defined translations, such as converting between a line-ending convention used on disk and the plain '\n' C code expects. Binary mode disables that translation and guarantees the exact bytes on disk are the exact bytes your program sees, byte for byte.

FILE *textFile   = fopen("data.txt", "r");    // text mode
FILE *binaryFile = fopen("data.bin", "rb");   // binary mode

Writing Formatted Text with fprintf()

fprintf() works exactly like printf(), except it takes a FILE * as its first argument telling it which stream to write to instead of always writing to the screen.

This writes exactly the two lines "Name: Rina" and "Score: 95" into data.txt, using the same format-specifier rules ( %s, %d, %f, and so on ) that printf() uses.

printf(...) is really just shorthand for writing to one particular, already-open stream: stdout. fprintf(file, ...) is the same formatted-output machinery pointed at a stream of your choosing instead.

FILE *file = fopen("data.txt", "w");
if (file == NULL) {
    return 1;
}

fprintf(file, "Name: %s\n", "Rina");
fprintf(file, "Score: %d\n", 95);

fclose(file);

Reading Formatted Data with fscanf()

fscanf() is fprintf()'s reading counterpart, and relates to fscanf(stdin, ...) the same way fprintf(file, ...) relates to printf() -- it reads formatted values from a file stream instead of the keyboard, using the same format-string rules as scanf().

fscanf() returns the number of input items it successfully assigned -- here that should be 2 (one for %d, one for %f). It can also return EOF if an input failure, including end-of-file, occurs before the first conversion even happens.

Always check fscanf()'s return value against the number of conversions you expected, exactly as above. A malformed line, an unexpected end of file, or a type mismatch all show up here first -- ignoring the return value means silently working with uninitialized or stale variables instead of noticing the read never actually happened.

int id;
float marks;

int result = fscanf(file, "%d %f", &id, &marks);
if (result == 2) {
    printf("Read id=%d marks=%.2f\n", id, marks);
} else {
    printf("Failed to read a complete record.\n");
}

Character I/O: fgetc() and fputc()

fgetc(file) reads a single character from a stream and returns it. fputc(ch, file) writes a single character to a stream. Together they are the building blocks every other text-reading function in <stdio.h> is ultimately built from.

This reads and echoes every character in the file, one at a time, stopping the instant fgetc() reports EOF.

Common mistake: declaring the character variable as char instead of int -- for example char ch; while ((ch = fgetc(file)) != EOF). fgetc() returns int, not char, specifically because its result has to represent every possible value an unsigned character can hold PLUS the separate, out-of-band value EOF. If ch is a plain (and on many platforms signed) char, a legitimate character value can collide with, or fail to compare equal to, EOF, breaking the loop's exit condition. Always store fgetc()'s result in an int, and only convert it to unsigned char afterward, once you've confirmed it isn't EOF.

int ch;
while ((ch = fgetc(file)) != EOF) {
    putchar(ch);
}
fputc('H', file);
fputc('i', file);
fputc('\n', file);
// writes exactly "Hi\n" to file

Reading Lines with fgets() and Writing Strings with fputs()

fgets() reads an entire line (or up to a size limit) into a buffer in one call, which makes it the natural tool for line-oriented text files. fputs() is its writing counterpart for a whole string.

fgets() reads at most sizeof(buffer) - 1 characters, always null-terminates whatever it stores, and keeps the newline character if one was encountered within that limit -- it does not strip it for you.

char buffer[100];
if (fgets(buffer, sizeof buffer, file) != NULL) {
    printf("%s", buffer);
}
fputs("Hello from C\n", file);

Text I/O Functions at a Glance

The six text-mode functions covered so far split cleanly into formatted vs. character-and-line-oriented, and reading vs. writing:

fprintf   formatted output      (like printf, to a stream)
fscanf    formatted input       (like scanf, from a stream)
fputc     write one character
fgetc     read one character
fputs     write a string
fgets     read a line / string

Reading a Whole File with the fgetc() Loop

Reading an entire text file, character by character, is a small, common pattern: open the file, loop on fgetc() until it reports EOF, and close it when done.

EOF itself is a negative integer constant defined by <stdio.h> that signals "no more input is available." It is a signal fgetc() (and friends) return to your program -- it is not a literal character sitting inside the file, so there is nothing to search the file's actual bytes for.

FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    return 1;
}

int ch;
while ((ch = fgetc(file)) != EOF) {
    putchar(ch);
}

fclose(file);

Why while (!feof(file)) Is a Bug

It is tempting to write the read loop the other way around, testing feof() up front:

feof() only becomes true AFTER an input operation has already attempted to read past the end of the file, not before. On the final real character, feof(file) is still false, the loop body runs one more time, fgetc() returns EOF, and that EOF value gets printed as if it were a character -- a spurious extra iteration on every single read.

Common mistake: using while (!feof(file)) as the primary condition for a read loop. It reliably produces one extra, bogus iteration at the end of the file. Prefer testing the read itself: while ((ch = fgetc(file)) != EOF) { putchar(ch); }. Reserve feof() for afterward, to distinguish a normal end-of-file from an actual read error once the loop has already exited.

// WRONG -- do not write file-reading loops this way
while (!feof(file)) {
    ch = fgetc(file);
    printf("%c", ch);
}

Detecting Errors with ferror()

A read loop that stops because fgetc() returned EOF could have stopped for either of two very different reasons: it genuinely reached the end of the file, or an actual input error occurred. ferror() is what tells them apart.

If the loop exited because of a genuine end-of-file, ferror() reports false here. If it exited because of an actual I/O error, ferror() reports true, letting you react differently than you would to an ordinary, successful end of the file.

int ch;
while ((ch = fgetc(file)) != EOF) {
    putchar(ch);
}

if (ferror(file)) {
    printf("File read error.\n");
}

The File Position Indicator

Every open stream keeps track of a file position indicator -- essentially, where the next read or write will happen. Reading or writing generally advances it automatically. ftell(), fseek(), and rewind() are the three functions that let a program inspect or directly control that position.

Finding the Current Position with ftell()

For a binary stream, the value ftell() returns is generally a genuinely useful byte offset from the start of the file. For a text stream, treat the returned value as an opaque position token suitable for handing back to fseek() later, rather than assuming it is a simple, human-meaningful byte count.

long position = ftell(file);
printf("Current position: %ld\n", position);

Jumping Around with fseek()

fseek() repositions the file position indicator to an offset measured from one of three origins.

int fseek(FILE *stream, long offset, int origin);

// origin is one of:
//   SEEK_SET -- measured from the beginning of the file
//   SEEK_CUR -- measured from the current position
//   SEEK_END -- measured from the end of the file

fseek(file, 0, SEEK_SET);   // move to the very beginning
fseek(file, 0, SEEK_END);   // move to the very end

Going Back to the Start with rewind()

rewind(file) is roughly equivalent to fseek(file, 0, SEEK_SET), with the added effect of clearing the stream's end-of-file and error indicators -- a convenient way to start reading a file over from the top.

rewind(file);

Binary File I/O with fread() and fwrite()

fread() and fwrite() move raw bytes between memory and a stream, with no text formatting or conversion involved at all -- exactly the in-memory representation of whatever object you hand them.

This writes the binary representation of the four ints directly -- not the human-readable text "10 20 30 40", but the exact bytes those ints occupy in memory.

count reports how many complete int-sized elements fread() actually managed to read -- it can be less than 4 if the file was shorter than expected or an error occurred partway through.

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
FILE *file = fopen("numbers.bin", "wb");
int numbers[] = {10, 20, 30, 40};

fwrite(numbers, sizeof numbers[0], 4, file);   // writes the raw bytes of 4 ints
fclose(file);
FILE *file = fopen("numbers.bin", "rb");
int numbers[4];

size_t count = fread(numbers, sizeof numbers[0], 4, file);
printf("Read %zu elements\n", count);
fclose(file);

Reading fread()'s Return Value Correctly

The return value of both fread() and fwrite() is the number of complete ELEMENTS successfully transferred, not the number of bytes.

Common mistake: treating fread()'s (or fwrite()'s) return value as a byte count. If size is 4 (an int) and count requested is 4, a full successful read returns 4 -- the element count -- not 16, the byte count. Comparing that return value against the wrong quantity makes a program think a perfectly successful read failed, or the reverse.

int numbers[4];
size_t count = fread(numbers, sizeof numbers[0], 4, file);

if (count != 4) {
    printf("Expected 4 elements, only got %zu.\n", count);
}

Binary Structs Are Not a Portable File Format

fwrite(&s, sizeof s, 1, file); and the matching fread(&s, sizeof s, 1, file); look like a convenient way to save and load an entire struct in one call, and they do work -- but only under conditions you have to be careful about.

This writes exactly the bytes struct Student occupies in memory, including any padding the compiler inserted between fields to satisfy alignment requirements.

Common mistake: assuming a raw struct dump like this one is a portable file format. It depends on struct padding and alignment, integer and floating-point representation, byte order (endianness), and even compiler/ABI details -- a file written this way by one program build is not guaranteed to be readable correctly by a different compiler, platform, or even a different build of the same program. It is appropriate mainly when the exact same reader and writer, built the same way, are the only programs involved. A genuinely portable format has to define its own explicit, documented byte layout instead of relying on however the compiler happens to lay the struct out.

struct Student {
    int roll_no;
    char name[50];
    float marks;
};

struct Student s = {1, "Rina", 95.5f};
FILE *file = fopen("student.dat", "wb");
fwrite(&s, sizeof s, 1, file);
fclose(file);

Never Serialize Pointers

A struct that contains a pointer is a special case of the portability problem above, and a much sharper one.

This does not persist a meaningful linked structure at all. What actually gets written for the next field is just a raw memory address -- a number that only meant "the node at this location" inside the exact process and address space that wrote it.

Common mistake: writing a struct containing a pointer field directly to a file and expecting to reconstruct the pointed-to relationship later. The address a pointer holds is meaningless once the writing process has exited or the file is opened by a different run -- that memory location is not guaranteed to hold anything related, or even to be valid at all. Persisting a linked structure means serializing the actual data and relationships (for example, an index or an ID that identifies the related record) in a format your own reading code reconstructs, never the pointer value itself.

struct Node {
    int data;
    struct Node *next;
};

struct Node n = {10, someOtherNode};
fwrite(&n, sizeof n, 1, file);   // writes n.next's raw address, not the node it points to

Buffering and fflush()

C streams are commonly buffered: calling fprintf(file, "Hello"); does not necessarily mean those bytes have actually reached the physical storage device the instant the call returns. The library is free to hold written data in an internal buffer and send it to the OS in larger batches, for efficiency.

fflush(file) asks the library to write out whatever output for that stream is currently sitting in its buffer. It is a request to flush the library's own buffer, not an absolute guarantee that every layer between your program and the physical disk has been bypassed. Calling fflush() on an input stream is also not generally portable C behavior and should be avoided.

fprintf(file, "Hello");
fflush(file);   // ask the library to push buffered output out now

fclose() Also Flushes

fclose(file); does more than just release the stream's resources -- as part of closing, it also handles any output still sitting in the stream's buffer, flushing it out. This is exactly why forgetting to call fclose() (mistake noted earlier) can leave written data never actually reaching the file at all, even though the fprintf() or fwrite() call that produced it appeared to succeed.

Standard Streams: stdin, stdout, stderr

<stdio.h> declares three streams that are already open before main() even starts: stdin (standard input, usually the keyboard), stdout (standard output, usually the screen), and stderr (standard error, also usually the screen, but a separate stream reserved for error messages).

scanf(...) and printf(...) are really just convenience wrappers around fscanf(stdin, ...) and fprintf(stdout, ...) -- fscanf() and fprintf() work identically whether the stream you hand them is one you opened yourself or one of these three that is always already open.

int value;
fscanf(stdin, "%d", &value);      // same as scanf("%d", &value)

fprintf(stdout, "Value: %d\n", value);  // same as printf("Value: %d\n", value)

fprintf(stderr, "Error occurred\n");    // dedicated error output

Checking fputs() and fclose() for Errors

Like fscanf(), the output functions fputs() and fclose() report failure through their return values, and it is worth checking them, especially when the data being written actually matters.

A failing fclose() is particularly worth catching -- since fclose() is also the point where buffered output actually gets flushed, an error there can mean data you thought you had already written successfully never made it to the file.

if (fputs("Hello\n", file) == EOF) {
    printf("Write failed.\n");
}

if (fclose(file) == EOF) {
    printf("Error closing file -- data may not have been saved.\n");
}

Renaming and Deleting Files

Two more <stdio.h> functions manage files on disk without ever opening them as streams: rename() to change a file's name, and remove() to delete it outright.

Both return 0 on success and a nonzero value on failure. Exactly what counts as failure (a destination that already exists for rename(), permissions, files currently open elsewhere, and so on) can depend on the host environment, so checking the return value matters here just as much as it does for fopen().

if (rename("old.txt", "new.txt") != 0) {
    printf("Rename failed.\n");
}

if (remove("data.txt") != 0) {
    printf("Could not delete file.\n");
}

Complete Worked Example: Student Records

Putting the text-mode pieces together: open a file for writing, write several records with fprintf(), close it while checking for errors, then reopen it for reading and print it back line by line with fgets().

fopen("students.txt", "w") creates (or truncates) the file and returns a handle, checked against NULL.

Three fprintf() calls write three lines of formatted text.

fclose() closes the write handle, and its own return value is checked for EOF.

The file is reopened with "r" for reading, again checking for NULL.

fgets() reads and prints one line at a time until it returns NULL at end of file.

ferror() distinguishes a normal end-of-file from a genuine read error before the final fclose().

#include <stdio.h>

int main(void) {
    FILE *file = fopen("students.txt", "w");
    if (file == NULL) {
        printf("Could not open file for writing.\n");
        return 1;
    }

    fprintf(file, "Name: Rina, Score: 95\n");
    fprintf(file, "Name: Aman, Score: 88\n");
    fprintf(file, "Name: Zoe, Score: 91\n");

    if (fclose(file) == EOF) {
        printf("Error closing file after writing.\n");
        return 1;
    }

    file = fopen("students.txt", "r");
    if (file == NULL) {
        printf("Could not open file for reading.\n");
        return 1;
    }

    char line[100];
    while (fgets(line, sizeof line, file) != NULL) {
        printf("%s", line);
    }

    if (ferror(file)) {
        printf("A read error occurred.\n");
    }

    fclose(file);
    return 0;
}

Text Files vs Binary Files

The two families of functions this page covers correspond to two different ways of thinking about a file's contents:

Text files
  - usually human-readable
  - read/written with fprintf, fscanf, fgets, fputs
  - easy to inspect in any text editor
  - does not preserve an object's exact in-memory representation
  - often easier to move between different programs/platforms as raw data
  - typical use: configuration files, data/log files

Binary files
  - usually not human-readable
  - read/written with fread, fwrite
  - hard to inspect directly
  - can preserve exact byte-for-byte representation
  - requires deliberate format design to be portable
  - typical use: images, custom application data formats, binary records

The File Handling Mental Model

It helps to picture a FILE * as one end of a stream interface sitting between your C program and a file that actually lives out in the operating system's storage.

The key distinction to hold onto: C's standard I/O library operates on streams -- an abstraction fopen() hands you -- while the operating system is what actually manages the underlying files and storage devices. Your program never has to deal with the storage device directly; the stream interface is the entire point of <stdio.h>.

C program
  +-----------------------------+
  | fread / fgets / fscanf / fgetc |----.
  |                               |    |  (input side)
  | fwrite / fputs / fprintf/fputc|----.
  +-----------------------------+    |
              |     ^                v
              v     |         +---------------+
         Stream (FILE *)  --> | Operating     |
                              | System        |
                              +---------------+
                                     |
                                     v
                              +---------------+
                              |  Storage      |
                              |  device/file  |
                              +---------------+

Quick Reference

Why does C need file handling at all, given that a program already has ordinary variables and dynamically allocated memory?

Ordinary variables and heap memory both disappear the moment the program that owns them exits -- neither can carry data from one run of a program to a later one. A file lives on persistent storage, so data written to it survives past the program's exit and can be read back by a later run, or a different program entirely.

Why should the result of fopen() always be checked before the returned pointer is used for anything else?

fopen() can fail for reasons outside the program's control -- a missing file, a permissions problem, an invalid path, a full or disconnected storage device, or too many already-open files -- and it signals that failure by returning NULL. Passing that NULL pointer into fprintf(), fgetc(), or any other file function without checking first is undefined behavior.

What actually happens differently between opening a file with "w" and opening it with "a", if the file already exists and already has content?

"w" immediately truncates the existing file to zero length the moment fopen() succeeds, discarding whatever was there before any new data is written. "a" leaves the existing content completely intact and positions every subsequent write at the end of the file, so new output is added after what was already there rather than replacing it.

Why must the variable holding fgetc()'s return value be declared as int rather than char?

fgetc() returns int specifically because its result has to be able to represent every possible unsigned-character value in addition to the distinct, out-of-band value EOF. A plain char (often signed) can't reliably hold every character value and still distinguish it from EOF, which can make a legitimate character compare equal to EOF or vice versa and break the loop's exit condition.

Why is while (!feof(file)) considered a bug as the main condition of a file-reading loop, and what should be used instead?

feof() only becomes true after a read has already been attempted and has already failed to find more input -- it is not a lookahead check. Using it as the loop condition means the loop body still runs one extra time after the last real character, processing a bogus EOF value as if it were data. The reliable pattern instead tests the read itself, as in while ((ch = fgetc(file)) != EOF) { ... }, and reserves feof() for afterward, to tell a normal end-of-file apart from a genuine error using ferror().

Write a C program that opens a file for writing, checks fopen()'s result for NULL, writes three student records with fprintf(), and then closes the file while checking fclose()'s return value for EOF.

The program opens "students.txt" with "w" (creating it, or truncating it if it already existed), checks the returned handle for NULL before using it, writes three formatted lines with fprintf(), and finally checks fclose()'s return value against EOF so a failure during the flush-on-close step is not silently ignored.

Write a C program that reopens a text file for reading, prints its contents line by line using fgets(), and checks ferror() after the read loop finishes.

fgets() reads one line at a time into a fixed-size buffer until it returns NULL, which happens both at a normal end of file and on an actual read error. Calling ferror() right after the loop is what tells those two cases apart, since fgets()'s own return value alone can't distinguish them.

Write a C program that counts the number of characters in a text file, using fgetc() with the return value stored in an int (not a char).

ch is declared as int, not char, so it can hold every possible character value returned by fgetc() as well as the distinct EOF value without any of them colliding, letting the while loop's comparison against EOF work correctly on every platform.

Write a C program that copies the entire contents of one text file to another using fgetc() and fputc().

Both files are opened and checked for NULL independently -- source for reading, destination for writing (which truncates it if it already existed). The copy loop reads one character at a time with fgetc() and immediately writes it back out with fputc(), stopping when fgetc() reports EOF.

Write a C program that appends a new line to an existing log file without erasing any of its previous content.

Opening with "a" creates the file if it does not already exist, but if it does exist, leaves all of its previous content untouched and forces every write, including this fputs() call, to happen at the end of the file rather than overwriting anything.

Write a C program that writes an array of integers to a binary file with fwrite() and then reads them back with fread(), printing how many elements were actually read.

fwrite() writes the raw in-memory bytes of the four ints to a binary-mode stream. fread() reads them back into a second array, and its return value -- the number of complete elements read, not bytes -- is printed directly to make clear that the count reflects elements successfully transferred.

Write a C program that defines a Student struct with roll_no, name, and marks fields, writes an array of them to a binary file with fwrite(), and reads them back with fread().

The whole array of struct Student values is written and read in a single fwrite()/fread() call each, transferring the exact in-memory bytes of both records including any compiler-inserted padding. This works reliably here because the same program, built the same way, both writes and reads the file -- exactly the condition under which a raw struct dump is safe to rely on.

Write a C program that uses fseek() and ftell() to determine the size, in bytes, of a binary file.

fseek(file, 0, SEEK_END) moves the file position indicator to the very end of the file, and ftell() immediately after reports that position as a byte offset from the start, which is exactly the file's size in bytes for a binary stream. rewind() then resets the position back to the beginning in case the file needs to be read afterward.

Write a C program that renames one file with rename() and deletes another file with remove(), checking and reporting the outcome of both operations.

Both rename() and remove() return 0 on success and a nonzero value on failure, so each call's result is checked and reported separately -- rename() can fail for reasons like a missing source file or an already-existing destination, and remove() can fail due to permissions or the file being open elsewhere.

Write a C program that reads an integer from a file with fscanf(), checking its return value, and reports success on stdout or failure on stderr.

fscanf()'s return value -- the number of successful conversions -- is checked against 1 before value is trusted. A successful read is reported through fprintf(stdout, ...), the same stream printf() uses, while a failed read is reported through fprintf(stderr, ...), the separate stream reserved for error messages.

fopen    open/create a stream
fclose   close a stream
fprintf  formatted file output
fscanf   formatted file input
fputc    write one character
fgetc    read one character
fputs    write a string
fgets    read a line
fwrite   write binary/object data
fread    read binary/object data
fseek    move the stream position
ftell    obtain the stream position
rewind   return to the beginning of a stream
feof     test the end-of-file indicator
ferror   test the error indicator
fflush   flush an output buffer
rename   rename a file
remove   remove a file