Reading and writing text files
Records, files and recursion13 min readLesson 4 of 6
4.1 A FILE handle
fopen(path, mode) asks the operating system for a stream and returns a FILE *. The pointer is a handle, and every later call uses it.
Modes: "r" reads an existing file, "w" creates or truncates, "a" appends, and adding b selects binary mode. Text mode on Windows translates \n to \r\n on write and back on read.
fopen returns NULL on failure: missing file, no permission, a bad path. Every program that opens a file checks the result before using the handle.
#include <stdio.h>
int main(void)
{
FILE *out = fopen("greeting.txt", "w");
if (out == NULL) {
printf("could not open for writing\n");
return 1;
}
fprintf(out, "hello file\n");
fclose(out);
printf("file written\n");
return 0;
}Output
file written
4.2 fprintf writes formatted text
fprintf(stream, format, ...) works like printf and sends the result to a stream. fputs(text, stream) writes a string without formatting, and fputc writes one character.
fprintf returns the number of characters written, or a negative value on error. Checking every write is tedious, so check ferror(stream) before closing.
Buffering means the bytes may not reach the disk immediately. fflush(stream) forces the buffer out, and fclose flushes as part of closing.
#include <stdio.h>
int main(void)
{
FILE *log = fopen("run.log", "w");
if (log == NULL) return 1;
for (int i = 1; i <= 3; i++) {
fprintf(log, "step %d complete\n", i);
}
if (ferror(log)) printf("write failed\n");
fclose(log);
printf("log closed\n");
return 0;
}Output
log closed
4.3 fgets reads a line at a time
fgets(buffer, size, stream) returns the buffer on success and NULL at end of file or on error. It keeps the newline, so strip it before comparing with other text.
A loop that reads until NULL handles files with or without a final newline. The buffer size caps one line, and a longer line arrives in pieces, which the missing newline reveals.
fgetc returns one character as an int, or EOF at the end. Keep the result in an int and compare it with EOF, because a char cannot hold every value the function may return.
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *out = fopen("notes.txt", "w");
if (out == NULL) return 1;
fputs("first line\nsecond line\nthird line\n", out);
fclose(out);
FILE *in = fopen("notes.txt", "r");
if (in == NULL) return 1;
char line[32];
int number = 0;
while (fgets(line, sizeof line, in) != NULL) {
line[strcspn(line, "\r\n")] = '\0';
printf("%d: %s\n", ++number, line);
}
fclose(in);
return 0;
}Output
1: first line 2: second line 3: third line
4.4 The open, use, close pattern
Every successful fopen needs a matching fclose. Closing releases the buffer and the operating system handle, and it reports errors that happened during buffered writes.
The clean shape is: open, check, work, close, return. Early returns between the open and the close leak handles, so initialise every FILE * to NULL and close it only when it is not null.
When several files are open, a single cleanup label handles every exit path. The heap cleanup pattern from the previous module applies here unchanged.
#include <stdio.h>
int main(void)
{
int status = 1;
FILE *a = fopen("a.txt", "w");
FILE *b = NULL;
if (a != NULL) b = fopen("b.txt", "w");
if (a != NULL && b != NULL) {
fprintf(a, "3\n");
fprintf(b, "4\n");
status = 0;
}
if (b != NULL) fclose(b);
if (a != NULL) fclose(a);
printf("status: %d\n", status);
return 0;
}Output
status: 0
4.5 Failing to open
A missing file is not an error in the language, it is a result. fopen returns NULL, and errno holds a code that describes why.
perror("open") prints that code in readable form after a label. The exact text depends on the platform and the locale, so a program that needs a stable message should print its own.
Returning a nonzero status from main lets scripts detect the failure. A diagnostic belongs on stderr, which keeps it out of piped output.
#include <stdio.h>
int main(void)
{
FILE *in = fopen("missing.txt", "r");
if (in == NULL) {
printf("could not open missing.txt\n");
return 1;
}
fclose(in);
printf("opened\n");
return 0;
}Output
could not open missing.txt
4.6 Appending and counting
Opening with "a" writes at the end without truncating. That suits logs and files that accumulate records across runs.
Appending then reopening for reading shows that the data survived. Counting lines means reading each one and incrementing a counter, or counting newlines with fgetc.
Text mode translation means the byte count on disk can differ from the character count on Windows, where each newline becomes two bytes. Counting characters read in text mode gives the number the program sees.
#include <stdio.h>
int main(void)
{
FILE *out = fopen("append.txt", "w");
if (out == NULL) return 1;
fputs("one\n", out);
fclose(out);
out = fopen("append.txt", "a");
if (out == NULL) return 1;
fputs("two\nthree\n", out);
fclose(out);
FILE *in = fopen("append.txt", "r");
if (in == NULL) return 1;
int lines = 0;
char buffer[32];
while (fgets(buffer, sizeof buffer, in) != NULL) lines++;
fclose(in);
printf("lines: %d\n", lines);
return 0;
}Output
lines: 3
4.7 One character at a time
fgetc reads one byte and returns it as an int, which leaves room for EOF. A loop that tests for EOF handles text and binary data the same way.
fread and fwrite move blocks and return counts of complete items. A short count means the file ended or an error occurred, and feof or ferror tells you which.
Binary mode with "rb" and "wb" turns off newline translation. Use it for anything that is not text, and use text mode for records you can read in an editor.
#include <stdio.h>
int main(void)
{
FILE *out = fopen("count.txt", "w");
if (out == NULL) return 1;
fputs("count these\ntwo lines\n", out);
fclose(out);
FILE *in = fopen("count.txt", "r");
if (in == NULL) return 1;
int chars = 0, lines = 0, ch;
while ((ch = fgetc(in)) != EOF) {
chars++;
if (ch == '\n') lines++;
}
fclose(in);
printf("%d chars, %d lines\n", chars, lines);
return 0;
}Output
22 chars, 2 lines
