In C programming, interacting with files and external devices is fundamental. The C Standard Library provides a powerful set of functions for Input/Output (I/O) operations, primarily centered around the concept of streams. This post will walk you through various aspects of C file I/O, using a comprehensive set of demo functions to illustrate core concepts.
Understanding File Streams (FILE *
)
At the heart of C’s I/O is the FILE
pointer. A FILE
object (pointed to by a FILE *
) represents a stream, which is an abstract representation of a data source or destination. It could be a physical file on disk, a printer, a network connection, or even your keyboard/screen.
Let’s start with a basic demonstration of opening and writing to a text file.
intro_to_streams()
: Your First File Interaction
// For binary data
typedef struct {
int id;
char name[50];
float score;
} Student; // Defined globally for all demos
void intro_to_streams() {
printf("\n--- FILE * Streams and Standard I/O ---\n");
FILE *fp; // FILE is a type that represents a stream
fp = fopen("textfile.txt", "w"); // Open "textfile.txt" in write mode
if (!fp) { // Check if file opened successfully
perror("Error opening file"); // Print error message
return;
}
fprintf(fp, "Hello, world!\n"); // Write formatted string to the file stream
fclose(fp); // Close the file stream
}
Explanation:
FILE *fp;
: Declares a pointerfp
of typeFILE *
. This pointer will be used to refer to our opened file stream.fp = fopen("textfile.txt", "w");
: Thefopen()
function attempts to open the file “textfile.txt”.- The
"w"
mode means “write”. If the file exists, its content is truncated (emptied). If it doesn’t exist, it’s created.
- The
if (!fp)
: It’s crucial to always check the return value offopen()
. If it returnsNULL
, the file could not be opened (e.g., due to permissions, invalid path).perror()
provides a system-specific error message.fprintf(fp, "Hello, world!\n");
:fprintf()
is used to write formatted output to a specifiedFILE
stream. Here, it writes “Hello, world!” followed by a newline totextfile.txt
.fclose(fp);
: This is vital!fclose()
closes the file stream, flushing any buffered data to disk and releasing system resources associated with the file. Failing to close files can lead to data loss or resource leaks.
Standard I/O Streams: stdin
, stdout
, stderr
C provides three pre-defined standard streams that are automatically available to every C program:
stdin
: Standard input, typically the keyboard.stdout
: Standard output, typically the console/terminal.stderr
: Standard error, also typically the console/terminal, used for error messages.
std_streams_demo()
: Interacting with Console
void std_streams_demo() {
fprintf(stdout, "\n--- Standard Output ---\n"); // Write to standard output
fprintf(stderr, "--- Standard Error ---\n"); // Write to standard error
char input[100];
printf("Enter your name: "); // Convenience function for fprintf(stdout, ...)
fgets(input, sizeof(input), stdin); // Read from standard input
printf("Hello, %s", input);
}
Explanation:
fprintf(stdout, ...)
andfprintf(stderr, ...)
: These demonstrate writing explicitly to the standard output and error streams. You’ll usually seeprintf()
used forstdout
andperror()
or customfprintf(stderr, ...)
for error messages.printf()
is actually just a wrapper aroundfprintf(stdout, ...)
.printf("Enter your name: ");
: A common function for printing tostdout
.fgets(input, sizeof(input), stdin);
:fgets()
is used to read a line of input.input
: The buffer to store the read string.sizeof(input)
: The maximum number of characters to read (including the null terminator, preventing buffer overflows).stdin
: Specifies to read from standard input (the keyboard).
printf("Hello, %s", input);
: Prints the user’s input back tostdout
. Note thatfgets
includes the newline character if present, so the output will automatically move to the next line.
File Buffering
File I/O operations are often buffered for efficiency. Instead of writing/reading every byte to/from disk immediately (which is slow), data is accumulated in a temporary memory area (a buffer) and then transferred in larger blocks.
- Full Buffering (
_IOFBF
): Data is written/read only when the buffer is full. - Line Buffering (
_IOLBF
): Data is written/read when a newline character is encountered or the buffer is full.stdout
is typically line-buffered when connected to a terminal. - No Buffering (
_IONBF
): Data is written/read immediately.stderr
is typically unbuffered.
You can explicitly control buffering with setvbuf()
.
buffering_demo()
: Customizing Buffering
void buffering_demo() {
printf("\n--- Buffering Demo ---\n");
FILE *fp = fopen("buffering.txt", "w");
if (!fp) {
perror("Failed to open file");
return;
}
char buf[BUFSIZ]; // Declare a buffer of system-optimal size
setvbuf(fp, buf, _IOFBF, BUFSIZ); // Set to full buffering using our custom buffer
fputs("This is a buffered line.\n", fp);
// Data is now in 'buf', not yet written to disk.
// To ensure it's written immediately, we need to flush or close.
fflush(fp); // Forces the buffer to be written to disk
fclose(fp); // Also flushes before closing
}
Explanation:
char buf[BUFSIZ];
: Declares a character arraybuf
of sizeBUFSIZ
.BUFSIZ
is a macro defined in<stdio.h>
that provides a recommended buffer size.setvbuf(fp, buf, _IOFBF, BUFSIZ);
: This function explicitly sets the buffering mode forfp
.fp
: The file stream.buf
: The custom buffer to use. IfNULL
is passed,setvbuf
allocates its own buffer._IOFBF
: Specifies full buffering.BUFSIZ
: The size of the provided buffer.
fputs("This is a buffered line.\n", fp);
: Writes the string to the stream. Because of full buffering, this string typically sits in thebuf
array until it’s full or flushed.fflush(fp);
: This function forces any buffered output data for the streamfp
to be written to the underlying file immediately. This is crucial if you need data to be persistent on disk before the file is closed or if other processes need to see the data.
Reading and Writing from the Same Stream
To read and write to the same file, you need to open it in a read/write mode (e.g., "r+"
, "w+"
, "a+"
). Switching between reading and writing requires repositioning the file pointer.
rw_stream_demo()
: Read/Write with Repositioning
void rw_stream_demo() {
printf("\n--- Read/Write Stream ---\n");
FILE *fp = fopen("rwfile.txt", "w+"); // Open in write/read mode
if (!fp) {
perror("Open failed");
return;
}
fputs("Line 1\nLine 2\nLine 3\n", fp); // Write data
// At this point, the file pointer is at the end of the written data.
rewind(fp); // Move the file pointer back to the beginning of the file
char line[100];
while (fgets(line, sizeof(line), fp)) { // Read line by line
printf("Read: %s", line);
}
fclose(fp);
}
Explanation:
fopen("rwfile.txt", "w+");
: Opens “rwfile.txt” inw+
mode. This mode truncates the file to zero length (if it exists) or creates it (if it doesn’t), and allows both reading and writing. The file pointer starts at the beginning.fputs("Line 1\nLine 2\nLine 3\n", fp);
: Writes three lines. After this, the file pointer is at the end of the file.rewind(fp);
: This function sets the file position indicator for the streamfp
back to the beginning of the file. Withoutrewind()
, the subsequentfgets()
calls would try to read from the end of the file and immediately hit EOF.while (fgets(line, sizeof(line), fp))
: Reads the lines one by one and prints them tostdout
.
Line-at-a-Time Input: getline
(POSIX)
While fgets
is standard, getline
(a POSIX standard function, often available on Linux/macOS but not guaranteed on all Windows compilers without specific libraries) offers a safer and more convenient way to read entire lines without worrying about buffer size, as it dynamically allocates memory.
line_by_line_io()
: Dynamic Line Reading
void line_by_line_io() {
printf("\n--- Line-at-a-Time I/O ---\n");
FILE *fp = fopen("lines.txt", "w+");
if (!fp) {
perror("open failed");
return;
}
fputs("Alpha\nBeta\nGamma\n", fp);
rewind(fp);
char *line = NULL; // Must be NULL initially for getline
size_t len = 0; // Must be 0 initially for getline
ssize_t read; // To store the number of characters read
while ((read = getline(&line, &len, fp)) != -1) {
printf("Line (%zu chars): %s", read, line); // %zu for size_t
}
free(line); // Free the memory allocated by getline
fclose(fp);
}
Explanation:
char *line = NULL; size_t len = 0;
:getline
expectsline
to beNULL
andlen
to be0
on the first call. It will then allocate memory forline
as needed. For subsequent calls, if the buffer isn’t large enough,getline
will reallocateline
and updatelen
.ssize_t read;
:getline
returns the number of characters read, or-1
on EOF/error.ssize_t
is a signed size type, useful for accommodating-1
.while ((read = getline(&line, &len, fp)) != -1)
: The loop continues as long asgetline
successfully reads a line. Note the&line
becausegetline
modifies the pointer itself.printf("Line (%zu chars): %s", read, line);
: Prints the line and the number of characters read.%zu
is the format specifier forsize_t
andssize_t
(which is typically atypedef
forlong int
orlong long int
but is compatible with%zu
for printing sizes).free(line);
: Crucial! Sincegetline
allocates memory, you mustfree()
it when you are done to prevent memory leaks.
Binary I/O
For structured data (like our Student
struct), writing and reading in binary format can be more efficient and precise than converting to text.
binary_io_demo()
: Storing Structs in Files
void binary_io_demo() {
printf("\n--- Binary I/O ---\n");
FILE *fp = fopen("students.dat", "wb"); // Open in write binary mode
if (!fp) {
perror("write open failed");
return;
}
Student s1 = {1, "Alice", 95.5};
Student s2 = {2, "Bob", 88.2};
// Write binary data of s1 and s2 to the file
fwrite(&s1, sizeof(Student), 1, fp); // Write 1 item of sizeof(Student) from &s1
fwrite(&s2, sizeof(Student), 1, fp); // Write 1 item of sizeof(Student) from &s2
fclose(fp);
// Reading it back
fp = fopen("students.dat", "rb"); // Open in read binary mode
if (!fp) {
perror("read open failed");
return;
}
Student temp; // Buffer to read into
while (fread(&temp, sizeof(Student), 1, fp)) { // Read 1 item of sizeof(Student) into &temp
// fread returns the number of items successfully read (0 on EOF/error)
printf("ID: %d, Name: %s, Score: %.2f\n", temp.id, temp.name, temp.score);
}
fclose(fp);
}
Explanation:
fopen("students.dat", "wb");
: Opens “students.dat” in write-binary mode. The ‘b’ is important for systems that distinguish text and binary modes (e.g., Windows), preventing newline translations.fwrite(&s1, sizeof(Student), 1, fp);
:&s1
: Address of theStudent
object to write.sizeof(Student)
: Size of oneStudent
object in bytes.1
: Number ofStudent
objects to write (in this case, one).fp
: The file stream.
fopen("students.dat", "rb");
: Opens the same file in read-binary mode.while (fread(&temp, sizeof(Student), 1, fp))
:&temp
: Address of theStudent
object to read into.sizeof(Student)
: Size of oneStudent
object.1
: Number ofStudent
objects to read (one at a time).fp
: The file stream.fread
returns the number of items successfully read. If it returns0
, it means EOF was reached or an error occurred.
Stream Positioning: ftell
, fseek
, rewind
You can control the current reading/writing position within a stream.
ftell()
: Returns the current file position indicator.fseek()
: Sets the file position indicator.rewind()
: Sets the file position indicator to the beginning of the file.
stream_positioning()
: Manipulating File Pointer
void stream_positioning() {
printf("\n--- Stream Positioning ---\n");
FILE *fp = fopen("seekfile.txt", "w+"); // read/write mode
if (!fp) {
perror("seek open failed");
return;
}
fputs("abcdefghij", fp); // Write 10 characters
fflush(fp); // Ensure data is written to file before seeking/reading from it
fseek(fp, 3, SEEK_SET); // Move 3 bytes from the beginning (to 'd')
fputc('X', fp); // Overwrite 'd' with 'X'
fflush(fp); // Flush the 'X' to disk
rewind(fp); // Go back to the very beginning
char ch;
while ((ch = fgetc(fp)) != EOF) { // Read and print character by character
putchar(ch);
}
printf("\n"); // Add newline for clean output
fclose(fp);
}
Explanation:
fputs("abcdefghij", fp);
: Writes a string of 10 characters. File pointer is at position 10.fflush(fp);
: Important after writing and before changing mode or seeking, especially if reading immediately after writing, to ensure the written data is available on disk.fseek(fp, 3, SEEK_SET);
:fp
: The file stream.3
: The offset in bytes.SEEK_SET
: The origin for the offset.SEEK_SET
means from the beginning of the file. Other options areSEEK_CUR
(from current position) andSEEK_END
(from end of file).- This moves the file pointer to the 4th character (index 3).
fputc('X', fp);
: Writes ‘X’ at the current position, overwriting ’d'. The file now conceptually contains “abcXefghij”.rewind(fp);
: Resets the file pointer to the very beginning.while ((ch = fgetc(fp)) != EOF) { putchar(ch); }
: Reads the modified file character by character and prints it to the console. You’ll see “abcXefghij” printed.
Efficient Block I/O (fread
/fwrite
for large files)
For very large files, reading and writing character by character or line by line can be inefficient. Using fread
and fwrite
to transfer data in larger blocks (e.g., 4KB, 8KB) is significantly more performant.
efficient_io_demo()
: Copying Large Files Efficiently
void efficient_io_demo() {
printf("\n--- Efficient Block I/O ---\n");
FILE *src = fopen("bigfile.txt", "w+"); // Source file
if (!src) {
perror("open failed");
return;
}
// Fill with large data (10000 lines)
for (int i = 0; i < 10000; ++i)
fprintf(src, "Line %d\n", i);
fflush(src);
rewind(src);
FILE *dst = fopen("bigfile_copy.txt", "w"); // Destination file
if (!dst) {
perror("copy open failed");
fclose(src); // Always close opened files on error
return;
}
char buffer[4096]; // 4KB block buffer
size_t bytes; // To store the number of bytes read/written in a block
// Read blocks from src and write to dst
while ((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0) {
// fread returns the number of items read. Here, items are 1-byte blocks.
fwrite(buffer, 1, bytes, dst);
// fwrite writes 'bytes' number of 1-byte items from 'buffer' to 'dst'.
}
fclose(src); // Close both files
fclose(dst);
printf("bigfile.txt copied to bigfile_copy.txt efficiently.\n");
}
Explanation:
- Creating a Large Source File: The code first generates a “bigfile.txt” with 10,000 lines to simulate a large file.
char buffer[4096];
: A buffer of 4096 bytes (4KB) is declared. This will be used to hold chunks of data.while ((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0)
:fread(buffer, 1, sizeof(buffer), src)
: Attempts to readsizeof(buffer)
(4096) items, each 1 byte in size, fromsrc
intobuffer
.bytes
stores the actual number of bytes read. This is important because the last read might be less thansizeof(buffer)
if it hits EOF.- The loop continues as long as
fread
reads at least one byte.
fwrite(buffer, 1, bytes, dst);
: Writes thebytes
number of characters that were just read fromsrc
intodst
using the same block size.
This pattern is a very common and efficient way to copy files or process large amounts of data in chunks.
C Standard I/O Functions: A Quick Reference
Here’s a table summarizing the core C I/O functions demonstrated and their characteristics:
C Function | Header File | Usage / Purpose | Typical Look in <stdio.h> |
Notes |
---|---|---|---|---|
fopen |
<stdio.h> |
Opens a file, returns a FILE * stream. |
FILE *fopen(const char *filename, const char *mode); |
mode examples: "r" (read), "w" (write, truncate), "a" (append), "r+" (read/write), "w+" (write/read, truncate), "rb" (read binary), etc. Returns NULL on failure. |
fclose |
<stdio.h> |
Closes an open file stream. | int fclose(FILE *stream); |
Flushes buffered data, releases resources. Returns 0 on success, EOF on error. |
fprintf |
<stdio.h> |
Writes formatted output to a FILE * stream. |
int fprintf(FILE *stream, const char *format, ...); |
Similar to printf , but writes to a specified stream. The stream argument is first due to its variadic nature. |
printf |
<stdio.h> |
Writes formatted output to stdout . |
int printf(const char *format, ...); |
Equivalent to fprintf(stdout, format, ...) . Convenient for console output. |
fputs |
<stdio.h> |
Writes a string to a FILE * stream. |
int fputs(const char *str, FILE *stream); |
Does not automatically append a newline. Returns non-negative on success, EOF on error. |
fgets |
<stdio.h> |
Reads a line from a FILE * stream. |
char *fgets(char *str, int size, FILE *stream); |
Reads up to size-1 chars or until newline/EOF. Includes newline character if read. Always null-terminates the string. Returns str on success, NULL on EOF/error. Safer than gets() . |
fgetc |
<stdio.h> |
Reads a single character from a FILE * stream. |
int fgetc(FILE *stream); |
Returns the character (as an int ) or EOF on end-of-file or error. |
fputc |
<stdio.h> |
Writes a single character to a FILE * stream. |
int fputc(int char_to_write, FILE *stream); |
Writes the character char_to_write to the specified stream. Returns the character written or EOF on error. |
putchar |
<stdio.h> |
Writes a single character to stdout . |
int putchar(int char_to_write); |
Equivalent to fputc(char_to_write, stdout) . More efficient for single character output to console than printf("%c", ...) . |
setvbuf |
<stdio.h> |
Controls buffering for a stream. | int setvbuf(FILE *stream, char *buf, int mode, size_t size); |
Must be called after fopen but before any other I/O. mode : _IOFBF , _IOLBF , _IONBF . buf can be NULL for auto-allocation. |
fflush |
<stdio.h> |
Flushes a stream’s buffer. | int fflush(FILE *stream); |
Forces buffered data to be written to the underlying file. Useful before switching between read/write on w+ files, or ensuring data persistence. |
rewind |
<stdio.h> |
Resets stream position to beginning. | void rewind(FILE *stream); |
Equivalent to (void)fseek(stream, 0L, SEEK_SET) . Also clears error and EOF indicators. |
ftell |
<stdio.h> |
Returns current file position. | long int ftell(FILE *stream); |
Returns the current offset in bytes from the beginning of the file, or -1L on error. |
fseek |
<stdio.h> |
Sets the file position indicator. | int fseek(FILE *stream, long int offset, int origin); |
offset : number of bytes to move. origin : SEEK_SET (start), SEEK_CUR (current), SEEK_END (end). Returns 0 on success, non-zero on error. |
fread |
<stdio.h> |
Reads blocks of binary data from a stream. | size_t fread(void *ptr, size_t size, size_t count, FILE *stream); |
Reads count items, each of size bytes, into ptr . Returns the number of items successfully read (may be less than count or 0 on EOF/error). Ideal for binary data (structs). |
fwrite |
<stdio.h> |
Writes blocks of binary data to a stream. | size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream); |
Writes count items, each of size bytes, from ptr . Returns the number of items successfully written. Ideal for binary data. |
getline |
<stdio.h> |
Reads an entire line (POSIX). | ssize_t getline(char **lineptr, size_t *n, FILE *stream); |
Dynamically allocates buffer. lineptr and n should be NULL and 0 on first call. Returns number of characters read or -1 on EOF/error. Requires free(*lineptr) . Not standard C, but widely available on Unix-like systems. |
perror |
<stdio.h> |
Prints a system error message. | void perror(const char *s); |
Prints s followed by a colon and a system-dependent error message based on errno . Useful for debugging file operation failures. |
feof |
<stdio.h> |
Checks for end-of-file indicator. | int feof(FILE *stream); |
Returns non-zero if the EOF indicator is set for the stream. |
ferror |
<stdio.h> |
Checks for error indicator. | int ferror(FILE *stream); |
Returns non-zero if the error indicator is set for the stream. |
stdin |
<stdio.h> |
Predefined standard input stream. | extern FILE *stdin; |
Typically maps to the keyboard. |
stdout |
<stdio.h> |
Predefined standard output stream. | extern FILE *stdout; |
Typically maps to the console/terminal. |
stderr |
<stdio.h> |
Predefined standard error stream. | extern FILE *stderr; |
Typically maps to the console/terminal for error messages. Usually unbuffered. |
BUFSIZ |
<stdio.h> |
Macro for an optimal buffer size. | (macro) |
System-dependent, typically a power of 2 (e.g., 512, 1024, 4096 bytes). |
Conclusion
Mastering C’s file I/O capabilities is essential for any serious C programmer. By understanding streams, buffering, and the specific roles of functions like fprintf
, fgets
, fread
, and fwrite
, you can efficiently and robustly interact with files and other I/O devices. Always remember to handle errors and close your files to ensure data integrity and prevent resource leaks.