Skip to content

C File I/O

Beyond interacting with standard input (keyboard) and standard output (screen), C programs often need to read data from or write data to files for persistent storage. This chapter covers the standard C library functions for file input/output (I/O).

In C, a file is typically treated as a sequence of bytes, commonly referred to as a stream. The standard I/O library (<stdio.h>) provides functions to manage these streams, whether they represent text files or binary files.

All standard I/O operations work with a pointer to a FILE structure. This structure, defined in <stdio.h>, holds internal information needed to manage the file stream, such as the current position, buffer details, error indicators, and end-of-file status. You typically don’t interact with the members of the FILE structure directly; instead, you pass a FILE * pointer to the various I/O functions.

To work with a file, you first need to open it using the fopen() function. It attempts to associate a stream with the specified file.

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

filename: A string containing the name (and potentially path) of the file to open.

mode: A string specifying how you intend to use the file (read, write, append, etc.). Common modes include:

ModeDescription (Text Files)
"r"Open an existing text file for reading. File must exist.
"w"Open a text file for writing. If the file exists, its contents are discarded (truncated). If it doesn’t exist, a new file is created.
"a"Open a text file for appending (writing at the end). If the file doesn’t exist, a new file is created.
"r+"Open an existing text file for both reading and writing. File must exist.
"w+"Open a text file for both reading and writing. Truncates existing file or creates a new one.
"a+"Open a text file for reading and appending. Creates file if it doesn’t exist. Initial reading position is the beginning, but writing always appends to the end.

For binary files, append b to the mode string (e.g., "rb", "wb", "ab", "rb+", "wb+", "ab+"). On Unix-like systems, b often has little effect, but on systems like Windows, it’s crucial for correctly handling binary data (prevents newline character translation).

Return Value: If successful, fopen() returns a non-NULL FILE * pointer associated with the file. If an error occurs (e.g., file not found for "r" mode, permission denied), it returns NULL and sets errno.

Crucial: Always check if the returned pointer is NULL after calling fopen().

When you are finished with a file, you must close it using fclose() to ensure all buffered data is written to disk (flushed) and system resources associated with the file are released.

int fclose(FILE *stream);

stream: The FILE * pointer obtained from fopen().

Return Value: Returns 0 on success. Returns EOF (a constant defined in <stdio.h>, usually -1) if an error occurs (e.g., error during final buffer flush). It’s good practice to check the return value of fclose(), especially for files opened for writing.

Several functions allow writing to text files:

  • int fputc(int c, FILE *stream);: Writes a single character c to the stream. Returns the character written on success, or EOF on error.
  • int fputs(const char * restrict s, FILE * restrict stream);: Writes the null-terminated string s to the stream (excluding the null terminator). Returns a non-negative value on success, or EOF on error.
  • int fprintf(FILE * restrict stream, const char * restrict format, ...);: Writes formatted output to the stream, similar to printf but targeting a file. Returns the number of characters written on success, or a negative value on error.

Example: Writing to a file

