C file positioning fseek ftell rewind random access guide
|

C File Positioning: fseek, ftell & rewind Explained (With Examples)

Sequential file reading — start at the beginning, read to the end — only gets you so far. What if you need to jump to record #500 in a database? Or read the last 100 bytes of a log file? Or update a single field in the middle of a file without rewriting everything? That’s where C file positioning comes in.

Building on what we learned in C File I/O Basics and Text vs Binary Files, this lesson covers the functions that let you move around within a file — reading and writing at arbitrary positions.

The File Position Indicator

Every open file has an invisible cursor called the file position indicator. It tracks where the next read or write operation will happen. When you open a file, this indicator starts at position 0 (the beginning). Each read or write advances it automatically.

// After opening, position is 0
FILE *fp = fopen("data.txt", "r");  // position: 0

char buf[10];
fread(buf, 1, 5, fp);   // reads 5 bytes, position: 5
fread(buf, 1, 3, fp);   // reads 3 bytes, position: 8

Think of it like a cursor in a text editor — it marks where you’re currently looking. The file positioning functions let you move this cursor anywhere you want.

ftell(): Where Am I?

ftell() returns the current position of the file indicator as a long value:

long ftell(FILE *stream);
// Returns current position, or -1L on error
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("example.txt", "r");
    if (!fp) return 1;
    
    printf("Start position: %ld\n", ftell(fp));  // 0
    
    char buf[20];
    fgets(buf, sizeof(buf), fp);
    printf("After first line: %ld\n", ftell(fp));
    
    fgets(buf, sizeof(buf), fp);
    printf("After second line: %ld\n", ftell(fp));
    
    fclose(fp);
    return 0;
}

For binary files, ftell() returns the exact byte offset from the beginning. For text files, the return value is meaningful only as an argument to fseek() — don’t try to do arithmetic with it in text mode, because newline translations can make the byte count unpredictable.

fseek(): Jump to Any Position

fseek() moves the file position indicator to a specific location:

int fseek(FILE *stream, long offset, int whence);
// Returns 0 on success, non-zero on error

The whence parameter specifies the reference point:

SEEK_SET  — offset from the BEGINNING of the file
SEEK_CUR  — offset from the CURRENT position
SEEK_END  — offset from the END of the file
// Jump to byte 100 from the beginning
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);

// Jump to the last byte of the file
fseek(fp, -1, SEEK_END);

// Jump to the beginning
fseek(fp, 0, SEEK_SET);

This is the function that transforms files from sequential streams into random-access storage. Combined with binary mode and fixed-size records, fseek() lets you jump directly to any record in O(1) time — no scanning needed.

rewind(): Back to the Start

rewind() is a convenience function that moves to the beginning and clears the error indicator:

void rewind(FILE *stream);

// Equivalent to:
fseek(fp, 0, SEEK_SET);
clearerr(fp);
// Process a file twice
FILE *fp = fopen("data.txt", "r");

// First pass: count lines
int lines = 0;
char buf[256];
while (fgets(buf, sizeof(buf), fp)) lines++;

// Go back to start
rewind(fp);

// Second pass: process each line
while (fgets(buf, sizeof(buf), fp)) {
    // now we know total is 'lines'
}

Getting File Size

A common trick uses fseek() and ftell() to determine a file’s size:

long get_file_size(const char *filename) {
    FILE *fp = fopen(filename, "rb");  // binary mode!
    if (!fp) return -1;
    
    fseek(fp, 0, SEEK_END);     // jump to end
    long size = ftell(fp);       // get position = file size
    fclose(fp);
    
    return size;
}

// Usage
long size = get_file_size("photo.jpg");
printf("File size: %ld bytes\n", size);

Note: always use binary mode for this. In text mode, ftell() may not return the actual byte count on Windows due to newline translation.

Random Access with Binary Files

The real power of file positioning shines with fixed-size binary records. If every record is the same size, you can calculate the exact byte offset of any record:

typedef struct {
    int id;
    char name[50];
    float score;
} Record;

// Jump to record N (0-indexed)
void seek_to_record(FILE *fp, int record_num) {
    long offset = (long)record_num * sizeof(Record);
    fseek(fp, offset, SEEK_SET);
}

// Read record N directly
int read_record(FILE *fp, int record_num, Record *r) {
    seek_to_record(fp, record_num);
    return fread(r, sizeof(Record), 1, fp) == 1 ? 0 : -1;
}

// Write record N directly
int write_record(FILE *fp, int record_num, const Record *r) {
    seek_to_record(fp, record_num);
    return fwrite(r, sizeof(Record), 1, fp) == 1 ? 0 : -1;
}

This gives you O(1) access to any record — the same principle that databases use internally. With C Structs of fixed size, you essentially get a simple database for free.

Updating Records In-Place

With "r+b" (read+write binary) mode, you can modify specific records without rewriting the entire file:

#include <stdio.h>

typedef struct {
    char name[32];
    int score;
} Player;

