Mastering File I/O in C: A Comprehensive Guide to Reading and Writing Files

File Input/Output (I/O) is a fundamental aspect of C programming that enables applications to persist data, read configuration files, process large datasets, and interact with the operating system. Unlike higher-level languages that abstract away file handling complexities, C provides a powerful yet granular set of functions that give developers precise control over file operations. This guide explores everything from basic file operations to advanced techniques for robust file handling.

Understanding File I/O in C

C treats files as streams of bytes, whether they're text files, binary files, or even hardware devices. The standard I/O library (stdio.h) provides functions for file operations, building on the concept of file pointers (FILE*) that serve as handles to open files.

Key Concepts:

  • Stream: A logical interface to a file or device
  • File Pointer: A pointer to a FILE structure that contains stream information
  • Buffer: Temporary storage that improves I/O performance
  • File Position: Current location within a file for read/write operations

Opening and Closing Files

1. The fopen() Function

The foundation of file I/O is opening a file with fopen():

#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
// Open file for reading
file = fopen("example.txt", "r");
if (file == NULL) {
printf("Error: Could not open file\n");
return 1;
}
// File operations go here
// Always close the file
fclose(file);
return 0;
}

File Opening Modes:

ModeDescriptionFile ExistsFile Doesn't Exist
"r"ReadOpens existing fileReturns NULL
"w"WriteTruncates to zero lengthCreates new file
"a"AppendOpens, positions at endCreates new file
"r+"Read/UpdateOpens existing fileReturns NULL
"w+"Write/UpdateTruncates to zero lengthCreates new file
"a+"Append/UpdateOpens, positions at endCreates new file
"rb"Binary readOpens existing fileReturns NULL
"wb"Binary writeTruncates to zero lengthCreates new file
"ab"Binary appendOpens, positions at endCreates new file

2. Error Handling with fopen()

FILE* safe_open_file(const char *filename, const char *mode) {
FILE *file = fopen(filename, mode);
if (file == NULL) {
fprintf(stderr, "Error opening file '%s': ", filename);
perror("");  // Prints system error message
return NULL;
}
return file;
}
int main() {
FILE *file = safe_open_file("config.txt", "r");
if (file == NULL) {
return EXIT_FAILURE;
}
// Process file...
fclose(file);
return EXIT_SUCCESS;
}

Reading from Files

1. Character-by-Character Reading with fgetc()

void read_file_char_by_char(const char *filename) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return;
}
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);  // Print each character
}
if (feof(file)) {
printf("\nEnd of file reached.\n");
} else if (ferror(file)) {
printf("Error reading file.\n");
}
fclose(file);
}

2. Line-by-Line Reading with fgets()

void read_file_line_by_line(const char *filename) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return;
}
char buffer[256];
int line_number = 1;
while (fgets(buffer, sizeof(buffer), file) != NULL) {
// Remove trailing newline if present
buffer[strcspn(buffer, "\n")] = 0;
printf("Line %d: %s\n", line_number++, buffer);
}
fclose(file);
}

3. Formatted Reading with fscanf()

typedef struct {
char name[50];
int age;
double salary;
} Employee;
void read_employee_data(const char *filename) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return;
}
Employee emp;
int count = 0;
// Read formatted data
while (fscanf(file, "%49s %d %lf", emp.name, &emp.age, &emp.salary) == 3) {
printf("Employee %d: %s, %d years, $%.2f\n", 
++count, emp.name, emp.age, emp.salary);
}
fclose(file);
}

4. Reading Binary Data with fread()

typedef struct {
int id;
char name[50];
double balance;
} Record;
void read_binary_records(const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
perror("Error opening file");
return;
}
Record rec;
size_t bytes_read;
while ((bytes_read = fread(&rec, sizeof(Record), 1, file)) == 1) {
printf("ID: %d, Name: %s, Balance: %.2f\n", 
rec.id, rec.name, rec.balance);
}
fclose(file);
}

Writing to Files

1. Character-by-Character Writing with fputc()

void write_characters_to_file(const char *filename) {
FILE *file = fopen(filename, "w");
if (file == NULL) {
perror("Error opening file for writing");
return;
}
const char *message = "Hello, World!\n";
for (int i = 0; message[i] != '\0'; i++) {
fputc(message[i], file);
}
printf("Successfully wrote to %s\n", filename);
fclose(file);
}

2. String Writing with fputs()

void write_lines_to_file(const char *filename) {
FILE *file = fopen(filename, "w");
if (file == NULL) {
perror("Error opening file for writing");
return;
}
const char *lines[] = {
"First line",
"Second line",
"Third line",
NULL
};
for (int i = 0; lines[i] != NULL; i++) {
if (fputs(lines[i], file) == EOF) {
printf("Error writing line %d\n", i);
break;
}
fputc('\n', file);  // Add newline
}
printf("Successfully wrote %d lines\n", 3);
fclose(file);
}