#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
#include <errno.h> // For errno
#include <string.h> // For strerror
int main(void) {
FILE *fp = NULL; // Initialize pointer to NULL
const char *filename = "output.txt"; // Use a relative path
// Open file in write mode ("w+" allows writing and reading)
fp = fopen(filename, "w+");
if (fp == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}
// Write using fprintf
int written = fprintf(fp, "Testing fprintf: line %d\n", 1);
if (written < 0) {
perror("Error writing with fprintf");
fclose(fp); // Attempt to close even on error
return EXIT_FAILURE;
}
// Write using fputs
if (fputs("Testing fputs: line 2\n", fp) == EOF) {
perror("Error writing with fputs");
fclose(fp);
return EXIT_FAILURE;
}
// Write using fputc
if (fputc('3', fp) == EOF) {
perror("Error writing with fputc");
fclose(fp);
return EXIT_FAILURE;
}
fputc('\n', fp); // Don't forget newline if needed
printf("Successfully wrote to %s\n", filename);
// Close the file and check for errors
if (fclose(fp) == EOF) {
perror("Error closing file");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

After running, output.txt will contain:

Testing fprintf: line 1
Testing fputs: line 2
3

Functions for reading from text files include:

  • int fgetc(FILE *stream);: Reads the next character from the stream. Returns the character read (as an int) on success, or EOF on end-of-file or error. Use feof() and ferror() to distinguish between EOF and error.
  • char *fgets(char * restrict s, int n, FILE * restrict stream);: Reads at most n-1 characters from the stream into the buffer s. Reading stops after a newline (\n) character, at end-of-file, or after n-1 characters. If a newline is read, it is stored in the buffer. A null terminator (\0) is always appended. Returns s on success, or NULL on end-of-file (with no characters read) or error. fgets is generally safer than fscanf for reading strings as it prevents buffer overflows.
  • int fscanf(FILE * restrict stream, const char * restrict format, ...);: Reads formatted input from the stream, similar to scanf. Returns the number of input items successfully matched and assigned, or EOF on input failure before any conversion. Warning: Using fscanf with %s without a width limit is dangerous and can lead to buffer overflows if the input is longer than the buffer. Use %<width>s (e.g., %254s) or preferably use fgets.

Example: Reading from the previously created file

#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
#define BUFFER_SIZE 256
int main(void) {
FILE *fp = NULL;
char buffer[BUFFER_SIZE];
const char *filename = "output.txt";
fp = fopen(filename, "r");
if (fp == NULL) {
perror("Error opening file for reading");
return EXIT_FAILURE;
}
printf("Reading content using fgets:\n---\n");
// Read line by line using fgets (safer)
while (fgets(buffer, BUFFER_SIZE, fp) != NULL) {
// Process the line (here, just print it)
printf("%s", buffer); // fgets includes the newline, so no extra \n needed
}
printf("---\n");
// Check if loop ended due to error or EOF
if (ferror(fp)) {
perror("Error reading file");
} else if (feof(fp)) {
printf("End of file reached.\n");
} else {
printf("fgets returned NULL without EOF or error set? Unexpected.\n");
}
if (fclose(fp) == EOF) {
perror("Error closing file");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

When compiled and executed, this reads output.txt and produces:

Reading content using fgets:
---
Testing fprintf: line 1
Testing fputs: line 2
3
---
End of file reached.

For reading and writing binary data (like raw bytes, arrays, or structures), use fread and fwrite. These functions work with blocks of data of a specified size.

size_t fread(void * restrict ptr, size_t size, size_t count, FILE * restrict stream);
size_t fwrite(const void * restrict ptr, size_t size, size_t count, FILE * restrict stream);
  • ptr: Pointer to the buffer to read data into (fread) or write data from (fwrite).
  • size: Size (in bytes) of each element to be read/written.
  • count: Number of elements to read/write.
  • stream: The FILE * pointer.
  • Return Value: Both functions return the number of elements successfully read or written. This value may be less than count if an error occurs or the end of the file is reached (fread). Use feof() and ferror() to determine the cause.

Example: Writing and reading a simple structure

#include <stdio.h>
#include <stdlib.h>
// Define a simple structure
typedef struct {
int id;
double value;
} Record;
int main(void) {
FILE *fp = NULL;
const char *filename = "records.bin";
Record rec_write = {101, 3.14159};
Record rec_read;
size_t items_written, items_read;
// --- Write the structure to a binary file ---
fp = fopen(filename, "wb"); // Note: "wb" mode
if (!fp) { perror("Cannot open for writing"); return EXIT_FAILURE; }
items_written = fwrite(&rec_write, sizeof(Record), 1, fp);
if (items_written != 1) {
fprintf(stderr, "Error writing record (wrote %zu elements).\n", items_written);
fclose(fp);
return EXIT_FAILURE;
}
printf("Wrote record: ID=%d, Value=%.5f\n", rec_write.id, rec_write.value);
if (fclose(fp) == EOF) { perror("Error closing after write"); return EXIT_FAILURE; }
// --- Read the structure back from the binary file ---
fp = fopen(filename, "rb"); // Note: "rb" mode
if (!fp) { perror("Cannot open for reading"); return EXIT_FAILURE; }
items_read = fread(&rec_read, sizeof(Record), 1, fp);
if (items_read != 1) {
if (feof(fp)) {
fprintf(stderr, "Error reading record: Unexpected end of file.\n");
} else if (ferror(fp)) {
perror("Error reading record");
}
fclose(fp);
return EXIT_FAILURE;
}
printf("Read record: ID=%d, Value=%.5f\n", rec_read.id, rec_read.value);
if (fclose(fp) == EOF) { perror("Error closing after read"); return EXIT_FAILURE; }
return EXIT_SUCCESS;
}

Output:

Wrote record: ID=101, Value=3.14159
Read record: ID=101, Value=3.14159