Files provide persistent data storage, unlike runtime variables that vanish when a program exits. The ability to persist data across program executions and retrieve it later is fundamental to most software applications. In C, file operations work through a unified interface centered around the concept of streams.
The Stream Abstraction
A stream is an abstraction that represents a flow of data between a program and an external device. Whether data moves to a disk file, the console, a printer, or network interface, C exposes all these destinations through a consistent API. The FILE type, defined in the standard library, represents a stream object. Each FILE object corresponds to an open stream and maintains state about the associated physical device.
typedef struct _IO_FILE FILE;
The standard library provides FILE * pointers for three pre-defined streams:
| Stream | Description |
|---|---|
stdin |
Standard input, typically keyboard |
stdout |
Standard output, typically screen |
stderr |
Standard error output, typically screen |
These streams require no explicit opening or closing—they are automatically available when a program starts.
File Access Functions
Opening and Closing Files
The fopen() function opens a file and associates it with a stream:
FILE *fopen(const char *filename, const char *mode);
File modes determine how the file is accessed:
| Mode | Description |
|---|---|
r |
Open text file for reading |
w |
Create or truncate to zero length for writing |
a |
Append; open or create for writing at end of file |
r+ |
Open for both reading and writing |
w+ |
Create for both reading and writing; truncate if exists |
a+ |
Open or create for reading and appending |
Binary file modes add a b suffix: rb, wb, ab, rb+, wb+, ab+.
Closing a file flushes any buffered data and releases resources:
int fclose(FILE *stream);
#include <stdio.h>
int main(void) {
FILE *handle = fopen("data.txt", "w");
if (handle == NULL) {
perror("Cannot open file");
return 1;
}
fprintf(handle, "Sample data: %d\n", 42);
fclose(handle);
return 0;
}
Path and Encoding Considerations: On Windows, fopen() uses the system ANSI code page by default. For paths containing non-ASCII characters, use the wide-character variant _wfopen():
FILE *handle = _wfopen(L"C:\\用户\\文档\\data.txt", L"r");
Unicode file handling with explicit encoding:
FILE *handle = fopen("utf8file.txt", "r, ccs=UTF-8");
Supported encodings include UTF-8, UTF-16LE, and UNICODE.
Character-Based I/O
Reading and Writing Individual Characters
int fgetc(FILE *stream); // Read one character
int fputc(int ch, FILE *stream); // Write one character
int getc(FILE *stream); // Often a macro version of fgetc
int putc(int ch, FILE *stream); // Often a macro version of fputc
The getchar() and putchar() functions operate on stdin and stdout respectively.
The ungetc() function pushes a character back onto the stream for subsequent reading:
int ungetc(int c, FILE *stream);
#include <stdio.h>
int main(void) {
FILE *src = fopen("input.txt", "r");
if (src == NULL) {
return 1;
}
int byte;
while ((byte = fgetc(src)) != EOF) {
putchar(byte);
}
fclose(src);
return 0;
}
#include <stdio.h>
int main(void) {
FILE *dest = fopen("output.txt", "w");
if (dest == NULL) {
return 1;
}
const char *text = "HELLO";
while (*text) {
fputc(*text++, dest);
}
fputc('\n', dest);
fclose(dest);
return 0;
}
For wide characters (required for multibyte characters like Chinece):
#include <stdio.h>
#include <wchar.h>
#include <locale.h>
int main(void) {
setlocale(LC_ALL, "zh_CN.GBK");
FILE *src = fopen("chinese_text.txt", "r, ccs=GBK");
if (src == NULL) {
return 1;
}
wint_t ch;
while ((ch = fgetwc(src)) != WEOF) {
putwchar(ch);
}
fclose(src);
return 0;
}
String-Based I/O
char *fgets(char *buffer, int n, FILE *stream);
int fputs(const char *str, FILE *stream);
fgets() reads up to n-1 characters, stopping at newline or EOF, and always terminates with a null character.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("log.txt", "w");
if (fp == NULL) return 1;
fputs("Log entry one\n", fp);
fputs("Log entry two\n", fp);
fclose(fp);
fp = fopen("log.txt", "r");
if (fp == NULL) return 1;
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
Block Data Transfer
For binary data structures or large amounts of data:
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
#include <stdio.h>
typedef struct {
int id;
double value;
char label[32];
} Record;
int main(void) {
Record data[3] = {
{1, 3.14, "alpha"},
{2, 2.71, "beta"},
{3, 1.41, "gamma"}
};
FILE *out = fopen("records.bin", "wb");
if (out == NULL) return 1;
fwrite(data, sizeof(Record), 3, out);
fclose(out);
Record read_back[3] = {0};
FILE *in = fopen("records.bin", "rb");
if (in == NULL) return 1;
size_t n = fread(read_back, sizeof(Record), 3, in);
fclose(in);
for (size_t i = 0; i < n; i++) {
printf("ID: %d, Value: %.2f, Label: %s\n",
read_back[i].id, read_back[i].value, read_back[i].label);
}
return 0;
}
File Positioning
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
The whence argument for fseek:
| Constant | Origin Point |
|---|---|
SEEK_SET |
Beginning of file |
SEEK_CUR |
Current position |
SEEK_END |
End of file |
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.bin", "r+b");
if (fp == NULL) return 1;
printf("Initial position: %ld\n", ftell(fp));
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
printf("File size: %ld bytes\n", file_size);
rewind(fp);
printf("After rewind: %ld\n", ftell(fp));
fseek(fp, 16, SEEK_SET);
printf("After seek to offset 16: %ld\n", ftell(fp));
fclose(fp);
return 0;
}
Formatted I/O
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
#include <stdio.h>
int main(void) {
FILE *fp = fopen("metrics.txt", "w");
if (fp == NULL) return 1;
fprintf(fp, "Count: %d\n", 100);
fprintf(fp, "Ratio: %.4f\n", 0.9375);
fprintf(fp, "Status: %s\n", "OK");
fclose(fp);
fp = fopen("metrics.txt", "r");
if (fp == NULL) return 1;
int count;
float ratio;
char status[32];
fscanf(fp, "Count: %d", &count);
fscanf(fp, "Ratio: %f", &ratio);
fscanf(fp, "Status: %s", status);
printf("Read: count=%d, ratio=%.4f, status=%s\n", count, ratio, status);
fclose(fp);
return 0;
}
Error Detection and Handling
int feof(FILE *stream); // Returns non-zero at end-of-file
int ferror(FILE *stream); // Returns non-zero if error occurred
void clearerr(FILE *stream); // Clear EOF and error indicators
void perror(const char *msg); // Print error message to stderr
#include <stdio.h>
int main(void) {
FILE *fp = fopen("nonexistent.dat", "r");
if (fp == NULL) {
perror("File open failed");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
if (feof(fp)) {
printf("\nEnd of file reached normally\n");
} else if (ferror(fp)) {
printf("\nRead error occurred\n");
clearerr(fp);
}
fclose(fp);
return 0;
}
Buffering
C's standard I/O library uses memory buffers to improve performance by reducing system calls.
Buffer Types
| Type | Flush Trigger | Typical Use |
|---|---|---|
| Unbuffered | Immediate, no buffering | Error streams (stderr) |
| Line buffered | Newline or buffer full | Terminal output |
| Fully buffered | Buffer full or explicit fflush |
Disk files |
Buffer Management
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
void setbuf(FILE *stream, char *buf);
int fflush(FILE *stream);
Buffer modes: _IONBF (unbuffered), _IOLBF (line buffered), _IOFBF (fully buffered).
#include <stdio.h>
int main(void) {
FILE *fp = fopen("buffered_output.txt", "w");
if (fp == NULL) return 1;
char custom_buffer[4096];
setvbuf(fp, custom_buffer, _IOFBF, sizeof(custom_buffer));
fputs("This string is buffered.\n", fp);
fflush(fp); // Force immediate write
fclose(fp);
return 0;
}
Stream Redirection with freopen
The freopen() function redirects an existing stream:
FILE *freopen(const char *filename, const char *mode, FILE *stream);
#include <stdio.h>
int main(void) {
FILE *original_stdin = stdin;
FILE *infile = freopen("commands.txt", "r", stdin);
if (infile == NULL) {
perror("Redirection failed");
return 1;
}
int value;
while (scanf("%d", &value) == 1) {
printf("Processing: %d\n", value);
}
freopen("error.log", "w", stderr);
fprintf(stderr, "Operation completed\n");
return 0;
}
Working with Binary File Formats
BMP Image Files
BMP files have a structured header followed by pixel data. Values are stored in little-endian byte order.
#include <stdio.h>
#include <stdint.h>
#pragma pack(push, 1)
typedef struct {
uint8_t signature[2]; // "BM"
uint32_t file_size;
uint16_t reserved1;
uint16_t reserved2;
uint32_t data_offset;
uint32_t header_size;
int32_t width;
int32_t height;
uint16_t planes;
uint16_t bits_per_pixel;
uint32_t compression;
uint32_t image_size;
int32_t x_pixels_per_meter;
int32_t y_pixels_per_meter;
uint32_t colors_used;
uint32_t important_colors;
} BMPHeader;
#pragma pack(pop)
uint32_t little_to_host(uint8_t *bytes, size_t len) {
uint32_t result = 0;
int started = 0;
for (int i = 0; i < (int)len; i++) {
if (!started && bytes[len - i - 1]) started = 1;
if (started) {
result = (result << 8) | bytes[len - i - 1];
}
}
return result;
}
int read_bmp_info(const char *path, BMPHeader *header) {
FILE *fp = fopen(path, "rb");
if (fp == NULL) return -1;
uint8_t raw[54];
if (fread(raw, sizeof(raw), 1, fp) != 1) {
fclose(fp);
return -2;
}
header->signature[0] = raw[0];
header->signature[1] = raw[1];
header->file_size = little_to_host(raw + 2, 4);
header->width = (int32_t)little_to_host(raw + 18, 4);
header->height = (int32_t)little_to_host(raw + 22, 4);
header->bits_per_pixel = little_to_host(raw + 28, 2);
header->data_offset = little_to_host(raw + 10, 4);
fclose(fp);
return 0;
}
DOCX and Office Documents
Modern Office formats (.docx, .xlsx, .pptx) are ZIP archives containing XML files. To work with them:
- Unzip the archive
- Extract and parse the XML content
- Repackage after modifications
Terminal Control Sequences
ANSI escape sequences control console formatting, colors, and cursor movement. All sequences begin with ESC (\x1b) followed by [.
Cursor Positioning
#define ESC "\x1b["
void move_cursor(int row, int col) {
printf(ESC"%d;%dH", row, col);
}
void move_up(int n) { printf(ESC"%dA", n); }
void move_down(int n) { printf(ESC"%dB", n); }
void move_right(int n) { printf(ESC"%dC", n); }
void move_left(int n) { printf(ESC"%dD", n); }
Cursor Visibility
void hide_cursor(void) { printf(ESC"?25l"); }
void show_cursor(void) { printf(ESC"?25h"); }
Text Formatting
void clear_screen(void) { printf(ESC"2J"); }
void clear_line_end(void) { printf(ESC"0K"); }
void clear_line_start(void){ printf(ESC"1K"); }
void clear_line(void) { printf(ESC"2K"); }
Text Colors (SGR Sequences)
#define RESET "0"
#define BOLD "1"
#define UNDERLINE "4"
// Foreground colors
#define FG_BLACK "30"
#define FG_RED "31"
#define FG_GREEN "32"
#define FG_YELLOW "33"
#define FG_BLUE "34"
#define FG_MAGENTA "35"
#define FG_CYAN "36"
#define FG_WHITE "37"
// Background colors
#define BG_BLACK "40"
#define BG_RED "41"
#define BG_GREEN "42"
#define BG_YELLOW "43"
#define BG_BLUE "44"
#define BG_MAGENTA "45"
#define BG_CYAN "46"
#define BG_WHITE "47"
void set_color(const char *fg, const char *bg) {
printf(ESC"%s;%sm", fg, bg);
}
void set_rgb_fg(int r, int g, int b) {
printf(ESC"38;2;%d;%d;%dm", r, g, b);
}
void set_rgb_bg(int r, int g, int b) {
printf(ESC"48;2;%d;%d;%dm", r, g, b);
}
int main(void) {
printf("Normal text\n");
printf(ESC"1;31m" "Bold red\n" ESC"0m");
printf(ESC"32;44m" "Green on blue\n" ESC"0m");
set_rgb_fg(255, 165, 0);
printf("Orange text\n");
printf(ESC"0m");
return 0;
}
Terminal Window Title
void set_window_title(const char *title) {
printf("\x1b]0;%s\x07", title);
}
Console Graphics
Combining RGB color control with BMP parsing enables rendering images in the terminal:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int width;
int height;
int offset;
unsigned char *pixels;
} ImageData;
void set_pixel_rgb(int r, int g, int b) {
printf("\x1b[48;2;%d;%d;%dm ", r, g, b);
fflush(stdout);
}
void reset_formatting(void) {
printf("\x1b[0m\n");
}
int load_bmp(const char *path, ImageData *img) {
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
unsigned char header[54];
fread(header, 54, 1, fp);
if (header[0] != 'B' || header[1] != 'M') {
fclose(fp);
return -2;
}
img->width = *(int*)(header + 18);
img->height = *(int*)(header + 22);
img->offset = *(int*)(header + 10);
fseek(fp, img->offset, SEEK_SET);
int row_size = img->width * 3;
int padding = (4 - (row_size % 4)) % 4;
int actual_row = row_size + padding;
unsigned char *raw = malloc(actual_row * img->height);
fread(raw, actual_row * img->height, 1, fp);
fclose(fp);
img->pixels = malloc(img->width * img->height * 3);
for (int y = img->height - 1; y >= 0; y--) {
for (int x = 0; x < img->width; x++) {
int src_idx = y * actual_row + x * 3;
int dst_idx = (img->height - 1 - y) * img->width * 3 + x * 3;
img->pixels[dst_idx] = raw[src_idx];
img->pixels[dst_idx + 1] = raw[src_idx + 1];
img->pixels[dst_idx + 2] = raw[src_idx + 2];
}
}
free(raw);
return 0;
}
void render_image(ImageData *img) {
printf("\x1b[?25l"); // Hide cursor
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
int idx = y * img->width * 3 + x * 3;
set_pixel_rgb(img->pixels[idx + 2],
img->pixels[idx + 1],
img->pixels[idx]);
}
reset_formatting();
}
printf("\x1b[?25h"); // Show cursor
}
This approach renders BMP images in the console by setting the background color of each character cell. For best results, configure the terminal with a small font size to approximate pixel-level control.
Summary of File API Functions
| Function | Purpose |
|---|---|
fopen |
Open file |
fclose |
Close file |
fgetc/fputc |
Read/write single character |
fgets/fputs |
Read/write string |
fread/fwrite |
Read/write blocks of data |
fprintf/fscanf |
Formatted I/O |
fseek |
Move file pointer |
ftell |
Get current position |
rewind |
Return to file beginning |
feof |
Check for end of file |
ferror |
Check for errors |
fflush |
Force write buffered data |
freopen |
Redirect stream |
setvbuf |
Configure buffering |