// Update a player's score without touching other records
int update_score(const char *filename, int player_idx, int new_score) {
    FILE *fp = fopen(filename, "r+b");  // read + write, binary
    if (!fp) return -1;
    
    // Seek to the record
    long offset = (long)player_idx * sizeof(Player);
    fseek(fp, offset, SEEK_SET);
    
    // Read current record
    Player p;
    if (fread(&p, sizeof(Player), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }
    
    // Modify
    p.score = new_score;
    
    // Seek BACK to the same position (fread advanced the cursor)
    fseek(fp, offset, SEEK_SET);
    
    // Write updated record
    fwrite(&p, sizeof(Player), 1, fp);
    
    fclose(fp);
    return 0;
}

The crucial detail: after reading a record, the file position advances past it. You must seek back to the record’s starting position before writing the update. Forgetting this step means you’ll overwrite the next record instead.

fgetpos() and fsetpos()

For very large files (larger than 2GB), ftell() returns a long which may not be big enough. The fgetpos()/fsetpos() pair uses an opaque fpos_t type that can handle any file size:

fpos_t pos;
fgetpos(fp, &pos);    // save current position

// ... do other operations ...

fsetpos(fp, &pos);    // restore saved position

Use fgetpos()/fsetpos() when you need to bookmark a position and return to it later. Use fseek()/ftell() when you need to calculate offsets arithmetically.

Practical Example: Fixed-Record Database

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

#define DB_FILE "records.db"

typedef struct {
    int id;
    char name[48];
    double balance;
    int active;
} Account;

long count_records(FILE *fp) {
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);
    return size / sizeof(Account);
}

void add_account(const char *name, double balance) {
    FILE *fp = fopen(DB_FILE, "ab");  // append binary
    if (!fp) { perror("add_account"); return; }
    
    // Calculate next ID
    long count = 0;
    FILE *rfp = fopen(DB_FILE, "rb");
    if (rfp) {
        count = count_records(rfp);
        fclose(rfp);
    }
    
    Account acc = {0};
    acc.id = (int)count + 1;
    strncpy(acc.name, name, 47);
    acc.balance = balance;
    acc.active = 1;
    
    fwrite(&acc, sizeof(Account), 1, fp);
    fclose(fp);
    printf("Account #%d created: %s ($%.2f)\n", acc.id, acc.name, acc.balance);
}

void list_accounts(void) {
    FILE *fp = fopen(DB_FILE, "rb");
    if (!fp) { printf("No accounts.\n"); return; }
    
    long total = count_records(fp);
    rewind(fp);
    
    printf("\n%-5s %-20s %12s %8s\n", "ID", "Name", "Balance", "Status");
    printf("%-5s %-20s %12s %8s\n", "---", "----", "-------", "------");
    
    Account acc;
    while (fread(&acc, sizeof(Account), 1, fp) == 1) {
        printf("%-5d %-20s $%11.2f %8s\n",
               acc.id, acc.name, acc.balance,
               acc.active ? "Active" : "Closed");
    }
    printf("Total: %ld accounts\n\n", total);
    fclose(fp);
}

int update_balance(int id, double new_balance) {
    FILE *fp = fopen(DB_FILE, "r+b");
    if (!fp) return -1;
    
    int idx = id - 1;  // 0-indexed
    long offset = (long)idx * sizeof(Account);
    fseek(fp, offset, SEEK_SET);
    
    Account acc;
    if (fread(&acc, sizeof(Account), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }
    
    acc.balance = new_balance;
    
    fseek(fp, offset, SEEK_SET);  // seek back!
    fwrite(&acc, sizeof(Account), 1, fp);
    fclose(fp);
    
    printf("Account #%d balance updated to $%.2f\n", id, new_balance);
    return 0;
}

int main(void) {
    // Demo
    add_account("Alice Johnson", 5000.00);
    add_account("Bob Smith", 12500.50);
    add_account("Carol Davis", 890.75);
    
    list_accounts();
    
    // Update Bob's balance
    update_balance(2, 15000.00);
    
    list_accounts();
    
    return EXIT_SUCCESS;
}

Common Mistakes

1. Using fseek() on Text Files with Calculated Offsets

// WRONG — text mode offsets aren't predictable
FILE *fp = fopen("data.txt", "r");
fseek(fp, 100, SEEK_SET);  // may not land where you expect

// OK — using a value from ftell()
long pos = ftell(fp);  // save position
// ... do stuff ...
fseek(fp, pos, SEEK_SET);  // restore position — this is fine

2. Forgetting to Seek Back Before Writing

// After fread(), position has moved past the record
fread(&record, sizeof(Record), 1, fp);
// DON'T write here — you'll overwrite the NEXT record!
fseek(fp, -sizeof(Record), SEEK_CUR);  // go back first
fwrite(&record, sizeof(Record), 1, fp);

3. Not Using Binary Mode for Random Access

// Always use binary mode for fseek()/ftell() arithmetic
FILE *fp = fopen("database.dat", "r+b");  // not "r+"

Summary

File positioning transforms files from sequential streams into random-access storage. ftell() tells you where you are, fseek() moves you anywhere, and rewind() brings you back to the start. Combined with fixed-size C Structs and binary mode, these functions let you build simple databases with O(1) record access. In the next lesson, we’ll cover error handling in C — how to write robust programs that fail gracefully.

Similar Posts

Leave a Reply

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