Docs

README

Random Access in C File Handling

Table of Contents

  1. •Introduction to Random Access
  2. •Sequential vs Random Access
  3. •The fseek() Function
  4. •The ftell() Function
  5. •The rewind() Function
  6. •The fgetpos() and fsetpos() Functions
  7. •Random Access with Fixed-Size Records
  8. •Practical Applications
  9. •Error Handling
  10. •Best Practices
  11. •Common Pitfalls

Introduction to Random Access

Random access allows you to read from or write to any position in a file without reading all preceding data. This is essential for databases, indexed files, and any application where you need to jump to specific locations.

Why Random Access?

  • •Efficiency: Jump directly to needed data
  • •Update in place: Modify records without rewriting entire file
  • •Large file handling: Work with files too big to load into memory
  • •Database operations: Implement search, update, delete operations

Key Functions

FunctionPurpose
fseek()Move to specific position
ftell()Get current position
rewind()Move to beginning
fgetpos()Save position (portable)
fsetpos()Restore position (portable)

Sequential vs Random Access

Sequential Access

// Must read all preceding data
while (!feof(fp)) {
    fread(&record, sizeof(record), 1, fp);
    if (record.id == target_id) break;
}

Characteristics:

  • •Reads data in order from start to end
  • •Simple to implement
  • •Good for processing all records
  • •Slow for finding specific records

Random Access

// Jump directly to record #5
fseek(fp, 5 * sizeof(record), SEEK_SET);
fread(&record, sizeof(record), 1, fp);

Characteristics:

  • •Jump to any position instantly
  • •More complex to implement
  • •Essential for large files
  • •Requires fixed-size records or index

The fseek() Function

Function Signature

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

Parameters

ParameterDescription
streamFile pointer
offsetNumber of bytes to move (can be negative)
whenceStarting point for offset

Whence Values

ConstantValueStarting Point
SEEK_SET0Beginning of file
SEEK_CUR1Current position
SEEK_END2End of file

Return Value

  • •0: Success
  • •Non-zero: Error (check errno)

Examples

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.bin", "rb");
    if (fp == NULL) return 1;

    // Move to beginning of file
    fseek(fp, 0, SEEK_SET);

    // Move to position 100
    fseek(fp, 100, SEEK_SET);

    // Move forward 50 bytes from current position
    fseek(fp, 50, SEEK_CUR);

    // Move backward 10 bytes from current position
    fseek(fp, -10, SEEK_CUR);

    // Move to end of file
    fseek(fp, 0, SEEK_END);

    // Move 100 bytes before end of file
    fseek(fp, -100, SEEK_END);

    fclose(fp);
    return 0;
}

Getting File Size

long get_file_size(FILE *fp) {
    long current = ftell(fp);      // Save current position
    fseek(fp, 0, SEEK_END);        // Go to end
    long size = ftell(fp);         // Get position (= size)
    fseek(fp, current, SEEK_SET);  // Restore position
    return size;
}

The ftell() Function

Function Signature

long ftell(FILE *stream);

Return Value

  • •Success: Current position in bytes from beginning
  • •Error: -1L (check errno)

Examples

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;

    // Get initial position
    long pos = ftell(fp);
    printf("Initial position: %ld\n", pos);  // 0

    // Read some data
    char buffer[100];
    fgets(buffer, sizeof(buffer), fp);

    // Get new position
    pos = ftell(fp);
    printf("After reading: %ld\n", pos);

    // Seek and check
    fseek(fp, 50, SEEK_SET);
    pos = ftell(fp);
    printf("After seek: %ld\n", pos);  // 50

    fclose(fp);
    return 0;
}

Counting Records

long count_records(const char *filename, size_t record_size) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);

    return size / record_size;
}

The rewind() Function

Function Signature

void rewind(FILE *stream);

Behavior

  • •Moves file position to beginning
  • •Clears error indicator
  • •Equivalent to: fseek(fp, 0, SEEK_SET) plus clearing errors

Example

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;

    // Read first line
    char line[100];
    fgets(line, sizeof(line), fp);
    printf("First read: %s", line);

    // Go back to beginning
    rewind(fp);

    // Read first line again
    fgets(line, sizeof(line), fp);
    printf("After rewind: %s", line);

    fclose(fp);
    return 0;
}

