Skip to content

C Crash Course, Part 2: Declaration vs. Definition

This is part two of a C programming mini crash-course.

So you’ve learned some C programming and know how to write methods. Quickly after that, you found out that you have to write the methods in a specific order, otherwise the compiler complains that it can’t find the methods. But why is that? And surely there must be a better way to make the program work than reordering the methods.

As you probably know, the entry point of all the software we write is main:

#include <stdio.h>
int main(int argc, char** argv)
{
printf("Hello world\n");
return 0;
}

We normally place this code in a file called main.c. Actually, the compiler doesn’t care what the file is called — it’s just for our own sake, so it is easier for us to find the code.

You build your program by invoking the compiler with the file as an argument. Let’s use the GNU C++ compiler (the C++ compiler gives us better error messages than the C compiler):

Terminal window
g++ main.c -o "myprogram"
./myprogram.exe
Hello World

The first line invokes the compiler, which builds the code and creates an executable named myprogram. The second line starts the program and the third line is the output.

I will use a simple calculator application as an example, to teach you about declarations and definitions. The calculator has addition and subtraction, implemented as add and sub methods.

The program compiles and runs correctly:

#include <stdio.h>
int add(int left, int right)
{
int result = left + right;
return result;
}
int sub(int left, int right)
{
int result = left - right;
return result;
}
int main(int argc, char** argv)
{
int a = 10;
int b = 5;
int res1 = add(a, b);
printf("a + b = %d\r\n", res1);
int res2 = sub(a, b);
printf("a - b = %d\r\n", res2);
return 0;
}
Terminal window
g++ main.c -o "myprogram"
./myprogram.exe
a + b = 15
a - b = 5

But what happens when I move add and sub to the end of the file?

#include <stdio.h>
int main(int argc, char** argv)
{
int a = 10;
int b = 5;
int res1 = add(a, b);
printf("a + b = %d\r\n", res1);
int res2 = sub(a, b);
printf("a - b = %d\r\n", res2);
return 0;
}
int add(int left, int right)
{
int result = left + right;
return result;
}
int sub(int left, int right)
{
int result = left - right;
return result;
}

Compilation will now result in an error:

g++ main.c -o "myprogram"
main.c: In function 'int main(int, char**)':
main.c:8:22: error: 'add' was not declared in this scope
int res1 = add(a, b);
^
main.c:11:22: error: 'sub' was not declared in this scope
int res2 = sub(a, b);
^

The compiler is really, really stupid. It reads the main.c file one line at a time, starting from the top. When it reaches the line with add(a, b) it hasn’t seen the add method before, so it says: “I don’t know what add means”.

In the previous version of the program, the compiler read the add and sub methods before it got to the lines where add and sub were used. Think of it as if the compiler has a little notebook, and when it encounters a new method, it writes down: “Ah..! There is a method here — it’s called add, it takes two integer parameters and returns an integer.” And when some other part of our code wants to use the add method, the compiler knows that there is such a method and knows the number and type of parameters and the return type of the method. (Remember: all methods have a return type — void is also a type, used to specify that there is no return value.)

The way to fix our program is to tell the compiler that there is an add (and sub) method, without depending on the order in which we write the different methods.

In C and C++ we have what is called declaration and definition:

  • Declaration — tells the compiler that a method exists. (Declarations are also called prototypes.)
  • Definition — is the implementation of a method.

The declarations of the add and sub methods are:

int add(int left, int right);
int sub(int left, int right);

If we place them at the top of the main.c file, the program compiles again:

#include <stdio.h>
int add(int left, int right);
int sub(int left, int right);
int main(int argc, char** argv)
{
int a = 10;
int b = 5;
int res1 = add(a, b);
printf("a + b = %d\r\n", res1);
int res2 = sub(a, b);
printf("a - b = %d\r\n", res2);
return 0;
}
int add(int left, int right)
{
int result = left + right;
return result;
}
int sub(int left, int right)
{
int result = left - right;
return result;
}
Terminal window
g++ main.c -o "myprogram"
./myprogram.exe
a + b = 15
a - b = 5

