TU ACM logo.
blog

Files in C

We're going to get files from our hard drive into our program.
Posted 15 January 2020 at 1:26 PM
By Joseph Mellor

This is the thirteenth article in the Making Sense of C series. In this article, we're going to discuss how to open and close files in C. We'll also go into a few topics we haven't mentioned yet, including the const keyword.

This article is going to go through two of the standard library functions that we'll use in our first program, fopen and fclose, which will move a file from our hard drive to our program.

Our First Attempt at fopen

First, we're going to need some consistent way to interact with a file, so we need some consistent object to pass to all the file manipulation functions in the future. We also need to specify how we want to interact with a file. For example, we could have permission to read from a file but not modify it, such as a file with system information. Lastly, we're going to need to know the name of the file before we can open it.

The FILE Type

Since we need some consistent way to interact with a file, we need to store data about the file, such as where we are in the file, whether we're reading from or writing to the file, etc. We could store this data in separate variables, but doing so would be an absolute nightmare. Even Dennis Ritchie et al couldn't figure out how to write the Unix OS (the original C project) without some way to group everything together. So, they came up with the idea of a struct, which represents some data grouped together. We're going to discuss structs in more depth later, but for now, just know that a FILE is a struct with a bunch of data relevant to file operations.

First Attempt at fopen's Syntax