rewind() vs fseek()

// These are similar but not identical:

rewind(fp);  // Clears error indicator, no return value

int result = fseek(fp, 0, SEEK_SET);  // Doesn't clear errors, returns status

Use rewind() when:

  • •You want to start over from beginning
  • •Error recovery is needed

Use fseek() when:

  • •You need to check for errors
  • •Moving to positions other than start

The fgetpos() and fsetpos() Functions

Why Use These?

  • •Portability: Work with files larger than long can represent
  • •Opacity: Position is stored in fpos_t type
  • •Reliability: Better for very large files

Function Signatures

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

Return Value

  • •0: Success
  • •Non-zero: Error (sets errno)

Example

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;

    fpos_t position;

    // Read and skip some data
    char buffer[100];
    fgets(buffer, sizeof(buffer), fp);

    // Save current position
    if (fgetpos(fp, &position) != 0) {
        perror("fgetpos failed");
        fclose(fp);
        return 1;
    }

    // Read more data
    fgets(buffer, sizeof(buffer), fp);
    printf("Second line: %s", buffer);

    // Restore saved position
    if (fsetpos(fp, &position) != 0) {
        perror("fsetpos failed");
        fclose(fp);
        return 1;
    }

    // Read same line again
    fgets(buffer, sizeof(buffer), fp);
    printf("After restore: %s", buffer);

    fclose(fp);
    return 0;
}

When to Use fgetpos/fsetpos vs ftell/fseek

ScenarioRecommended
Files < 2GB, arithmetic on positionftell/fseek
Very large filesfgetpos/fsetpos
Maximum portabilityfgetpos/fsetpos
Simple offset calculationsftell/fseek

Random Access with Fixed-Size Records

Record Structure

#define RECORD_SIZE 128  // Fixed size for each record

struct Record {
    int id;
    char name[50];
    char data[RECORD_SIZE - sizeof(int) - 50];  // Padding to fixed size
};

Calculating Record Position

// Position of record N (0-indexed)
long position = N * sizeof(struct Record);

Reading Specific Record

#include <stdio.h>

struct Employee {
    int id;
    char name[50];
    float salary;
};

int read_record(FILE *fp, int record_num, struct Employee *emp) {
    // Calculate position
    long pos = record_num * sizeof(struct Employee);

    // Seek to position
    if (fseek(fp, pos, SEEK_SET) != 0) {
        return -1;  // Seek failed
    }

    // Read record
    if (fread(emp, sizeof(struct Employee), 1, fp) != 1) {
        return -1;  // Read failed
    }

    return 0;  // Success
}

Writing Specific Record

int write_record(FILE *fp, int record_num, const struct Employee *emp) {
    long pos = record_num * sizeof(struct Employee);

    if (fseek(fp, pos, SEEK_SET) != 0) {
        return -1;
    }

    if (fwrite(emp, sizeof(struct Employee), 1, fp) != 1) {
        return -1;
    }

    return 0;
}

Complete CRUD Example

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

struct Record {
    int id;
    char name[50];
    int active;  // 0 = deleted, 1 = active
};

// Create/Update record
int save_record(const char *filename, int index, const struct Record *rec) {
    FILE *fp = fopen(filename, "r+b");
    if (fp == NULL) {
        fp = fopen(filename, "wb");
        if (fp == NULL) return -1;
    }

    fseek(fp, index * sizeof(struct Record), SEEK_SET);
    size_t written = fwrite(rec, sizeof(struct Record), 1, fp);
    fclose(fp);

    return (written == 1) ? 0 : -1;
}

// Read record
int load_record(const char *filename, int index, struct Record *rec) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;

    fseek(fp, index * sizeof(struct Record), SEEK_SET);
    size_t read = fread(rec, sizeof(struct Record), 1, fp);
    fclose(fp);

    return (read == 1) ? 0 : -1;
}

// Delete record (mark as inactive)
int delete_record(const char *filename, int index) {
    struct Record rec;
    if (load_record(filename, index, &rec) != 0) return -1;

    rec.active = 0;  // Mark as deleted
    return save_record(filename, index, &rec);
}