3. Formatted Writing with fprintf()

void write_formatted_data(const char *filename) {
FILE *file = fopen(filename, "w");
if (file == NULL) {
perror("Error opening file for writing");
return;
}
// Write header
fprintf(file, "%-20s %10s %15s\n", "Name", "Age", "Salary");
fprintf(file, "%s\n", "----------------------------------------");
// Write data rows
fprintf(file, "%-20s %10d %15.2f\n", "John Doe", 30, 55000.50);
fprintf(file, "%-20s %10d %15.2f\n", "Jane Smith", 28, 62000.75);
fprintf(file, "%-20s %10d %15.2f\n", "Bob Johnson", 35, 48500.00);
fclose(file);
}

4. Writing Binary Data with fwrite()

void write_binary_records(const char *filename) {
FILE *file = fopen(filename, "wb");
if (file == NULL) {
perror("Error opening file for binary writing");
return;
}
Record records[] = {
{1, "Alice", 1000.50},
{2, "Bob", 2500.75},
{3, "Charlie", 3200.25},
{4, "Diana", 1800.00}
};
size_t num_records = sizeof(records) / sizeof(records[0]);
size_t written = fwrite(records, sizeof(Record), num_records, file);
if (written == num_records) {
printf("Successfully wrote %zu records\n", written);
} else {
printf("Error: Only wrote %zu of %zu records\n", written, num_records);
}
fclose(file);
}

File Positioning

1. Using ftell() and fseek()

void demonstrate_file_positioning(const char *filename) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return;
}
// Get current position (should be 0)
long position = ftell(file);
printf("Initial position: %ld\n", position);
// Read first 10 characters
char buffer[11] = {0};
fread(buffer, 1, 10, file);
printf("Read: '%s'\n", buffer);
// Get new position
position = ftell(file);
printf("After reading 10 bytes: %ld\n", position);
// Seek back to beginning
fseek(file, 0, SEEK_SET);
position = ftell(file);
printf("After seeking to start: %ld\n", position);
// Seek to 20 bytes from end
fseek(file, -20, SEEK_END);
position = ftell(file);
printf("20 bytes from end: %ld\n", position);
fclose(file);
}

2. Using fgetpos() and fsetpos()

void demonstrate_fgetpos_fsetpos(const char *filename) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return;
}
fpos_t pos;
// Save current position
fgetpos(file, &pos);
// Read some data
char buffer[50];
fgets(buffer, sizeof(buffer), file);
printf("Read: %s", buffer);
// Restore saved position
fsetpos(file, &pos);
// Read again - should read the same line
fgets(buffer, sizeof(buffer), file);
printf("Read again: %s", buffer);
fclose(file);
}

Advanced File Operations

1. Temporary Files

void demonstrate_temp_files() {
char temp_filename[] = "/tmp/tempfileXXXXXX";
int fd = mkstemp(temp_filename);
if (fd == -1) {
perror("Error creating temp file");
return;
}
// Convert file descriptor to FILE stream
FILE *temp_file = fdopen(fd, "w+");
if (temp_file == NULL) {
perror("Error opening temp file stream");
close(fd);
return;
}
// Write to temp file
fprintf(temp_file, "Temporary data");
rewind(temp_file);
// Read back
char buffer[100];
fgets(buffer, sizeof(buffer), temp_file);
printf("Temp file contains: %s\n", buffer);
fclose(temp_file);
unlink(temp_filename);  // Delete the file
}

2. Memory-Mapped Files (Linux/Unix)

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void demonstrate_mmap(const char *filename) {
int fd = open(filename, O_RDWR);
if (fd == -1) {
perror("Error opening file");
return;
}
// Get file size
struct stat st;
fstat(fd, &st);
size_t filesize = st.st_size;
// Map file into memory
char *data = mmap(NULL, filesize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("Error mapping file");
close(fd);
return;
}
// Access file as memory
printf("First 50 bytes: %.50s\n", data);
// Modify file (if writable)
if (filesize > 0) {
data[0] = toupper(data[0]);
}
// Clean up
munmap(data, filesize);
close(fd);
}

3. Directory Operations

