File handling in C

Subpage of C Deep Dive

A close look at the C language.

In C, we can set where the input and output go in two ways.

Redirecting Input

This is the most common and simplest method, handled by your operating system's shell (like Bash or Command Prompt), not by special C code. Your C program simply reads from stdin or writes to stdout as usual. 

For reading information, use the < operator to feed the contents of a file as the standard input to your program.

  • Syntax: ./your_program < input_file.txt
  • In your C code: Use standard input functions like scanf()gets()getchar(), or fgets(). Your program won't "know" the input is from a file; it just reads from stdin.

For writing information, use the > or >> operators to send the program's standard output to a file.

  • >: Overwrites the file if it exists.
  • >>: Appends the output to the end of the file.
  • Syntax: ./your_program > output_file.txt
  • In your C code: Use standard output functions like printf()puts(), or putchar().

Piping also comes in useful. Use the | operator to send the stdout of one program to the stdin of another.

  • Syntax: ./program1 | ./program2

Here is a complex example of all of these put together:

(./bermuda | ./geo2json) < spooky.csv > output.json

Input from spooky.csv is fed into bermuda followed by geo2json then written to output.json. The parentheses will make sure the data file is read by the Standard Input of the bermuda program.

Error Handling

One issue that comes up with this approach is handling error messages. A naive approach will just redirect all error messages to the file and we wouldn’t know that something even went wrong.

To overcome this problem we can use the fact that the program returns a status that represents whether the code has run successfully. First, here is how we can access the status produced by the code:

# In MacOS/Linux
$ echo $?
2
# In Windows
C:\> echo %ERRORLEVEL%
2

Now it is important to remember that there are two output streams from all processes:

  • Standard Output: stdout carries normal program results to the terminal (screen) by default
  • Standard Error: stderr carries error messages or diagnostic info separately, allowing users to redirect or filter them independently (e.g., save output to a file but log errors to another) for better control and debugging. 

So to be able to do file handling with this approach, we need to be sure to redirect our error messages through the standard error stream.

For this we use the fprintf function. This is the superset of printf which allows us to choose where you want to send text to.

The function prototype for fprintf() is included in the <stdio.h> header file:

int fprintf(FILE *stream, const char *format, ...);
  • FILE *stream: A pointer to the file object where the output will be written. This pointer is usually obtained using the fopen() function.
  • const char *format: A C string that contains the text to be written, which can include format specifiers like %d (integer), %f (float), or %s (string).

On success, fprintf() returns the number of characters successfully written to the stream. If a writing error occurs, it returns a negative value. 

If needed we can redirect the data we write to standard error using 2>. For example:

command > stdout.txt 2> stderr.txt

Within the C program

You can control redirection from inside your C code using the freopen() function from stdio.h. This is useful if you cannot use command line redirection or need to dynamically change streams during execution. 

#include <stdio.h>

int main() {
    // Redirect stdout to a file named "log.txt"
    FILE *original_stdout = stdout; // Optional: save original stdout

    if (freopen("log.txt", "w", stdout) == NULL) {
        perror("freopen failed");
        return 1;
    }

    printf("This message goes to the file 'log.txt'.\n");
    fprintf(stderr, "This message still goes to the console (stderr).\n");

    // To restore original stdout (e.g., for further console output)
    fflush(stdout); // Flush buffered data to the file before closing/resetting
    // You would use platform-specific methods to reopen the console, e.g., freopen("/dev/tty", "w", stdout) on Unix

    return 0;
}

Another way to do this is by creating a data stream to a file:

FILE *in_file = fopen("input.txt", "r");
FILE *out_file = fopen("output.txt", "w");

The fopen() function takes two parameters: a filename and a mode. The mode can be w to write to a file, r to read from a file, or a to append data to the end of a file. Pay attention to the type: FILE.

Once you’ve created a data stream, you can print to it using fprintf(), just like before. But what if you need to read from a file? Well, there’s also an fscanf() function to help you do that too:

fprintf(out_file, "Don't wear %s with %s", "red", "green");
fscanf(in_file, "%79[^\n]\n", sentence);

Finally, when you’re finished with a data stream, you need to close it. The truth is that all data streams are automatically closed when the program ends, but it’s still a good idea to always close the data stream yourself:

fclose(in_file);
fclose(out_file);

Usually a process can have up to 256 data streams. The key thing is there’s a limited number of them, so make sure you close them when you’re done using them.

// 1. Define the structure
struct Person {
char name[50];
int age;
};

// 2. Declare a function that accepts a pointer to the structure
void update_person_age(struct Person *person_ptr) {
// 4. Access members using the arrow operator (->)
    person_ptr->age =30;
// The arrow operator is a shorthand for (*person_ptr).age.
}

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.