// Count total records
int count_records(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return 0;

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);

    return size / sizeof(struct Record);
}

Practical Applications

Application 1: Simple Database

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

#define MAX_NAME 50
#define DB_FILE "database.bin"

struct Student {
    int id;
    char name[MAX_NAME];
    float gpa;
    int valid;  // 1 = valid, 0 = deleted
};

// Find student by ID
int find_student(int id, struct Student *result) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) return -1;

    struct Student s;
    int index = 0;

    while (fread(&s, sizeof(struct Student), 1, fp) == 1) {
        if (s.valid && s.id == id) {
            *result = s;
            fclose(fp);
            return index;
        }
        index++;
    }

    fclose(fp);
    return -1;  // Not found
}

// Update student at specific index
int update_student(int index, const struct Student *s) {
    FILE *fp = fopen(DB_FILE, "r+b");
    if (fp == NULL) return -1;

    if (fseek(fp, index * sizeof(struct Student), SEEK_SET) != 0) {
        fclose(fp);
        return -1;
    }

    if (fwrite(s, sizeof(struct Student), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

Application 2: Index-Based File

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

// Index entry
struct IndexEntry {
    int key;
    long offset;  // Position in data file
};

// Data record (variable conceptually, but fixed in this example)
struct DataRecord {
    int id;
    char content[256];
};

// Build index for existing data file
int build_index(const char *data_file, const char *index_file) {
    FILE *data = fopen(data_file, "rb");
    FILE *idx = fopen(index_file, "wb");

    if (data == NULL || idx == NULL) {
        if (data) fclose(data);
        if (idx) fclose(idx);
        return -1;
    }

    struct DataRecord rec;
    struct IndexEntry entry;

    while (1) {
        entry.offset = ftell(data);  // Save position before reading

        if (fread(&rec, sizeof(struct DataRecord), 1, data) != 1) {
            break;
        }

        entry.key = rec.id;
        fwrite(&entry, sizeof(struct IndexEntry), 1, idx);
    }

    fclose(data);
    fclose(idx);
    return 0;
}

// Find record using index (binary search)
int find_by_index(const char *data_file, const char *index_file,
                  int key, struct DataRecord *result) {
    FILE *idx = fopen(index_file, "rb");
    if (idx == NULL) return -1;

    // Get number of index entries
    fseek(idx, 0, SEEK_END);
    long count = ftell(idx) / sizeof(struct IndexEntry);

    // Binary search
    long left = 0, right = count - 1;
    struct IndexEntry entry;

    while (left <= right) {
        long mid = (left + right) / 2;

        fseek(idx, mid * sizeof(struct IndexEntry), SEEK_SET);
        fread(&entry, sizeof(struct IndexEntry), 1, idx);

        if (entry.key == key) {
            fclose(idx);

            // Found! Now read data record
            FILE *data = fopen(data_file, "rb");
            if (data == NULL) return -1;

            fseek(data, entry.offset, SEEK_SET);
            fread(result, sizeof(struct DataRecord), 1, data);
            fclose(data);

            return 0;
        }

        if (entry.key < key) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    fclose(idx);
    return -1;  // Not found
}

Application 3: Log File with Rotation

#include <stdio.h>
#include <time.h>
#include <string.h>

#define MAX_LOG_SIZE (1024 * 1024)  // 1MB
#define LOG_FILE "app.log"

void rotate_log(void) {
    // Rename current log to backup
    rename(LOG_FILE, LOG_FILE ".bak");
}

void write_log(const char *message) {
    FILE *fp = fopen(LOG_FILE, "a+b");
    if (fp == NULL) return;

    // Check file size
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);

    if (size > MAX_LOG_SIZE) {
        fclose(fp);
        rotate_log();
        fp = fopen(LOG_FILE, "wb");
        if (fp == NULL) return;
    }

    // Write timestamp and message
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    char timestamp[32];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm);

    fprintf(fp, "[%s] %s\n", timestamp, message);
    fclose(fp);
}

Error Handling

Checking fseek Errors

if (fseek(fp, offset, SEEK_SET) != 0) {
    perror("fseek failed");
    // Handle error
}

Checking ftell Errors

long pos = ftell(fp);
if (pos == -1L) {
    perror("ftell failed");
    // Handle error
}

Validating Seek Position

int seek_to_record(FILE *fp, int record_num, size_t record_size) {
    // First, get file size
    long current = ftell(fp);
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);

    // Calculate target position
    long target = record_num * record_size;

    // Validate
    if (target < 0 || target >= file_size) {
        fseek(fp, current, SEEK_SET);  // Restore position
        return -1;  // Invalid position
    }

    // Seek to target
    if (fseek(fp, target, SEEK_SET) != 0) {
        return -1;
    }

    return 0;
}

Complete Error-Checked Read

int safe_read_record(const char *filename, int record_num,
                     void *buffer, size_t record_size) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        perror("Cannot open file");
        return -1;
    }

    // Get file size
    if (fseek(fp, 0, SEEK_END) != 0) {
        perror("Seek to end failed");
        fclose(fp);
        return -1;
    }

    long file_size = ftell(fp);
    if (file_size == -1L) {
        perror("ftell failed");
        fclose(fp);
        return -1;
    }

    // Calculate and validate position
    long position = record_num * record_size;
    if (position < 0 || position + record_size > file_size) {
        fprintf(stderr, "Record %d out of range\n", record_num);
        fclose(fp);
        return -1;
    }

    // Seek to record
    if (fseek(fp, position, SEEK_SET) != 0) {
        perror("Seek to record failed");
        fclose(fp);
        return -1;
    }

    // Read record
    if (fread(buffer, record_size, 1, fp) != 1) {
        if (feof(fp)) {
            fprintf(stderr, "Unexpected EOF\n");
        } else {
            perror("Read failed");
        }
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

Best Practices

1. Use Fixed-Size Records for Random Access

// Good: Fixed size, easy to calculate position
struct Record {
    int id;
    char name[50];    // Fixed size
    char padding[6];  // Explicit padding if needed
};

// Avoid: Variable size, requires index
struct Variable {
    int id;
    char *name;  // Pointer! Cannot be stored directly
};

2. Save and Restore Position When Needed

void peek_next_record(FILE *fp, struct Record *rec) {
    long saved_pos = ftell(fp);

    fread(rec, sizeof(struct Record), 1, fp);

    fseek(fp, saved_pos, SEEK_SET);  // Restore position
}

3. Use r+b for Update Operations

// Read and update existing file
FILE *fp = fopen("data.bin", "r+b");

// Read record
struct Record rec;
fread(&rec, sizeof(rec), 1, fp);

// Go back to record start
fseek(fp, -sizeof(rec), SEEK_CUR);

// Update and write
rec.value *= 2;
fwrite(&rec, sizeof(rec), 1, fp);

4. Always Check Return Values

if (fseek(fp, offset, SEEK_SET) != 0) {
    // Handle error
}

long pos = ftell(fp);
if (pos == -1L) {
    // Handle error
}

5. Flush After Random Writes

// After updating records randomly
fwrite(&rec, sizeof(rec), 1, fp);
fflush(fp);  // Ensure data is written

Common Pitfalls

Pitfall 1: Seeking in Text Mode

// PROBLEM: Seeking in text mode may not work as expected
FILE *fp = fopen("text.txt", "r");
fseek(fp, 100, SEEK_SET);  // May not land at byte 100

// SOLUTION: Use binary mode for random access
FILE *fp = fopen("text.txt", "rb");

Pitfall 2: Seeking Past End of File

// PROBLEM: Seeking past EOF creates sparse file on some systems
fseek(fp, 1000000, SEEK_SET);  // May create hole

// SOLUTION: Check file size first
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
if (desired_pos > size) {
    // Handle error or extend file properly
}

Pitfall 3: Forgetting to Seek After Read/Write Mix

// PROBLEM: Reading then writing without seek
fread(&rec, sizeof(rec), 1, fp);
fwrite(&rec, sizeof(rec), 1, fp);  // Writes at wrong position!

// SOLUTION: Seek before switching operations
fread(&rec, sizeof(rec), 1, fp);
fseek(fp, -sizeof(rec), SEEK_CUR);  // Go back
fwrite(&rec, sizeof(rec), 1, fp);   // Now correct

Pitfall 4: Integer Overflow in Position Calculation

// PROBLEM: Overflow with large files
int record_num = 1000000;
long pos = record_num * sizeof(struct Record);  // May overflow!

// SOLUTION: Cast to long first
long pos = (long)record_num * sizeof(struct Record);

Pitfall 5: Not Handling Negative Seek

// PROBLEM: Seeking before beginning
fseek(fp, -100, SEEK_SET);  // Undefined behavior!

// SOLUTION: Validate offset
long offset = -100;
if (offset < 0 && whence == SEEK_SET) {
    // Error: cannot seek before beginning
}

Complete Example: Mini Database System

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define DB_FILE "minidb.dat"
#define MAX_RECORDS 1000

struct Record {
    int id;
    char name[50];
    char email[50];
    time_t created;
    int deleted;  // Soft delete flag
};

// Get total record count
int get_record_count(void) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) return 0;

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);

    return size / sizeof(struct Record);
}

