C File I/O with stdio.h

C provides file input and output facilities through the standard I/O library declared in stdio.h. These functions allow programs to create, open, read, write, append, seek, and close files.

Including stdio.h

C
#include <stdio.h>

The FILE Type

The standard I/O library represents an open stream with the FILE type. Programs normally use a FILE pointer to interact with an opened file.

C
FILE *file;

Opening a File with fopen

The fopen function opens a file and returns a pointer to a FILE object when successful. If the operation fails, it returns NULL.

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

if (file == NULL) {
    perror("fopen");
    return 1;
}

Closing a File

An opened stream should be closed with fclose when it is no longer needed.

C
if (fclose(file) != 0) {
    perror("fclose");
}

File Opening Modes

ModePurpose
rOpen an existing file for reading
wOpen for writing; create or truncate the file
aOpen for writing at the end; create if necessary
r+Open an existing file for reading and writing
w+Open for reading and writing; create or truncate
a+Open for reading and writing with writes at the end

Binary Mode

The b character can be added to a mode to request binary mode.

C
FILE *file = fopen("image.dat", "rb");

Binary mode is particularly relevant on systems where text and binary streams have different handling.

Reading One Character

fgetc reads one character from a stream and returns it as an int. EOF is used to indicate end-of-file or an input error.

C
int ch;

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

Why fgetc Returns int

The return type must be able to represent every possible unsigned char value as well as EOF. Therefore, storing fgetc's result directly in char can make EOF detection incorrect.

Writing One Character

C
if (fputc('A', file) == EOF) {
    perror("fputc");
}

Reading a Line with fgets

fgets reads at most one fewer than the specified number of characters and terminates the resulting string with a null character when it successfully reads characters.

C
char buffer[256];

while (fgets(buffer, sizeof buffer, file) != NULL) {
    printf("%s", buffer);
}

Writing a String with fputs

C
if (fputs("Hello, file!\n", file) == EOF) {
    perror("fputs");
}

Formatted Output with fprintf

C
int id = 42;
const char *name = "Alice";
printf(file, "%d %s\n", id, name);

Formatted Input with fscanf

C
int id;
char name[50];

if (fscanf(file, "%d %49s", &id, name) == 2) {
    printf("%d %s\n", id, name);
}

Why fscanf Can Be Tricky

Formatted input is convenient but whitespace handling and malformed input can make fscanf difficult to use for complex formats. For line-oriented input, fgets followed by explicit parsing is often easier to control.

Reading Binary Data with fread

fread reads a specified number of objects from a stream into memory.

C
int values[10];

size_t count = fread(values, sizeof values[0], 10, file);

printf("Read %zu elements\n", count);

Writing Binary Data with fwrite

C
int values[] = {10, 20, 30, 40};

size_t count = fwrite(values, sizeof values[0], 4, file);

if (count != 4) {
    perror("fwrite");
}

Checking fread Results

A short fread result does not by itself tell you whether end-of-file or an input error occurred. Use feof and ferror after the operation.

C
size_t count = fread(buffer, 1, sizeof buffer, file);

if (count < sizeof buffer) {
    if (feof(file)) {
        printf("Reached end of file\n");
    }

    if (ferror(file)) {
        perror("fread");
    }
}

Checking End-of-File

feof reports whether the end-of-file indicator for a stream is set. It should generally be checked after a read operation rather than used as the loop condition before reading.

C
int ch;

while ((ch = fgetc(file)) != EOF) {
    /* process ch */
}

if (ferror(file)) {
    perror("read error");
}

Checking Stream Errors

ferror tests whether the error indicator for a stream is set.

C
if (ferror(file)) {
    perror("file I/O error");
}

Clearing Error and EOF Indicators

clearerr clears both the error and end-of-file indicators associated with a stream.

C
clearerr(file);

File Position

Each stream maintains a file position that determines where the next read or write occurs.

ftell

ftell reports the current file position for a stream using type long.

C
long position = ftell(file);

if (position == -1L) {
    perror("ftell");
}

fseek

fseek changes the file position according to an offset and a reference point.

C
if (fseek(file, 0L, SEEK_SET) != 0) {
    perror("fseek");
}

SEEK_SET, SEEK_CUR, and SEEK_END

ConstantReference
SEEK_SETBeginning of the file
SEEK_CURCurrent file position
SEEK_ENDEnd of the file

Rewinding a Stream

rewind moves the file position to the beginning and clears the stream's error and EOF indicators.

C
rewind(file);

Random Access

fseek allows programs to move around a file instead of processing it strictly from beginning to end.

