C File I/O: Reading and Writing Files
File input/output is fundamental to C programming. Unlike higher-level languages, C exposes the raw operating system interface for file operations. You manage buffers, check return codes, and handle errors explicitly. This gives you complete control but demands careful attention to detail.
This guide covers text and binary file I/O, formatted reading and writing, error handling, and practical patterns for real-world file processing.
Opening and Closing Files
The fopen function opens a file and returns a FILE* pointer:
#include <stdio.h>
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("Failed to open file");
return 1;
---
// File operations here
fclose(fp);File Modes
| Mode | Description |
|---|---|
"r" | Read (file must exist) |
"w" | Write (creates or truncates) |
"a" | Append (creates if missing) |
"r+" | Read and write (file must exist) |
"w+" | Read and write (creates or truncates) |
"a+" | Read and append (creates if missing) |
"rb" | Binary read |
"wb" | Binary write |
"ab" | Binary append |
Always check the return value of fopen. A NULL return means the file could not be opened. Use perror() to print the system error message.
Text File I/O
Reading Characters
int ch;
FILE *fp = fopen("input.txt", "r");
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
---
fclose(fp);fgetc returns int (not char) because EOF is a negative value that cannot be represented in unsigned char.
Reading Lines with fgets
char line[1024];
while (fgets(line, sizeof(line), fp) != NULL) {
// Remove trailing newline
line[strcspn(line, "\n")] = '\0';
printf("Read: %s\n", line);
---fgets reads up to n-1 characters or until a newline, then null-terminates the buffer. It includes the newline if there is room. Use strcspn to strip the trailing newline safely.
Formatted Reading with fscanf
// data.txt:
// Alice 42 3.14
// Bob 37 2.71
char name[64];
int age;
double value;
while (fscanf(fp, "%63s %d %lf", name, &age, &value) == 3) {
printf("%s is %d years old, value = %.2f\n", name, age, value);
---Check the return value of fscanf — it returns the number of successfully matched items. A return value less than expected indicates a parse error or EOF. Use width specifiers (%63s) to prevent buffer overflows.
Formatted Writing with fprintf
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
perror("Failed to create output file");
return 1;
---
fprintf(fp, "Name: %s\n", "Alice");
fprintf(fp, "Score: %d / %d\n", 95, 100);
fprintf(fp, "Average: %.2f\n", 85.7);
fclose(fp);Binary File I/O
Binary I/O reads and writes raw bytes without formatting. It is more compact and precise than text I/O:
Writing Binary Data
#include <stdio.h>
#include <stdint.h>
typedef struct {
uint32_t id;
char name[32];
float salary;
--- Employee;
Employee emp = {
.id = 1001,
.name = "Alice Smith",
.salary = 75000.50
---;
FILE *fp = fopen("employees.bin", "wb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
---
size_t written = fwrite(&emp, sizeof(Employee), 1, fp);
if (written != 1) {
perror("Write failed");
fclose(fp);
return 1;
---
fclose(fp);Reading Binary Data
Employee emp;
FILE *fp = fopen("employees.bin", "rb");
if (fp == NULL) {
perror("Cannot open file");
return 1;
---
size_t read = fread(&emp, sizeof(Employee), 1, fp);
if (read != 1) {
if (feof(fp)) {
fprintf(stderr, "Unexpected end of file\n");
} else {
perror("Read failed");
}
fclose(fp);
return 1;
---
printf("ID: %u, Name: %s, Salary: %.2f\n", emp.id, emp.name, emp.salary);
fclose(fp);Binary Arrays
// Write an array
int data[] = {10, 20, 30, 40, 50};
size_t count = sizeof(data) / sizeof(data[0]);
fwrite(data, sizeof(int), count, fp);
// Read an array
int buffer[100];
size_t items_read = fread(buffer, sizeof(int), count, fp);
for (size_t i = 0; i < items_read; i++) {
printf("%d ", buffer[i]);
---Random Access
fseek and ftell let you navigate within a file:
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) return 1;
// Seek to 100 bytes from the start
fseek(fp, 100, SEEK_SET);
// Seek 50 bytes forward from current position
fseek(fp, 50, SEEK_CUR);
// Seek 20 bytes before the end
fseek(fp, -20, SEEK_END);
// Get current position
long pos = ftell(fp);
// Seek to a specific record
int record_num = 5;
fseek(fp, record_num * sizeof(Record), SEEK_SET);
fread(&record, sizeof(Record), 1, fp);
fclose(fp);SEEK_SET, SEEK_CUR, and SEEK_END control the reference point. ftell returns the current file position or -1 on error. For files larger than 2 GB, use fseeko and ftello with off_t.
Error Handling
Checking for Errors
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
return 1;
---
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
---
if (ferror(fp)) {
fprintf(stderr, "Error reading file\n");
clearerr(fp);
---
fclose(fp);| Function | Purpose | |
Text vs Binary File Modes
When opening files, C distinguishes between text mode and binary mode. On Unix-like systems they are identical, but on Windows text mode translates \n to \r\n on write and \r\n to \n on read. Binary mode ("rb", "wb") disables this translation, which is essential for reading non-text data like images or serialized structures. Always use binary mode when working with non-text data to avoid corruption across platforms.
File Positioning
ftell() returns the current position in the file, and fseek() moves to a specific position:
fseek(fp, 0, SEEK_END);
long size = ftell(fp); // Get file size
rewind(fp); // Go back to beginning
fseek(fp, 100, SEEK_SET); // Go to byte 100 from start
fseek(fp, -50, SEEK_CUR); // Go back 50 bytes from current
fseek(fp, 0, SEEK_END); // Go to end
Random access is useful for database files, index structures, and record-based file formats where you need to read or update specific records without scanning the entire file.
Error Handling with errno
File operations can fail for many reasons — disk full, permission denied, file not found. Always check return values and use errno and perror() to diagnose failures:
#include <errno.h>
#include <string.h>
FILE *fp = fopen("config.dat", "r");
if (!fp) {
fprintf(stderr, "Failed to open file: %s\n", strerror(errno));
return 1;
---Common error codes include ENOENT (file not found), EACCES (permission denied), ENOSPC (disk full), and EISDIR (path is a directory). Handling these specifically allows your program to give users meaningful error messages rather than a generic failure.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
———-|———|
| ferror(fp) | Check if error indicator is set |
| feof(fp) | Check if end-of-file indicator is set |
| clearerr(fp) | Clear both error and EOF indicators |
| perror(msg) | Print msg: <system error> to stderr |
| strerror(errno) | Get string description of error code |
Robust Read Loop
char buffer[4096];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
size_t written = fwrite(buffer, 1, bytes_read, out_fp);
if (written != bytes_read) {
perror("Write failed");
break;
}
---
if (ferror(fp)) {
perror("Read error");
---Buffering
C’s file I/O is buffered by default. Three buffering modes are available:
#include <stdio.h>
// No buffering — writes go directly to disk
setvbuf(fp, NULL, _IONBF, 0);
// Line buffering — flush on newline (default for terminals)
setvbuf(fp, NULL, _IOLBF, 0);
// Full buffering — flush when buffer is full (default for files)
char buf[8192];
setvbuf(fp, buf, _IOFBF, sizeof(buf));Set buffering before any I/O operations on the file. For performance-sensitive applications, a large custom buffer (64 KB or more) can significantly reduce system calls.
Practical Examples
CSV Parser
#include <stdio.h>
#include <string.h>
int parse_csv(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) return -1;
char line[4096];
int line_num = 0;
while (fgets(line, sizeof(line), fp)) {
line_num++;
line[strcspn(line, "\n")] = '\0';
char *token = strtok(line, ",");
int col = 0;
while (token) {
printf("Row %d, Col %d: %s\n", line_num, col++, token);
token = strtok(NULL, ",");
}
}
fclose(fp);
return 0;
---File Copy
int copy_file(const char *src, const char *dst) {
FILE *in = fopen(src, "rb");
if (in == NULL) {
perror("Source open failed");
return -1;
}
FILE *out = fopen(dst, "wb");
if (out == NULL) {
perror("Dest open failed");
fclose(in);
return -1;
}
char buffer[65536];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if (fwrite(buffer, 1, bytes, out) != bytes) {
perror("Write failed");
fclose(in);
fclose(out);
return -1;
}
}
if (ferror(in)) {
perror("Read failed");
fclose(in);
fclose(out);
return -1;
}
fclose(in);
fclose(out);
return 0;
---Temporary Files
#include <stdlib.h>
void process_temp(void) {
char template[] = "/tmp/myapp_XXXXXX";
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp failed");
return;
}
FILE *fp = fdopen(fd, "w+");
if (fp == NULL) {
perror("fdopen failed");
close(fd);
return;
}
fprintf(fp, "Temporary data\n");
rewind(fp);
char buf[256];
fgets(buf, sizeof(buf), fp);
printf("Read back: %s", buf);
fclose(fp);
unlink(template); // Delete the temp file
---Best Practices
- Always check return values — Every I/O function can fail. Check and handle errors.
- Close files promptly — Not closing leads to resource leaks. Use
fclosein cleanup paths. - Use sizeof on variables —
fread(buf, 1, sizeof(buf), fp)is correct even ifbuftype changes. - Prefer binary for structured data — Binary is more compact and avoids parsing ambiguity.
- Flush before seeking between read/write — Call
fflushorfseekbetween read and write operations on"r+"files. - Use fixed-width integer types —
uint32_tguarantees portability in binary files.
Summary
C’s file I/O library provides complete control over reading and writing data. Text I/O with fgets, fscanf, and fprintf handles human-readable formats. Binary I/O with fread and fwrite stores structured data compactly. Error checking with ferror, feof, and return values from every function call separates robust programs from fragile ones. Buffering, random access, and temporary file functions complete the toolkit for production file operations.
For a comprehensive overview, read our article on C Memory Management.
For a comprehensive overview, read our article on C Pointers Guide.