// Add new record
int add_record(const struct Record *rec) {
    FILE *fp = fopen(DB_FILE, "ab");
    if (fp == NULL) return -1;

    if (fwrite(rec, sizeof(struct Record), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

// Get record by index
int get_record(int index, struct Record *rec) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) return -1;

    long pos = index * sizeof(struct Record);
    if (fseek(fp, pos, SEEK_SET) != 0) {
        fclose(fp);
        return -1;
    }

    if (fread(rec, sizeof(struct Record), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

// Update record by index
int update_record(int index, const struct Record *rec) {
    FILE *fp = fopen(DB_FILE, "r+b");
    if (fp == NULL) return -1;

    long pos = index * sizeof(struct Record);
    if (fseek(fp, pos, SEEK_SET) != 0) {
        fclose(fp);
        return -1;
    }

    if (fwrite(rec, sizeof(struct Record), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

// Delete record (soft delete)
int delete_record(int index) {
    struct Record rec;
    if (get_record(index, &rec) != 0) return -1;

    rec.deleted = 1;
    return update_record(index, &rec);
}

// Find record by ID
int find_by_id(int id, struct Record *rec) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) return -1;

    int index = 0;
    while (fread(rec, sizeof(struct Record), 1, fp) == 1) {
        if (!rec->deleted && rec->id == id) {
            fclose(fp);
            return index;
        }
        index++;
    }

    fclose(fp);
    return -1;
}

// List all records
void list_records(void) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) {
        printf("No records found.\n");
        return;
    }

    struct Record rec;
    int index = 0;

    printf("%-5s %-6s %-20s %-30s %-20s\n",
           "Idx", "ID", "Name", "Email", "Created");
    printf("%-5s %-6s %-20s %-30s %-20s\n",
           "---", "--", "----", "-----", "-------");

    while (fread(&rec, sizeof(struct Record), 1, fp) == 1) {
        if (!rec.deleted) {
            char time_str[20];
            strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M",
                    localtime(&rec.created));

            printf("%-5d %-6d %-20s %-30s %-20s\n",
                   index, rec.id, rec.name, rec.email, time_str);
        }
        index++;
    }

    fclose(fp);
}

Summary

Random access file operations allow efficient manipulation of file data:

FunctionPurposeKey Usage
fseek()Move to positionfseek(fp, offset, SEEK_SET)
ftell()Get positionlong pos = ftell(fp)
rewind()Go to startrewind(fp)
fgetpos()Save positionfgetpos(fp, &pos)
fsetpos()Restore positionfsetpos(fp, &pos)

Key Points:

  1. •Use binary mode for reliable random access
  2. •Fixed-size records simplify position calculation
  3. •Always validate seek positions
  4. •Check return values for errors
  5. •Save/restore position when peeking
  6. •Use r+b mode for read-write operations

Random access is essential for implementing databases, file editors, and any application that needs to modify specific parts of large files efficiently.

README - C Programming Tutorial | DeepML