C / FILES AND ERRORS
fseek, ftell and navigating inside a file
Move a stream's cursor with fseek, read it back with ftell, and use them to size a file, jump to any record, and return to a saved position.
What you will learn
- Compute a file's size with fseek(f, 0, SEEK_END) followed by ftell(f).
- Pick SEEK_SET, SEEK_CUR or SEEK_END and know what the offset is measured from.
- Jump to record n in a fixed-size record file with fseek(f, n * sizeof rec, SEEK_SET).
- Use fseek to clear the EOF flag and to switch between reading and writing on r+ streams.
Understanding fseek, ftell and navigating inside a file
Every open FILE carries one number besides its buffer: the file position indicator, the offset of the next byte that will be read or written. Reading does not consume the file, it drags that offset forward by exactly the number of bytes transferred, which is why fgetc at the end of a 16-byte file leaves the offset at 16 rather than 17. fseek sets that offset to a place you choose and ftell hands you its current value, so together they turn a stream from a one-way trickle of bytes into random access over an array.
fseek takes an offset and a reference point: SEEK_SET counts from byte 0, SEEK_CUR from wherever the cursor is now, SEEK_END from one past the last byte, so fseek(f, -1, SEEK_END) lands on the final byte and fseek(f, 0, SEEK_END) lands where ftell reports the size. The offset is a long and may be negative with SEEK_CUR and SEEK_END, but asking to move before byte 0 is an error: fseek returns a nonzero value and the position stays put. Two side effects matter as much as the movement — a successful fseek clears the end-of-file indicator and discards any character pushed back with ungetc, and on a stream opened for update it is the positioning call the language requires between a write and the read that follows.
The catch is that offsets are plain byte counts only on binary streams. On a text stream the value ftell gives you is a cookie whose one guaranteed use is being handed back to fseek with SEEK_SET; where a newline is stored as two bytes the number will not match your count of characters, and arithmetic on it can drop the cursor mid-line. Not every stream can seek at all: when stdin is a pipe or a terminal, fseek fails with errno set to ESPIPE and ftell returns -1L, so those results deserve the same checking you give fopen.
<stdio.h>
int main(void)
{
const char *path = "digits.bin";
FILE *f;
long pos;
int c;
f = fopen(path, "wb");
if (f == NULL) {
perror(path);
return 1;
}
if (fwrite("0123456789ABCDEF", 1, 16, f) != 16) {
perror("fwrite");
fclose(f);
return 1;
}
fclose(f);
f = fopen(path, "rb");
if (f == NULL) {
perror(path);
return 1;
}
if (fseek(f, 0, SEEK_END) != 0) { /* one past the last byte */
perror("fseek");
fclose(f);
return 1;
}
printf("size: %ld bytes\n", ftell(f));
fseek(f, 10, SEEK_SET); /* absolute offset */
pos = ftell(f);
c = fgetc(f);
printf("offset %2ld holds '%c'\n", pos, c);
printf("the read moved the cursor to %ld\n", ftell(f));
fseek(f, -2, SEEK_CUR); /* two bytes back from here */
pos = ftell(f);
c = fgetc(f);
printf("offset %2ld holds '%c'\n", pos, c);
fseek(f, -1, SEEK_END); /* the final byte */
pos = ftell(f);
c = fgetc(f);
printf("offset %2ld holds '%c'\n", pos, c);
c = fgetc(f); /* nothing left to read */
printf("fgetc past the end returns %d, cursor still %ld, feof %d\n",
c, ftell(f), feof(f) != 0);
fseek(f, 0, SEEK_SET); /* a seek clears the EOF flag */
printf("after fseek, feof %d\n", feof(f) != 0);
pos = ftell(f);
c = fgetc(f);
printf("offset %2ld holds '%c'\n", pos, c);
fclose(f);
return 0;
}
A stream is a movable cursor over a sequence of bytes: fseek places the cursor, ftell reports where it sits, and every successful read or write shifts it by the bytes actually transferred.
Worked examples
Reading and patching record number 2
Turns a record index into a byte offset so one record can be read and rewritten without touching its neighbours.
<stdio.h>
<string.h>
struct Row {
int id;
char name[12];
};
int main(void)
{
struct Row rows[4] = { {1, "ada"}, {2, "brian"}, {3, "clara"}, {4, "dennis"} };
struct Row r;
long recsize = (long)sizeof(struct Row);
FILE *f;
f = fopen("rows.bin", "w+b");
if (f == NULL) {
perror("rows.bin");
return 1;
}
if (fwrite(rows, sizeof rows[0], 4, f) != 4) {
perror("fwrite");
fclose(f);
return 1;
}
fseek(f, 2 * recsize, SEEK_SET); /* straight to record 2 */
if (fread(&r, sizeof r, 1, f) != 1) {
perror("fread");
fclose(f);
return 1;
}
printf("record 2: %d %s\n", r.id, r.name);
r.id = 20; /* patch record 1 in place */
strcpy(r.name, "bjarne");
fseek(f, 1 * recsize, SEEK_SET);
fwrite(&r, sizeof r, 1, f);
fseek(f, 0, SEEK_SET);
while (fread(&r, sizeof r, 1, f) == 1)
printf("%d %s\n", r.id, r.name);
fseek(f, 0, SEEK_END);
printf("file: %ld bytes, record: %ld bytes\n", ftell(f), recsize);
fclose(f);
return 0;
}
Example explained
Line 1fseek(f, 2 * recsize, SEEK_SET) converts a record number into byte offset 32, so records 0 and 1 are never read.
Line 2The second fseek is needed because fread left the cursor at 48, and fwrite always starts writing wherever the cursor is.
Line 3Those seeks also satisfy the rule that a stream opened "w+b" needs a positioning call or fflush between a write and the next read.
Line 4ftell after fseek(f, 0, SEEK_END) reports 64, four records of 16 bytes, which is how you count records in a file you did not create.
Saving a line's position and coming back to it
Records the ftell value at the start of each line, then hands one of those values back to fseek to re-read that line directly.
<stdio.h>
int main(void)
{
FILE *f;
char buf[64];
long here, line3 = -1;
int line = 0;
f = fopen("notes.txt", "w+");
if (f == NULL) {
perror("notes.txt");
return 1;
}
fputs("alpha\nbeta\ngamma\ndelta\n", f);
rewind(f); /* seek to 0, and clear the error flag too */
while (1) {
here = ftell(f); /* where this line starts */
if (fgets(buf, sizeof buf, f) == NULL)
break;
line++;
if (line == 3)
line3 = here;
}
printf("line 3 begins at position %ld\n", line3);
if (fseek(f, line3, SEEK_SET) != 0) {
perror("fseek");
fclose(f);
return 1;
}
if (fgets(buf, sizeof buf, f) != NULL)
printf("re-read: %s", buf);
fclose(f);
return 0;
}
Example explained
Line 1ftell is called before fgets, because once fgets returns the cursor already sits at the start of the following line.
Line 211 is 6 bytes of "alpha\n" plus 5 of "beta\n" here, but on a system that stores a newline as two bytes the same program prints 12 — the number is a token for fseek, not a count to compute with.
Line 3The fgets that returned NULL set the end-of-file flag; the fseek clears it, which is why the last fgets can read anything at all.
Line 4rewind(f) is fseek(f, 0, SEEK_SET) plus a cleared error indicator, and it returns void, so it cannot tell you when it failed.
A portable bookmark with fgetpos and fsetpos
Marks a position without ever seeing it as a number, which is the portable way to save a spot in a text stream or a very large file.
<stdio.h>
int main(void)
{
FILE *f;
fpos_t mark;
int c;
f = fopen("data.txt", "w+");
if (f == NULL) {
perror("data.txt");
return 1;
}
fputs("abcdef", f);
rewind(f);
fgetc(f); /* skip 'a' */
fgetc(f); /* skip 'b' */
if (fgetpos(f, &mark) != 0) { /* remember this spot */
perror("fgetpos");
fclose(f);
return 1;
}
while ((c = fgetc(f)) != EOF)
putchar(c);
putchar('\n');
if (fsetpos(f, &mark) != 0) { /* jump back to the mark */
perror("fsetpos");
fclose(f);
return 1;
}
printf("after fsetpos, next byte is '%c'\n", fgetc(f));
fclose(f);
return 0;
}
Example explained
Line 1fpos_t is opaque: you cannot print it or add to it, only give it back to fsetpos, and that is exactly why it works where a long offset would not.
Line 2fgetpos and fsetpos both return 0 on success, so a nonzero result means this stream cannot be positioned and the program says so instead of reading garbage.
Line 3The while loop stops by setting the end-of-file flag; fsetpos clears it, so the following fgetc returns 'c' rather than EOF again.
Important notes
fseek and ftell work in long, which runs out near 2 GiB where long is 32 bits; for bigger files use POSIX fseeko and ftello with off_t, or fgetpos and fsetpos.
A successful fseek clears the end-of-file indicator but not the error indicator; after a read error you need clearerr, or rewind, which clears both.
Common mistakes
Doing arithmetic on a text-stream ftell value, such as seeking to pos + 20: on a platform that translates newlines the cursor lands mid-line and every field parsed after it is wrong.
Calling fseek on a stream opened "a" or "a+" and expecting the next write to overwrite that spot; append mode forces every write to the end, so the old bytes survive and the file simply grows.
Using ftell's result without checking it — on a pipe it is -1L, and passing that on as a file size yields a nonsense length or a huge allocation request.
Try it yourself
Change, predict, then run
Write the 26 bytes 'a' through 'z' to a file in binary mode, reopen it read-only, and print the alphabet backwards by starting at fseek(f, -1, SEEK_END) and stepping with fseek(f, -2, SEEK_CUR) after each fgetc. Let the loop end on the seek that would move before byte 0, and print that fseek's return value.
Open the C workspaceCheck your understanding
A file holds exactly 100 bytes. A program calls fseek(f, 0, SEEK_END), then fgetc(f) twice, then ftell(f). What does ftell report, and why?
- 100, because a read that transfers no bytes leaves the cursor where it was
- 102, because each fgetc advances the cursor whether or not it returns data
- -1, because ftell fails once the end-of-file indicator has been set
- 99, because SEEK_END places the cursor on the last byte rather than past it
Show answer
SEEK_END with offset 0 puts the cursor at 100, one past the last byte, so both fgetc calls find nothing: each sets the end-of-file indicator and returns EOF without moving anything, since the cursor advances by bytes actually transferred rather than by calls attempted. -1 is tempting because that really is ftell's failure value, but end-of-file is not an error on a seekable stream and ftell keeps answering correctly; it takes an fseek or clearerr to clear the flag.