#include <dirent.h>
#include <sys/stat.h>
void list_directory_contents(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
perror("Error opening directory");
return;
}
struct dirent *entry;
struct stat file_stat;
char full_path[1024];
printf("Contents of '%s':\n", path);
printf("%-30s %10s %s\n", "Name", "Size", "Type");
printf("%s\n", "----------------------------------------");
while ((entry = readdir(dir)) != NULL) {
// Skip . and ..
if (strcmp(entry->d_name, ".") == 0 || 
strcmp(entry->d_name, "..") == 0) {
continue;
}
// Build full path
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
if (stat(full_path, &file_stat) == 0) {
const char *type = S_ISDIR(file_stat.st_mode) ? "DIR" : "FILE";
printf("%-30s %10ld %s\n", 
entry->d_name, file_stat.st_size, type);
}
}
closedir(dir);
}

Error Handling and Best Practices

1. Comprehensive Error Handling

typedef enum {
FILE_SUCCESS = 0,
FILE_ERR_OPEN,
FILE_ERR_READ,
FILE_ERR_WRITE,
FILE_ERR_SEEK,
FILE_ERR_CLOSE
} FileErrorCode;
typedef struct {
FileErrorCode code;
char message[256];
} FileResult;
FileResult safe_file_copy(const char *src, const char *dest) {
FileResult result = {FILE_SUCCESS, ""};
FILE *source = fopen(src, "rb");
if (source == NULL) {
result.code = FILE_ERR_OPEN;
snprintf(result.message, sizeof(result.message),
"Cannot open source file: %s", src);
return result;
}
FILE *destination = fopen(dest, "wb");
if (destination == NULL) {
result.code = FILE_ERR_OPEN;
snprintf(result.message, sizeof(result.message),
"Cannot open destination file: %s", dest);
fclose(source);
return result;
}
char buffer[8192];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source)) > 0) {
if (fwrite(buffer, 1, bytes_read, destination) != bytes_read) {
result.code = FILE_ERR_WRITE;
strcpy(result.message, "Error writing to destination");
break;
}
}
if (ferror(source)) {
result.code = FILE_ERR_READ;
strcpy(result.message, "Error reading from source");
}
fclose(source);
fclose(destination);
return result;
}

2. Buffering Control

void demonstrate_buffering() {
FILE *file = fopen("buffered.txt", "w");
// Set full buffering with custom buffer
char buffer[4096];
setvbuf(file, buffer, _IOFBF, sizeof(buffer));
// These writes will be buffered
for (int i = 0; i < 1000; i++) {
fprintf(file, "Line %d\n", i);
}
// Force write of buffer
fflush(file);
// Switch to line buffering
setvbuf(file, NULL, _IOLBF, 0);
// These will be written when newline is encountered
fprintf(file, "This will be written immediately\n");
fprintf(file, "This too\n");
fclose(file);
}

3. Atomic Operations with flock()

#include <sys/file.h>
void safe_write_with_lock(const char *filename, const char *data) {
FILE *file = fopen(filename, "a");
if (file == NULL) {
perror("Error opening file");
return;
}
int fd = fileno(file);
// Acquire exclusive lock
if (flock(fd, LOCK_EX) == -1) {
perror("Error locking file");
fclose(file);
return;
}
// Write data (now safe from concurrent access)
fprintf(file, "%s\n", data);
fflush(file);  // Ensure data is written
// Release lock
flock(fd, LOCK_UN);
fclose(file);
}

Performance Optimization

1. Efficient Reading with Larger Buffers

void efficient_file_copy(const char *src, const char *dest) {
FILE *source = fopen(src, "rb");
FILE *destination = fopen(dest, "wb");
if (!source || !destination) {
perror("Error opening files");
return;
}
// Use large buffer for efficiency
char *buffer = malloc(65536);  // 64KB buffer
if (buffer == NULL) {
perror("Memory allocation failed");
return;
}
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 65536, source)) > 0) {
fwrite(buffer, 1, bytes_read, destination);
}
free(buffer);
fclose(source);
fclose(destination);
}

2. Reading Entire File into Memory

char* read_entire_file(const char *filename, long *file_size) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
return NULL;
}
// Get file size
fseek(file, 0, SEEK_END);
*file_size = ftell(file);
rewind(file);
// Allocate memory
char *buffer = malloc(*file_size + 1);
if (buffer == NULL) {
fclose(file);
return NULL;
}
// Read entire file
size_t bytes_read = fread(buffer, 1, *file_size, file);
if (bytes_read != *file_size) {
free(buffer);
fclose(file);
return NULL;
}
buffer[*file_size] = '\0';  // Null terminate
fclose(file);
return buffer;
}

Practical Examples

1. CSV File Parser