Remember that when the compiler reads the main.c file, it does so one line at a time. It sees the declaration of add and the declaration of sub, writes them down in its little notebook, and when it reaches the line where add is used, it knows that there is an add method in the program and it can check that the parameters to the method and its return value are correct.

The program compiles again :)

We often want to split a program into multiple files. This way, functionality which has something in common can be grouped together in a file. This makes it easier for us to find the code we are looking for, and it lets multiple programmers work on different parts of the code.

At some point you will want code from one file to call a method defined in another file. But as you know, the compiler is really stupid and it only reads one file at a time. It is actually so stupid that it throws away the little notebook every time it starts reading a new file. This means you would have to declare all the methods you want to use, even though they are defined (and thus implemented) in other files.

This is where header files come in handy. A header file is simply a file which ends with .h or .hpp. We move the method declarations to the header file, and #include the header file at the top of both the .c file that contains the method definitions, and the top of any other file(s) that want to call the methods.

The header file calculator.h becomes:

int add(int left, int right);
int sub(int left, int right);

The calculator.c file will be:

#include "calculator.h"
int add(int left, int right)
{
int result = left + right;
return result;
}
int sub(int left, int right)
{
int result = left - right;
return result;
}

The main.c file will be:

#include <stdio.h>
#include "calculator.h"
int main(int argc, char** argv)
{
int a = 10;
int b = 5;
int res1 = add(a, b);
printf("a + b = %d\r\n", res1);
int res2 = sub(a, b);
printf("a - b = %d\r\n", res2);
return 0;
}

When we compile the program, we have to tell the compiler that there are two .c files:

Terminal window
g++ main.c calculator.c -o "myprogram"

Note that we do not tell the compiler that there is a header file. The compiler first reads main.c, and when it sees #include "calculator.h" it opens the calculator.h file and reads it from start to end before continuing to read main.c. A useful mental image is that the compiler copies the entire contents of calculator.h into main.c at the point where it sees the #include.

After it has compiled main.c, the compiler reads calculator.c and does the exact same thing again: it copies the entire contents of calculator.h into calculator.c at the point of the #include.

We can instruct the compiler to only compile the files, without linking them, with the -c compiler flag:

Terminal window
g++ -c main.c calculator.c

If you look in the folder where you placed main.c and calculator.c, you’ll now see two more files: main.o and calculator.o. Files ending in .o are called object files. The compiler makes an object file for each .c file you ask it to compile. When all files are compiled, they have to be linked so all the code from the different files becomes a single program (g++ does this automatically, unless we tell it not to with -c).

If we want to link the object files ourselves, we can do that with g++ too:

Terminal window
g++ main.o calculator.o -o "myprogram"

Which creates a myprogram executable.

How to avoid multiple includes of the same header file

Section titled “How to avoid multiple includes of the same header file”

Header files are allowed to #include other header files. This can lead to endless recursion: fileA.h includes fileB.h, which includes fileA.h, which includes fileB.h, which…

We can avoid this situation with compiler directives. The header file calculator.h becomes:

#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int left, int right);
int sub(int left, int right);
#endif

It has the add and sub declarations we know, but also these strange lines beginning with #. Those lines are instructions to the compiler.

#ifndef means “if not defined”. CALCULATOR_H after #ifndef is what is called a symbol. The line #define CALCULATOR_H tells the compiler to create a symbol named CALCULATOR_H.

Two things can happen when the compiler reads the file:

  1. If CALCULATOR_H is already defined, the compiler skips every line in the file until it sees #endif.
  2. If CALCULATOR_H is not defined, the compiler reads the lines in the file, defines CALCULATOR_H, and notes down in its little book that there is an add method and a sub method.

Endless recursion in #include is now prevented. Yay!

  1. Modify the room temperature control system from part 1 so the methods come after main().
  2. Split the temperature control system into two files: one with everything related to the heater, and one with the control algorithm (the code that decides what to do with the heater).
  3. Add cooling to the room temperature control system. First decide when cooling should be turned on and when it should be turned off, then implement it.