Remember that we need to create a FILE object from the name of the file (which will be an array of characters or a char *) and how we want to interact with it (which we could represent in multiple ways, so let's leave it unknown for now), which means the declaration of fopen will look something like:

FILE fopen(char * filename, ??? how_to_interact);

where the ??? means we haven't figured out the type yet.

Ways of Interacting With Files

As we said before, we can read from files and we can write to files. We can also add stuff to the end of files, like if we're recording a log of everything that's happened on a server or something. Lastly, we might be in situations where we want to be able to read and write to a file, such as when we're using save files in a video game (saving is writing or appending to a file and loading is reading from a file) or if we're updating settings in an application.

In short, Ritchie et al decided that how_to_interact should be easy for programmers to remember, so he decided to make the input short strings.

Operation Input String
Read from a file. "r"
Write to a file. "w"
Append to a file. "a"
Read from and write to a file. "r+" or "w+"
Read from and append to a file. "a+"

Read, write, and append are just the first letters of each word, and if you want to read and write, add a + at the end. You can only open a file that already exists with "r+", but if the file does not exist, then opening a file with "w+" will create a new file with the name you provide. In general, "r" and "r+" can only work on existing files while "w", "w+", "a", and "a+" will create new files if you give it the name of a file that doesn't exist. "w" and "w+" will overwrite files if they already exist, but "a" and "a+" will not. Lastly, "a" and "a+" will always append to a file and they cannot change anything that was in the file before it was opened.

Second Attempt at fopen's Syntax

Knowing that, we should have something like

FILE fopen(char * filename, char * mode);

right?

We actually have a few problems with the declaration of fopen as is. We'll see the first problem when we try to call the function. It was back in the article on memory addresses, but do you remember how I said that you could only initialize an array once and that just creating a char * will only allocate enough memory to store a memory address? Until this point, I just told you that was a fact of life and didn't offer any explanation and now is a good time to explain it.

String Literals

When you write the following code

char string[] = "Hello, World!";

the compiler converts that into machine instructions that say

  1. get fourteen bytes of memory
  2. and copy the bytes { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' } into those fourteen bytes of memory.

That sounds simple enough, but where exactly are the bytes that make up the string? How does the computer know which bytes to copy into string?

First, whenever someone runs your program, it is moved into RAM and so it has its own set of memory addresses for things like instructions (what your code becomes) and data (stuff in your program such as strings). Whenever your compiler sees a string literal (i.e., almost anything in double quotes), it will copy it directly into the executable, which you can see if you open it in a text editor like Sublime Text, VS Code, Notepad, etc.

char string[] = "Hello, World!";    // "Hello, World!" is a string literal.

Whenever you use the string literal, it will hand the memory address of the first byte in the string to whatever you're using it for just as it would for a string. Furthermore, when you use a string literal multiple times, it will store the string once and give you the same memory address.

char string1[] = "Hello, World!";   // Copy the fourteen bytes starting at
                                    // memory address 1234 (which contain
                                    // "Hello, World!") into the fourteen bytes
                                    // reserved for string1
char string2[] = "Hello, World!";   // Copy the fourteen bytes starting at
                                    // memory address 1234 (which contain
                                    // "Hello, World!") into the fourteen bytes
                                    // reserved for string2

If we try to store the memory address in a char *, we'll get a compiler error in our code because we would be modifying the executable file itself if the compiler let us. A program being able to modify itself is how Skynet or other AIs are supposed to take over the world, but your computer is more likely to produce wrong outputs or delete some data in the worst case. To see how we could modify an executable, let's come up with a simple program and convert it into assembly (a more natural language version of assembly, to be precise) like a compiler. Assume that the bytes "Hello, World!" is stored in memory addresses 1234 to 1247 within the program itself.

char * string1 = "Hello, World!";   // Copy the memory address 1234 into string1

string1[0] = 'h';                   // Set the value at memory address 1234 to
                                    // 'h'.

Memory address 1234 is actually within the program itself, meaning that the next time you run the program, string1 will be initialized with "hello, World!" instead of "Hello, World!". Since your compiler doesn't want you to write such a program, it will throw a compilation error and not make an executable. If we could guarantee the compiler that we would not modify the string literal, then we should be fine to store the memory address.

The const Keyword

The const keyword guarantees the compiler that we aren't going to modify whatever we apply it to. If the compiler sees us modifying it, then it will throw an error to make sure that everything runs properly.

const int a = 7;
a = 4;              // ERROR
int b = a + 5;      // No error because we aren't modifying a

So for string literals, we need to use a const char * instead.

const char * string = "Hello, World!";      // No error
string[0] = 'h';                            // ERROR

You should read const char * string; as "string stores a memory address to a bunch of chars that we cannot change through the variable string.

We can also do

char string[] = "Hello, World!";    // string has copied "Hello, World!", so it
                                    // can do whatever it wants to the copy
                                    // without messing up the program.
const char * str = string;          // str is a pointer to characters that it
                                    // can't modify.
string[0] = 'h';                    // string can modify it since it's just a
                                    // char *
str[0] = 'H';                       // ERROR: str is a const char *, which means
                                    // it can't modify the data at the memory
                                    // address it stores.

Declaring a variable const just means that you can't use it to modify the memory it holds, you can use other variables to modify the memory so long as you create it yourself.

Lastly, we can store any type into a const version of the type, but we can't store pointers to const types in pointers to regular types.

char string[] = "Hello, World!";                // Valid
const char * str_literal;                       // Valid
char * str = string;                            // Valid
str_literal = str;                              // ERROR

Our Correct Attempt at fopen

Well, since we don't plan on modifying either of the arguments, we can declare them const and be done with it.

FILE fopen(const char * filename, const char * mode);

The syntax for fopen is almost right as we have it, but we have a slightly more efficient way of using it. Instead of returning a FILE object, we're going to return a pointer to a FILE object, like so

FILE * fopen(const char * filename, const char * mode);

If we do this, we can then modify the underlying FILE object with functions instead of having to take the address of the FILE and pass it into the function or modify the FILE object in our code. Furthermore, it allows us to return NULL to indicate a problem in opening the file.

fclose

Since we're opening a file, we need to close it when we're done, so we'll have a function called fclose. fclose only needs to know which file to close, so it has the syntax

int fclose(FILE * file_object);

where the return value is 0 if the file closed properly or the constant EOF if the file was not closed properly.

fopen and fclose in Our Program

Now that we can open and close files in our program, let's add it into our code. We just want to read from a file, so we're going to use "r" as our mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int check_if_strings_differ(char * str1, char * str2);

int main(int argc, char ** argv) {
    char * program_name = argv[0];
    if (argc < 3) {
        // TODO: Print Usage Message
        return -1;
    }
    char * filename = argv[1];
    char * word = argv[2];
    FILE * reader = fopen(filename, "r");
    // TODO: Count number of occurrences in a file
    fclose(reader)
    return 0;
}

int check_if_strings_differ(const char * str1, const char * str2) {
    int i = 0;
    while (str1[i] && str2[i] && (str1[i] == str2[i])) {
        i += 1;
    }
    return str1[i] != str2[i];
}

I also modified our check_if_strings_differ function to take in const char *s instead of char *s because we will not modify the strings and it will allow us to call it with const char *s and char *s.

Between the first two highlighted lines, we now can use the variable reader to read from the file.

Summary

In this article, we learned about

What's Next

In the next article, Compilers and IDEs for C, we're going to go through the basics of setting up a compiler and and IDE for C so that we can actually compile programs.

A picture of Joseph Mellor, the author.

Joseph Mellor is a Senior at TU majoring in Physics, Computer Science, and Math. He is also the chief editor of the website and the author of the tumd markdown compiler. If you want to see more of his work, check out his personal website.
Credit to Allison Pennybaker for the picture.