What you'll learn
Files turn in-memory values into persistent byte sequences. Correct file code chooses a format, validates every operation, distinguishes end-of-file from failure, and closes every opened stream.
By the end, you'll be able to:
- Open text or binary streams with the correct mode
- Read and write complete records while checking short operations
- Navigate seekable streams and diagnose I/O failures
- Design cleanup paths and portable file formats
Program objects
struct · int · string
Byte sequence
text or binary format
Persistent file
closed and durable
Open & close streams
| Mode | Meaning |
|---|---|
r / rb | Read an existing text/binary file |
w / wb | Create or truncate for writing |
a / ab | Create or append at the end |
r+ / w+ / a+ | Permit both reading and writing with mode-specific behavior |
fopen returns a FILE * or NULL. A successfully opened stream must eventually reach fclose, whose failure matters for output because buffered bytes may be written during closing.
Text files
Text mode translates C characters to the platform's text representation. Use formatted or line-based functions and define a grammar that can reject malformed records.
#include <stdio.h>
int main(void) {
FILE *file = fopen("scores.txt", "w");
if (file == NULL) {
perror("scores.txt");
return 1;
}
if (fprintf(file, "%s,%d\n", "Maya", 93) < 0 ||
fprintf(file, "%s,%d\n", "Omar", 88) < 0) {
fprintf(stderr, "Could not write scores\n");
fclose(file);
return 1;
}
if (fclose(file) == EOF) {
perror("closing scores.txt");
return 1;
}
return 0;
}#include <stdio.h>
int main(void) {
FILE *file = fopen("scores.txt", "r");
if (file == NULL) {
perror("scores.txt");
return 1;
}
char line[100];
while (fgets(line, sizeof line, file) != NULL) {
char name[40];
int score;
if (sscanf(line, "%39[^,],%d", name, &score) == 2) {
printf("%-10s %d\n", name, score);
} else {
fprintf(stderr, "Skipped malformed line: %s", line);
}
}
int failed = ferror(file);
fclose(file);
return failed ? 1 : 0;
}Watch out
fprintf(file, user_text). Use fprintf(file, "%s", user_text)to prevent format-string vulnerabilities.Binary files
fread and fwrite transfer blocks and return the number of complete elements processed. Always compare that count with the request.
#include <stdint.h>
#include <stdio.h>
int main(void) {
uint32_t values[] = {10, 20, 30, 40};
size_t count = sizeof values / sizeof values[0];
FILE *file = fopen("values.bin", "wb");
if (file == NULL) return 1;
size_t written = fwrite(values, sizeof values[0], count, file);
if (written != count) {
fprintf(stderr, "Short write\n");
fclose(file);
return 1;
}
if (fclose(file) == EOF) return 1;
return 0;
}Note
Seek & position
Seekable streams support fseek, ftell, and rewind. Use SEEK_SET, SEEK_CUR, or SEEK_END rather than assuming numeric origin values.
#include <stdio.h>
int main(void) {
FILE *file = fopen("values.bin", "rb");
if (file == NULL) return 1;
if (fseek(file, 0, SEEK_END) != 0) {
fclose(file);
return 1;
}
long bytes = ftell(file);
if (bytes < 0) {
fclose(file);
return 1;
}
rewind(file);
printf("File size: %ld bytes\n", bytes);
fclose(file);
return 0;
}Watch out
ftell and seek back to those positions.Errors & end-of-file
A read function often uses one return value for both end-of-file and error. Attempt the read first, then use ferror to determine whether failure occurred.
#include <stdio.h>
int main(void) {
FILE *file = fopen("input.txt", "r");
if (file == NULL) return 1;
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
if (ferror(file)) {
perror("reading input.txt");
fclose(file);
return 1;
}
fclose(file);
return 0;
}Key idea
while (!feof(file)). End-of-file is set only after a read tries to go past the available input. Make the read operation the loop condition.Reliable cleanup
Multi-resource functions need one path that releases everything acquired so far. Initialize handles to null, acquire in order, and release in reverse order.
#include <stdio.h>
int copy_file(const char *source_path, const char *target_path) {
int result = 1;
FILE *source = NULL;
FILE *target = NULL;
unsigned char buffer[4096];
source = fopen(source_path, "rb");
if (source == NULL) goto cleanup;
target = fopen(target_path, "wb");
if (target == NULL) goto cleanup;
size_t count;
while ((count = fread(buffer, 1, sizeof buffer, source)) > 0) {
if (fwrite(buffer, 1, count, target) != count) goto cleanup;
}
if (ferror(source)) goto cleanup;
result = 0;
cleanup:
if (target != NULL && fclose(target) == EOF) result = 1;
if (source != NULL) fclose(source);
return result;
}Tip
Portable formats
- Define field order, encoding, delimiters, and escaping for text
- Define byte order and exact-width fields for binary data
- Include a magic value and format version when appropriate
- Validate lengths before allocating or reading payloads
- Reject trailing, truncated, contradictory, or unsupported data
Key idea
Recap & quick check
Key takeaways
- fopen returns a stream or NULL; every successful open needs a closing path.
- Read and write functions report completed work, which may be shorter than requested.
- Test ferror after a read loop to distinguish failure from normal end-of-file.
- Raw structure bytes are not a portable persistent format.
- Reliable code defines a format, validates it, and releases resources on every path.
Quick check
1. What does mode "w" do to an existing file?
2. Why is while (!feof(file)) incorrect?
3. What does fwrite return?
4. Why not persist raw structs as a portable format?
Your data can now persist safely. Next up: Module 14 — Function Pointers & Callbacks, where functions become configurable behavior.