C
if (fseek(file, 100L, SEEK_SET) == 0) {
    int ch = fgetc(file);
    if (ch != EOF) {
        printf("Character: %c\n", ch);
    }
}

Append Mode

Opening a stream with mode a causes writes to occur at the end of the file.

C
FILE *file = fopen("log.txt", "a");

if (file != NULL) {
    fprintf(file, "New log entry\n");
    fclose(file);
}

Truncating a File

Opening a file with w mode creates the file if necessary and truncates an existing file to zero length.

C
FILE *file = fopen("output.txt", "w");

Read-and-Write Modes

Modes containing + permit both reading and writing. When switching between reading and writing on an update stream, the program must follow the positioning and sequencing rules required by the C standard.

Text Files

Text streams are intended for textual data and may have implementation-specific translation behavior. Programs should use the standard stream functions rather than assuming a particular platform's line-ending representation.

Binary Files

Binary streams provide access to data without text-mode translations that an implementation may perform.

C
FILE *file = fopen("data.bin", "rb");

Writing Structures to Files

A structure can sometimes be written with fwrite for application-controlled binary files, but raw structure representations may contain padding and are not necessarily portable across different compilers, architectures, or program versions.

C
struct Record {
    int id;
    double value;
};

struct Record record = {1, 42.5};

fwrite(&record, sizeof record, 1, file);

For portable file formats, explicitly serialize individual fields using a defined representation.

File Serialization

Serialization converts program data into a defined external representation. A robust format should specify field sizes, byte order, encoding, versioning, and error behavior when portability matters.

Buffering

The standard I/O library commonly buffers stream operations. Buffering can improve performance by reducing the number of lower-level I/O operations.

fflush

fflush writes buffered output data for an output stream to the associated file or device.

C
fprintf(file, "Important data\n");

if (fflush(file) != 0) {
    perror("fflush");
}

For input streams, the meaning and permitted behavior of fflush differ; do not treat fflush as a general input-buffer clearing function.

setvbuf

setvbuf can be used to control buffering for a stream, subject to the requirements of the C standard.

C
setvbuf(file, NULL, _IOFBF, BUFSIZ);

Standard Streams

C provides three predefined standard streams.

StreamTypical Purpose
stdinStandard input
stdoutStandard output
stderrStandard error output

Writing to stderr

C
fprintf(stderr, "An error occurred\n");

Redirecting Standard Output

The shell can redirect stdout to a file without requiring the C program to open that file itself.

TEXT
./program > output.txt

Checking fopen Errors

C
FILE *file = fopen("config.txt", "r");

if (file == NULL) {
    perror("config.txt");
    return EXIT_FAILURE;
}

Using strerror with errno

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

FILE *file = fopen("missing.txt", "r");

if (file == NULL) {
    fprintf(stderr, "Open failed: %s\n", strerror(errno));
}

File Removal

The remove function requests that a file be removed.

C
if (remove("temporary.txt") != 0) {
    perror("remove");
}

Renaming a File

C
if (rename("old.txt", "new.txt") != 0) {
    perror("rename");
}

Temporary Files

The standard library provides temporary-file facilities such as tmpfile. Platform-specific secure temporary-file creation should be used when stronger security properties are required.

C
FILE *file = tmpfile();

if (file == NULL) {
    perror("tmpfile");
}

