C File I/O with stdio.h
C provides file input and output facilities through the standard I/O library declared in stdio.h. These functions allow programs to create, open, read, write, append, seek, and close files.
Including stdio.h
#include <stdio.h>
The FILE Type
The standard I/O library represents an open stream with the FILE type. Programs normally use a FILE pointer to interact with an opened file.
FILE *file;
Opening a File with fopen
The fopen function opens a file and returns a pointer to a FILE object when successful. If the operation fails, it returns NULL.
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
perror("fopen");
return 1;
}
Closing a File
An opened stream should be closed with fclose when it is no longer needed.
if (fclose(file) != 0) {
perror("fclose");
}
File Opening Modes
| Mode | Purpose |
|---|---|
| r | Open an existing file for reading |
| w | Open for writing; create or truncate the file |
| a | Open for writing at the end; create if necessary |
| r+ | Open an existing file for reading and writing |
| w+ | Open for reading and writing; create or truncate |
| a+ | Open for reading and writing with writes at the end |
Binary Mode
The b character can be added to a mode to request binary mode.
FILE *file = fopen("image.dat", "rb");
Binary mode is particularly relevant on systems where text and binary streams have different handling.
Reading One Character
fgetc reads one character from a stream and returns it as an int. EOF is used to indicate end-of-file or an input error.
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
Why fgetc Returns int
The return type must be able to represent every possible unsigned char value as well as EOF. Therefore, storing fgetc's result directly in char can make EOF detection incorrect.
Writing One Character
if (fputc('A', file) == EOF) {
perror("fputc");
}
Reading a Line with fgets
fgets reads at most one fewer than the specified number of characters and terminates the resulting string with a null character when it successfully reads characters.
char buffer[256];
while (fgets(buffer, sizeof buffer, file) != NULL) {
printf("%s", buffer);
}
Writing a String with fputs
if (fputs("Hello, file!\n", file) == EOF) {
perror("fputs");
}
Formatted Output with fprintf
int id = 42;
const char *name = "Alice";
printf(file, "%d %s\n", id, name);
Formatted Input with fscanf
int id;
char name[50];
if (fscanf(file, "%d %49s", &id, name) == 2) {
printf("%d %s\n", id, name);
}
Why fscanf Can Be Tricky
Formatted input is convenient but whitespace handling and malformed input can make fscanf difficult to use for complex formats. For line-oriented input, fgets followed by explicit parsing is often easier to control.
Reading Binary Data with fread
fread reads a specified number of objects from a stream into memory.
int values[10];
size_t count = fread(values, sizeof values[0], 10, file);
printf("Read %zu elements\n", count);
Writing Binary Data with fwrite
int values[] = {10, 20, 30, 40};
size_t count = fwrite(values, sizeof values[0], 4, file);
if (count != 4) {
perror("fwrite");
}
Checking fread Results
A short fread result does not by itself tell you whether end-of-file or an input error occurred. Use feof and ferror after the operation.
size_t count = fread(buffer, 1, sizeof buffer, file);
if (count < sizeof buffer) {
if (feof(file)) {
printf("Reached end of file\n");
}
if (ferror(file)) {
perror("fread");
}
}
Checking End-of-File
feof reports whether the end-of-file indicator for a stream is set. It should generally be checked after a read operation rather than used as the loop condition before reading.
int ch;
while ((ch = fgetc(file)) != EOF) {
/* process ch */
}
if (ferror(file)) {
perror("read error");
}
Checking Stream Errors
ferror tests whether the error indicator for a stream is set.
if (ferror(file)) {
perror("file I/O error");
}
Clearing Error and EOF Indicators
clearerr clears both the error and end-of-file indicators associated with a stream.
clearerr(file);
File Position
Each stream maintains a file position that determines where the next read or write occurs.
ftell
ftell reports the current file position for a stream using type long.
long position = ftell(file);
if (position == -1L) {
perror("ftell");
}
fseek
fseek changes the file position according to an offset and a reference point.
if (fseek(file, 0L, SEEK_SET) != 0) {
perror("fseek");
}
SEEK_SET, SEEK_CUR, and SEEK_END
| Constant | Reference |
|---|---|
| SEEK_SET | Beginning of the file |
| SEEK_CUR | Current file position |
| SEEK_END | End of the file |
Rewinding a Stream
rewind moves the file position to the beginning and clears the stream's error and EOF indicators.
rewind(file);
Random Access
fseek allows programs to move around a file instead of processing it strictly from beginning to end.
if (fseek(file, 100L, SEEK_SET) == 0) {
int ch = fgetc(file);
if (ch != EOF) {
printf("Character: %c\n", ch);
}
}
Append Mode
Opening a stream with mode a causes writes to occur at the end of the file.
FILE *file = fopen("log.txt", "a");
if (file != NULL) {
fprintf(file, "New log entry\n");
fclose(file);
}
Truncating a File
Opening a file with w mode creates the file if necessary and truncates an existing file to zero length.
FILE *file = fopen("output.txt", "w");
Read-and-Write Modes
Modes containing + permit both reading and writing. When switching between reading and writing on an update stream, the program must follow the positioning and sequencing rules required by the C standard.
Text Files
Text streams are intended for textual data and may have implementation-specific translation behavior. Programs should use the standard stream functions rather than assuming a particular platform's line-ending representation.
Binary Files
Binary streams provide access to data without text-mode translations that an implementation may perform.
FILE *file = fopen("data.bin", "rb");
Writing Structures to Files
A structure can sometimes be written with fwrite for application-controlled binary files, but raw structure representations may contain padding and are not necessarily portable across different compilers, architectures, or program versions.
struct Record {
int id;
double value;
};
struct Record record = {1, 42.5};
fwrite(&record, sizeof record, 1, file);
For portable file formats, explicitly serialize individual fields using a defined representation.
File Serialization
Serialization converts program data into a defined external representation. A robust format should specify field sizes, byte order, encoding, versioning, and error behavior when portability matters.
Buffering
The standard I/O library commonly buffers stream operations. Buffering can improve performance by reducing the number of lower-level I/O operations.
fflush
fflush writes buffered output data for an output stream to the associated file or device.
fprintf(file, "Important data\n");
if (fflush(file) != 0) {
perror("fflush");
}
For input streams, the meaning and permitted behavior of fflush differ; do not treat fflush as a general input-buffer clearing function.
setvbuf
setvbuf can be used to control buffering for a stream, subject to the requirements of the C standard.
setvbuf(file, NULL, _IOFBF, BUFSIZ);
Standard Streams
C provides three predefined standard streams.
| Stream | Typical Purpose |
|---|---|
| stdin | Standard input |
| stdout | Standard output |
| stderr | Standard error output |
Writing to stderr
fprintf(stderr, "An error occurred\n");
Redirecting Standard Output
The shell can redirect stdout to a file without requiring the C program to open that file itself.
./program > output.txt
Checking fopen Errors
FILE *file = fopen("config.txt", "r");
if (file == NULL) {
perror("config.txt");
return EXIT_FAILURE;
}
Using strerror with errno
#include <errno.h>
#include <string.h>
#include <stdio.h>
FILE *file = fopen("missing.txt", "r");
if (file == NULL) {
fprintf(stderr, "Open failed: %s\n", strerror(errno));
}
File Removal
The remove function requests that a file be removed.
if (remove("temporary.txt") != 0) {
perror("remove");
}
Renaming a File
if (rename("old.txt", "new.txt") != 0) {
perror("rename");
}
Temporary Files
The standard library provides temporary-file facilities such as tmpfile. Platform-specific secure temporary-file creation should be used when stronger security properties are required.
FILE *file = tmpfile();
if (file == NULL) {
perror("tmpfile");
}
Opening a File Safely
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
perror("data.txt");
return EXIT_FAILURE;
}
char buffer[256];
while (fgets(buffer, sizeof buffer, file) != NULL) {
fputs(buffer, stdout);
}
if (ferror(file)) {
perror("read");
fclose(file);
return EXIT_FAILURE;
}
if (fclose(file) != 0) {
perror("fclose");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Writing a Text File
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
perror("output.txt");
return EXIT_FAILURE;
}
if (fprintf(file, "Hello from C\n") < 0) {
perror("fprintf");
fclose(file);
return EXIT_FAILURE;
}
if (fclose(file) != 0) {
perror("fclose");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Reading an Entire Binary Block
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("data.bin", "rb");
if (file == NULL) {
perror("data.bin");
return EXIT_FAILURE;
}
unsigned char buffer[1024];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof buffer, file)) > 0) {
/* process bytes */
}
if (ferror(file)) {
perror("fread");
fclose(file);
return EXIT_FAILURE;
}
fclose(file);
return EXIT_SUCCESS;
}
Determining File Size
For streams where seeking is supported, a common approach is to save the current position, seek to the end, obtain the position, and then restore the original position.
long current = ftell(file);
if (current == -1L) {
perror("ftell");
}
if (fseek(file, 0L, SEEK_END) != 0) {
perror("fseek");
}
long size = ftell(file);
if (size == -1L) {
perror("ftell");
}
if (fseek(file, current, SEEK_SET) != 0) {
perror("fseek");
}
This pattern is not universally suitable for every stream, and for large files or platform-specific requirements, APIs such as fseeko and ftello may be preferable where available.
Line Ending Considerations
Programs should avoid assuming that every platform represents a text newline using the same byte sequence. Standard text-stream functions abstract much of this difference.
File Position and Binary Data
For binary streams, fseek and ftell provide positioning semantics defined by the C standard, but portable code should not assume that arbitrary byte offsets have the same meaning for every kind of text stream.
Mixing Input Functions
Mixing functions such as fscanf, fgets, and fgetc requires understanding how each function consumes input. For example, fscanf may leave a newline in the stream after reading a numeric value.
int age;
char name[100];
fscanf(file, "%d", &age);
fgets(name, sizeof name, file);
The fgets call may immediately read the remaining newline. A line-oriented input strategy often avoids this class of problem.
A Line-Oriented Parsing Pattern
char line[256];
while (fgets(line, sizeof line, file) != NULL) {
/* parse line explicitly */
}
Handling Long Lines
If a line can exceed the buffer size, a single fgets call may not consume the entire line. Programs should detect incomplete lines and continue reading or use a dynamically growing buffer.
File Ownership
The code that successfully opens a FILE stream should have a clear responsibility for closing it. This makes resource management easier to reason about.
Common File I/O Pitfalls
- Failing to check whether fopen returned NULL
- Forgetting to fclose an opened stream
- Using the wrong file mode
- Accidentally truncating a file with w mode
- Assuming fgetc returns char instead of int
- Using feof as the loop condition before attempting a read
- Ignoring ferror after a failed or short read
- Assuming fgets always reads a complete line
- Using unsafe fscanf formats without field widths
- Assuming raw structure representations are portable file formats
- Ignoring fwrite or fprintf failures
- Using fflush as an input-buffer clearing mechanism
- Assuming every stream supports arbitrary seeking
- Mixing input functions without understanding whitespace consumption
Best Practices
- Always check fopen results
- Use the narrowest appropriate file mode
- Close every stream you successfully open
- Check important read and write return values
- Use fgets for predictable line-oriented input
- Use fgetc's int return value correctly
- Check ferror when a read stops unexpectedly
- Use binary mode when processing binary data
- Define portable serialization formats instead of dumping structures when portability matters
- Use explicit buffer sizes
- Keep file ownership clear
- Use temporary variables and careful error handling for multi-step file operations
Quick Reference
| Function | Purpose |
|---|---|
| fopen | Open a file stream |
| fclose | Close a file stream |
| fgetc | Read one character |
| fputc | Write one character |
| fgets | Read a line or character sequence |
| fputs | Write a string |
| fprintf | Formatted output to a stream |
| fscanf | Formatted input from a stream |
| fread | Read binary/object data |
| fwrite | Write binary/object data |
| fseek | Change file position |
| ftell | Get file position |
| rewind | Move to beginning and clear indicators |
| fflush | Flush buffered output |
| feof | Test end-of-file indicator |
| ferror | Test stream error indicator |
| clearerr | Clear EOF and error indicators |
| remove | Remove a file |
| rename | Rename a file |
Practice Exercises
- Write a program that opens a text file and prints every line
- Count the number of characters in a file using fgetc
- Count lines and words in a text file
- Copy one text file to another using fgets and fputs
- Copy a binary file using fread and fwrite
- Append timestamped messages to a log file
- Read integers from a file and calculate their sum
- Use fseek and ftell to inspect file positions
- Implement a program that reads a file in fixed-size chunks
- Detect whether a file read ended because of EOF or an error
- Create a simple binary record format
- Read and write a structure while considering portability issues
- Implement a line parser using fgets and sscanf
- Safely handle a file that contains lines longer than your input buffer
Conclusion
C's stdio library provides a flexible interface for both text and binary file operations. Functions such as fopen, fclose, fgets, fprintf, fread, fwrite, fseek, and ftell cover most basic file-processing needs.
Reliable file I/O requires more than simply calling these functions. Programs should check return values, distinguish EOF from errors, manage stream ownership carefully, select appropriate modes, and avoid assumptions about platform-specific file representations.