Secara default, setiap operasi baca/tulis memajukan file position indicator satu arah—dari awal ke akhir file. Tetapi banyak skenario yang membutuhkan akses acak: kita ingin langsung lompat ke byte ke-1000, kembali ke awal, atau mengintip ukuran file tanpa membaca seluruh isinya. Tiga fungsi yang membantu kita di sini adalah fseek untuk memindahkan posisi, ftell untuk membaca posisi saat ini, dan rewind untuk kembali ke awal. Pada chapter ini kita akan mempraktikkan pola klasik mendapatkan ukuran file dengan fseek(SEEK_END) + ftell, lalu membaca data dari posisi mana pun yang kita mau.

Kontrak fseek, ftell, rewind #

ISO/IEC 9899 §7.21.9.2 dan §7.21.9.4 mendefinisikan:

c
int fseek(FILE *stream, long int offset, int whence);
long int ftell(FILE *stream);
void rewind(FILE *stream);

Kontrak penting:

  • fseek memindahkan file position indicator — argumen offset bisa positif (maju) atau negatif (mundur). Argumen whence menentukan titik acak: SEEK_SET (awal file), SEEK_CUR (posisi saat ini), atau SEEK_END (akhir file).
  • fseek mengembalikan 0 bila berhasil — nilai non-nol menandakan kegagalan (misalnya mencari ke posisi negatif).
  • ftell mengembalikan offset byte dari awal file — tipe long (biasanya 32-bit pada sistem 32-bit, 64-bit pada sistem 64-bit). Untuk file besar, gunakan ftello/fseeko dengan tipe off_t.
  • rewind setara dengan fseek(f, 0L, SEEK_SET) ditambah clearerr — mengembalikan posisi ke awal dan menghapus flag EOF serta error.

Pola Klasik: Mendapatkan Ukuran File #

Idiom paling terkenal untuk mendapatkan ukuran file tanpa membacanya adalah:

  1. fseek(f, 0L, SEEK_END) — lompat ke akhir file.
  2. size = ftell(f) — posisi saat ini sama dengan ukuran file.
  3. rewind(f) — kembali ke awal untuk operasi normal.
c
#include <stdio.h>

