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):
g++ main.c -o "myprogram"./myprogram.exeHello WorldThe 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.
A simple calculator
Section titled “A simple calculator”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;}g++ main.c -o "myprogram"./myprogram.exea + b = 15a - b = 5But 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.)
Declaration and definition
Section titled “Declaration and definition”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;}g++ main.c -o "myprogram"./myprogram.exea + b = 15a - b = 5Remember 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 :)
Splitting a program into multiple files
Section titled “Splitting a program into multiple files”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:
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:
g++ -c main.c calculator.cIf 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:
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);
#endifIt 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:
- If
CALCULATOR_His already defined, the compiler skips every line in the file until it sees#endif. - If
CALCULATOR_His not defined, the compiler reads the lines in the file, definesCALCULATOR_H, and notes down in its little book that there is anaddmethod and asubmethod.
Endless recursion in #include is now prevented. Yay!
Exercises
Section titled “Exercises”- Modify the room temperature control system from
part 1
so the methods come after
main(). - 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).
- 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.