Opening a File Safely

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *file = fopen("data.txt", "r");

    if (file == NULL) {
        perror("data.txt");
        return EXIT_FAILURE;
    }

    char buffer[256];

    while (fgets(buffer, sizeof buffer, file) != NULL) {
        fputs(buffer, stdout);
    }

    if (ferror(file)) {
        perror("read");
        fclose(file);
        return EXIT_FAILURE;
    }

    if (fclose(file) != 0) {
        perror("fclose");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Writing a Text File

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *file = fopen("output.txt", "w");

    if (file == NULL) {
        perror("output.txt");
        return EXIT_FAILURE;
    }

    if (fprintf(file, "Hello from C\n") < 0) {
        perror("fprintf");
        fclose(file);
        return EXIT_FAILURE;
    }

    if (fclose(file) != 0) {
        perror("fclose");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Reading an Entire Binary Block

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *file = fopen("data.bin", "rb");

    if (file == NULL) {
        perror("data.bin");
        return EXIT_FAILURE;
    }

    unsigned char buffer[1024];
    size_t bytes;

    while ((bytes = fread(buffer, 1, sizeof buffer, file)) > 0) {
        /* process bytes */
    }

    if (ferror(file)) {
        perror("fread");
        fclose(file);
        return EXIT_FAILURE;
    }

    fclose(file);
    return EXIT_SUCCESS;
}

Determining File Size

For streams where seeking is supported, a common approach is to save the current position, seek to the end, obtain the position, and then restore the original position.

C
long current = ftell(file);

if (current == -1L) {
    perror("ftell");
}

if (fseek(file, 0L, SEEK_END) != 0) {
    perror("fseek");
}

long size = ftell(file);

if (size == -1L) {
    perror("ftell");
}

if (fseek(file, current, SEEK_SET) != 0) {
    perror("fseek");
}

This pattern is not universally suitable for every stream, and for large files or platform-specific requirements, APIs such as fseeko and ftello may be preferable where available.

Line Ending Considerations

Programs should avoid assuming that every platform represents a text newline using the same byte sequence. Standard text-stream functions abstract much of this difference.

File Position and Binary Data

For binary streams, fseek and ftell provide positioning semantics defined by the C standard, but portable code should not assume that arbitrary byte offsets have the same meaning for every kind of text stream.

Mixing Input Functions

Mixing functions such as fscanf, fgets, and fgetc requires understanding how each function consumes input. For example, fscanf may leave a newline in the stream after reading a numeric value.

C
int age;
char name[100];

fscanf(file, "%d", &age);
fgets(name, sizeof name, file);

The fgets call may immediately read the remaining newline. A line-oriented input strategy often avoids this class of problem.

A Line-Oriented Parsing Pattern

C
char line[256];

while (fgets(line, sizeof line, file) != NULL) {
    /* parse line explicitly */
}

Handling Long Lines

If a line can exceed the buffer size, a single fgets call may not consume the entire line. Programs should detect incomplete lines and continue reading or use a dynamically growing buffer.

File Ownership

The code that successfully opens a FILE stream should have a clear responsibility for closing it. This makes resource management easier to reason about.

Common File I/O Pitfalls

  • Failing to check whether fopen returned NULL
  • Forgetting to fclose an opened stream
  • Using the wrong file mode
  • Accidentally truncating a file with w mode
  • Assuming fgetc returns char instead of int
  • Using feof as the loop condition before attempting a read
  • Ignoring ferror after a failed or short read
  • Assuming fgets always reads a complete line
  • Using unsafe fscanf formats without field widths
  • Assuming raw structure representations are portable file formats
  • Ignoring fwrite or fprintf failures
  • Using fflush as an input-buffer clearing mechanism
  • Assuming every stream supports arbitrary seeking
  • Mixing input functions without understanding whitespace consumption

Best Practices

  • Always check fopen results
  • Use the narrowest appropriate file mode
  • Close every stream you successfully open
  • Check important read and write return values
  • Use fgets for predictable line-oriented input
  • Use fgetc's int return value correctly
  • Check ferror when a read stops unexpectedly
  • Use binary mode when processing binary data
  • Define portable serialization formats instead of dumping structures when portability matters
  • Use explicit buffer sizes
  • Keep file ownership clear
  • Use temporary variables and careful error handling for multi-step file operations

Quick Reference

FunctionPurpose
fopenOpen a file stream
fcloseClose a file stream
fgetcRead one character
fputcWrite one character
fgetsRead a line or character sequence
fputsWrite a string
fprintfFormatted output to a stream
fscanfFormatted input from a stream
freadRead binary/object data
fwriteWrite binary/object data
fseekChange file position
ftellGet file position
rewindMove to beginning and clear indicators
fflushFlush buffered output
feofTest end-of-file indicator
ferrorTest stream error indicator
clearerrClear EOF and error indicators
removeRemove a file
renameRename a file

Practice Exercises

  • Write a program that opens a text file and prints every line
  • Count the number of characters in a file using fgetc
  • Count lines and words in a text file
  • Copy one text file to another using fgets and fputs
  • Copy a binary file using fread and fwrite
  • Append timestamped messages to a log file
  • Read integers from a file and calculate their sum
  • Use fseek and ftell to inspect file positions
  • Implement a program that reads a file in fixed-size chunks
  • Detect whether a file read ended because of EOF or an error
  • Create a simple binary record format
  • Read and write a structure while considering portability issues
  • Implement a line parser using fgets and sscanf
  • Safely handle a file that contains lines longer than your input buffer

Conclusion

C's stdio library provides a flexible interface for both text and binary file operations. Functions such as fopen, fclose, fgets, fprintf, fread, fwrite, fseek, and ftell cover most basic file-processing needs.

Reliable file I/O requires more than simply calling these functions. Programs should check return values, distinguish EOF from errors, manage stream ownership carefully, select appropriate modes, and avoid assumptions about platform-specific file representations.