int main(void) {
    /* Buat file dengan teks known */
    FILE *f = fopen("/tmp/test_seek.txt", "w");
    if (!f) { perror("fopen"); return 1; }
    fputs("ABCDEFGHIJ\n", f);   /* 11 byte termasuk \n */
    fputs("KLMNOPQRST\n", f);   /* 11 byte */
    fputs("UVWXYZ01234\n", f);  /* 12 byte */
    fclose(f);
    /* Total: 34 byte */

    f = fopen("/tmp/test_seek.txt", "r");
    if (!f) { perror("fopen"); return 1; }

    /* Cara dapatkan ukuran file: fseek(SEEK_END) + ftell, lalu rewind */
    fseek(f, 0L, SEEK_END);
    long size = ftell(f);
    printf("Ukuran file: %ld byte\n", size);

    /* Baca dari posisi tertentu */
    fseek(f, 11L, SEEK_SET);   /* mulai dari baris kedua */
    char buf[16] = {0};
    fgets(buf, sizeof(buf), f);
    printf("Dari offset 11: \"%s\"", buf);

    /* SEEK_CUR: maju 5 byte dari posisi saat ini */
    long pos1 = ftell(f);
    fseek(f, 5L, SEEK_CUR);
    long pos2 = ftell(f);
    printf("Posisi sebelum: %ld, setelah SEEK_CUR+5: %ld\n", pos1, pos2);

    /* Baca sisa baris */
    char buf2[16] = {0};
    fgets(buf2, sizeof(buf2), f);
    printf("Dari offset %ld: \"%s\"", pos2, buf2);

    /* rewind: kembali ke awal */
    rewind(f);
    printf("Setelah rewind: posisi %ld\n", ftell(f));

    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Ukuran file: 34 byte
Dari offset 11: "KLMNOPQRST
"Posisi sebelum: 22, setelah SEEK_CUR+5: 27
Dari offset 27: "Z01234
"Setelah rewind: posisi 0

Perhatikan output di atas. fseek(f, 0L, SEEK_END) membawa kita ke akhir file, dan ftell melaporkan posisi 34—yang merupakan ukuran total file (11 + 11 + 12 = 34 byte). Setelah rewind, posisi kembali ke 0. Pada saat fgets membaca dari offset 11, kita mendapat "KLMNOPQRST\n" karena offset 11 adalah posisi awal baris kedua.

Mari kita telaah baris-baris kode di atas:

  1. Baris 22 (fseek(f, 0L, SEEK_END);): argumen offset adalah 0L (long zero) dan whence adalah SEEK_END. Ini memindahkan FPI ke akhir file—tepat setelah byte terakhir.
  2. Baris 23 (long size = ftell(f);): ftell mengembalikan posisi saat ini. Karena kita di akhir file, posisi sama dengan jumlah byte file.
  3. Baris 28 (fseek(f, 11L, SEEK_SET);): SEEK_SET berarti offset dihitung dari awal file. Jadi kita lompat ke byte ke-11 (byte pertama adalah A di posisi 0, byte ke-11 adalah K).
  4. Baris 30 (fgets(buf, sizeof(buf), f);): membaca dari posisi saat ini (offset 11) sampai \n atau sizeof(buf)-1 byte. Karena baris kedua berakhir di offset 22 dengan \n, fgets membaca 11 byte "KLMNOPQRST\n".
  5. Baris 35 (fseek(f, 5L, SEEK_CUR);): SEEK_CUR berarti offset relatif terhadap posisi saat ini. Setelah fgets selesai, posisi adalah 22 (setelah \n). Maju 5 byte membawa kita ke offset 27.
  6. Baris 41 (rewind(f);): setara dengan (void)fseek(f, 0L, SEEK_SET) plus clearerr(f). Berguna untuk membaca file dari awal tanpa harus menutup dan membuka kembali.

Random Access pada File Biner #

Akses acak paling berguna pada file biner berisi record berukuran tetap. Bayangkan file database berisi 1000 record Mahasiswa berukuran 40 byte setiap record. Untuk membaca record ke-500, kita tidak perlu membaca 499 record sebelumnya—cukup fseek ke offset 500 * 40 = 20000.

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

typedef struct {
    int  id;
    char nama[28];
    int  nilai;
} Record;

int main(void) {
    /* Tulis 5 record ke file */
    Record data[] = {
        {0, "Nol",    10},
        {1, "Satu",   20},
        {2, "Dua",    30},
        {3, "Tiga",   40},
        {4, "Empat",  50},
    };
    FILE *f = fopen("/tmp/test_records.bin", "wb");
    if (!f) { perror("fopen"); return 1; }
    fwrite(data, sizeof(Record), 5, f);
    fclose(f);

    /* Baca record ke-3 (index 2) secara langsung */
    f = fopen("/tmp/test_records.bin", "rb");
    if (!f) { perror("fopen"); return 1; }

    long offset = 2 * sizeof(Record);
    fseek(f, offset, SEEK_SET);

    Record rec;
    fread(&rec, sizeof(Record), 1, f);
    printf("Record ke-3: id=%d nama=%s nilai=%d\n",
           rec.id, rec.nama, rec.nilai);
    fclose(f);

    /* Update record ke-3 (overwrite). Buka dengan "r+b" untuk baca+tulis */
    f = fopen("/tmp/test_records.bin", "r+b");
    if (!f) { perror("fopen r+b"); return 1; }
    fseek(f, offset, SEEK_SET);
    rec.nilai = 99;
    fwrite(&rec, sizeof(Record), 1, f);
    fflush(f);

    /* Baca kembali untuk verifikasi */
    fseek(f, offset, SEEK_SET);
    Record rec2;
    fread(&rec2, sizeof(Record), 1, f);
    printf("Setelah update: id=%d nama=%s nilai=%d\n",
           rec2.id, rec2.nama, rec2.nilai);

    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Record ke-3: id=2 nama=Dua nilai=30
Setelah update: id=2 nama=Dua nilai=99

Pola fseek(offset) + fread/fwrite di atas adalah dasar implementasi database sederhana di C. Untuk membaca/menulis record ke-N, kita hitung offset sebagai N * sizeof(Record), lalu lompat langsung. Operasi ini berjalan dalam waktu konstan (O(1)) berkat dukungan sistem operasi—kita tidak perlu membaca semua record sebelumnya.

Batasan ftell dan ftello #

ftell mengembalikan tipe long yang pada beberapa sistem 32-bit hanya 32-bit—maksimum ~2 GB. Untuk file besar (> 2 GB), gunakan ftello dengan tipe off_t:

c
#define _FILE_OFFSET_BITS 64   /* sebelum #include */
#include <stdio.h>
#include <sys/types.h>

off_t pos = ftello(f);          /* bisa sampai 8 EB bila off_t 64-bit */
fseeko(f, (off_t)1234567890, SEEK_SET);

ftello dan fseeko adalah bagian dari POSIX.1-2001 dan tersedia di glibc Linux, macOS, dan sebagian besar sistem Unix modern.