typedef struct {
char **fields;
int field_count;
} CSVRow;
typedef struct {
CSVRow *rows;
int row_count;
int capacity;
} CSVData;
CSVData* parse_csv(const char *filename, char delimiter) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
return NULL;
}
CSVData *data = malloc(sizeof(CSVData));
data->rows = NULL;
data->row_count = 0;
data->capacity = 0;
char line[4096];
while (fgets(line, sizeof(line), file)) {
// Remove trailing newline
line[strcspn(line, "\n")] = 0;
// Count fields
int field_count = 1;
for (char *p = line; *p; p++) {
if (*p == delimiter) field_count++;
}
// Allocate row
CSVRow row;
row.fields = malloc(field_count * sizeof(char*));
row.field_count = 0;
// Parse fields
char *token = strtok(line, &delimiter);
while (token != NULL) {
row.fields[row.field_count] = malloc(strlen(token) + 1);
strcpy(row.fields[row.field_count], token);
row.field_count++;
token = strtok(NULL, &delimiter);
}
// Add to data structure
if (data->row_count >= data->capacity) {
data->capacity = data->capacity == 0 ? 10 : data->capacity * 2;
data->rows = realloc(data->rows, 
data->capacity * sizeof(CSVRow));
}
data->rows[data->row_count++] = row;
}
fclose(file);
return data;
}
void free_csv_data(CSVData *data) {
for (int i = 0; i < data->row_count; i++) {
for (int j = 0; j < data->rows[i].field_count; j++) {
free(data->rows[i].fields[j]);
}
free(data->rows[i].fields);
}
free(data->rows);
free(data);
}

2. Configuration File Reader

typedef struct {
char key[128];
char value[256];
} ConfigEntry;
typedef struct {
ConfigEntry *entries;
int count;
int capacity;
} Config;
Config* read_config(const char *filename) {
Config *config = malloc(sizeof(Config));
config->entries = NULL;
config->count = 0;
config->capacity = 0;
FILE *file = fopen(filename, "r");
if (file == NULL) {
free(config);
return NULL;
}
char line[512];
while (fgets(line, sizeof(line), file)) {
// Skip comments and empty lines
if (line[0] == '#' || line[0] == '\n') {
continue;
}
// Remove trailing newline
line[strcspn(line, "\n")] = 0;
// Split at '='
char *equals = strchr(line, '=');
if (equals == NULL) continue;
*equals = '\0';
char *key = line;
char *value = equals + 1;
// Trim whitespace
while (isspace(*key)) key++;
while (isspace(*value)) value++;
// Add to config
if (config->count >= config->capacity) {
config->capacity = config->capacity == 0 ? 10 : config->capacity * 2;
config->entries = realloc(config->entries, 
config->capacity * sizeof(ConfigEntry));
}
strncpy(config->entries[config->count].key, key, 127);
strncpy(config->entries[config->count].value, value, 255);
config->count++;
}
fclose(file);
return config;
}
const char* get_config_value(Config *config, const char *key) {
for (int i = 0; i < config->count; i++) {
if (strcmp(config->entries[i].key, key) == 0) {
return config->entries[i].value;
}
}
return NULL;
}

Common Pitfalls and Solutions

1. Buffer Overflow Protection

// UNSAFE
char buffer[100];
fscanf(file, "%s", buffer);  // No bounds checking!
// SAFE
char buffer[100];
fscanf(file, "%99s", buffer);  // Limit input size
// SAFER
char buffer[100];
if (fgets(buffer, sizeof(buffer), file) == NULL) {
// Handle error
}

2. End-of-File and Error Distinction

// WRONG - can't distinguish EOF from error
while (!feof(file)) {
fgets(buffer, sizeof(buffer), file);
// Process buffer
}
// CORRECT
while (fgets(buffer, sizeof(buffer), file) != NULL) {
// Process buffer
}
// Check for errors after loop
if (ferror(file)) {
printf("Error reading file\n");
}

3. Portability Considerations

// NOT portable (Windows vs Unix line endings)
fprintf(file, "Line 1\nLine 2\n");
// Portable
fprintf(file, "Line 1" NEWLINE "Line 2" NEWLINE);

Conclusion

File I/O in C provides developers with powerful, low-level control over data persistence and retrieval. From basic text file operations to advanced memory-mapped I/O, the standard library offers a comprehensive set of tools for every file handling need. Understanding buffering, error handling, and performance optimization techniques is essential for writing robust, efficient file operations.

Key takeaways:

  • Always check return values and handle errors gracefully
  • Choose the right file opening mode for your needs
  • Use appropriate buffer sizes for optimal performance
  • Distinguish between text and binary modes when necessary
  • Remember to close files to prevent resource leaks
  • Consider portability when working across different platforms

Mastering file I/O in C is essential for systems programming, data processing, and building applications that interact persistently with the operating system. With the techniques covered in this guide, you'll be well-equipped to handle any file operation challenge in your C programs.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper