In this C++ Makefile tutorial, we will discuss the major aspects of Make tool and makefile including its advantages and applications in C++:
In any C++ project, one of the important goals is to simplify the building of the project so that we get all dependencies and project files in one place and execute them in one go so that we get the desired output with a single command.
At the same time, whenever any of the project files are modified, we do not have to go through the trouble of building the entire project again i.e. whenever a file or two are modified in the project, we rebuild only these changed files and then proceed with the execution.
These are exactly the features that are addressed by the “make” tool and “makefiles” in C++. In this tutorial, we will discuss all the major aspects of makefiles as well as their applications in C++.
What You Will Learn:
Make is a UNIX tool and is used as a tool to simplify building executable from different modules of a project. There are various rules that are specified as target entries in the makefile. The make tool reads all these rules and behaves accordingly.
For example, if a rule specifies any dependency, then the make tool will include that dependency for compilation purposes. The make command is used in the makefile to build modules or to clean up the files.
The general syntax of make is:
%make target_label #target_label is a specific target in makefile
For example, if we want to execute rm commands to clean up files, we write:
%make clean #here clean is a target_label specified for rm commands
A makefile is nothing but a text file that is used or referenced by the ‘make’ command to build the targets. A makefile also contains information like source-level dependencies for each file as well as the build-order dependencies.
Now let’s see the general structure of makefile.
A makefile typically starts with variable declarations followed by a set of target entries for building specific targets. These targets may be .o or other executable files in C or C++ and .class files in Java.
We can also have a set of target entries for executing a set of commands specified by the target label.
So a generic makefile is as shown below:
# comment target: dependency1 dependency2 ... dependencyn <tab> command # (note: the <tab> in the command line is necessary for make to work)
A simple example of the makefile is shown below.
# a build command to build myprogram executable from myprogram.o and mylib.lib all:myprogram.o mylib.o gcc –o myprogram myprogram.o mylib.o clean: $(RM) myprogram
In the above makefile, we have specified two target labels, first is the label ‘all’ to build executable from myprogram and mylib object files. The second target label ‘clean’ removes all the files with the name ‘myprogram’.
Let’s see another variation of the makefile.
# the compiler: gcc for C program, define as g++ for C++ CC = gcc # compiler flags: # -g - this flag adds debugging information to the executable file # -Wall - this flag is used to turn on most compiler warnings CFLAGS = -g -Wall # The build target TARGET = myprogram all: $(TARGET) $(TARGET): $(TARGET).c $(CC) $(CFLAGS) -o $(TARGET) $(TARGET).c clean: $(RM) $(TARGET)
As shown in the above example, in this makefile we make use of the variable ‘CC’ that contains the compiler value that we are using (GCC in this case). Another variable ‘CFLAGS’ contains the compiler flags that we will use.
The third variable ‘TARGET’ contains the name of the program for which we need to build the executable.
The measure advantage of this variation of the makefile is that we just need to change the values of the variables that we have used whenever there is some change in the compiler, compiler flags, or executable program name.
Example Of Make And Makefile
Consider a program example with the following files:
- Main.cpp: Main driver program
- Point.h: Header file for point class
- Point.cpp: CPP implementation file for point class
- Square.h: Header file for square class
- Square.cpp: CPP implementation file for square class
With the above-given .cpp and .h files, we need to compile these files separately to generate .o files and then link them into executable named main.
So next we compile these files separately.
- g++ -c main.cpp: generates main.o
- g++ -c point.cpp: generates a point.o
- g++ -c square.cpp: generates square.o
Next, we link the object files together to generate the executable main.
g++ -o main main.o point.o square.o
Next, we need to decide which of the files we will have to recompile and regenerate when certain parts of the program are updated. For this, we will have a dependency chart that shows various dependencies for each of the implementation files.
Given below is the dependency chart for the above files.
So in the above dependency chart, we can see the executable ‘main’ at the root. The executable ‘main’ consists of object files viz. main.o, point.o, square.o that is generated by compiling main.cpp, point.cpp and square.cpp respectively.
All cpp implementations use header files as shown in the above chart. As shown above main.cpp references both point.h and square.h as it’s the driver program and uses point and square classes.
Next file point.cpp references point.h. The third file square.cpp references square.h as well as the point.h as it will need a point as well to draw the square.
From the dependency chart above, it’s clear that whenever any .cpp file or .h file referenced by .cpp file changes, we need to regenerate that .o file. For example, when main.cpp changes, we need to regenerate the main.o and link the object files again to generate the main executable.
All the above explanations that we have given will work smoothly if there are few files in the project. When the project is huge and files are big and too many, then it becomes difficult to regenerate the files repeatedly.
Thus, we go for make files and we use to make a tool to build the project and generate the executable.
We have already seen various parts of a make file. Note that the file should be named “MAKEFILE” or ‘makefile’ and should be placed in the source folder.
Now we will write down the makefile for the above example.
We will define variables to hold the values of compiler and compiler flags as shown below.
CC = g++ CFLAGS = -wall -g
Then we create the first target in our makefile i.e. the executable main. So we write a target with its dependencies.
main: main.o point.o square.o
Thus the command to generate this target is
<tab>$(CC) $(CFLAGS) –o main main.o point.o square.o
Note: The above command actually translates into g++ -wall –g –o main main.o point.o square.o
Our next target will be to generate object files, main.o, point.o, square.o
Now to generate main.o, the target will be written as:
Main.o: main.cpp point.h square.h
The command for this target is:
<tab>$(CC) $(CFLAGS) –c main.cpp
The next file point.o can be generated using the below command:
<tab>$(CC) $(CFLAGS) –c point.h
In the above command, we have skipped point.cpp. This is because make already knows that .o files are generated from the .cpp files, thus only .h (include file) is enough.
Similarly, square.o can be generated with the following command.
<tab>$(CC) $(CFLAGS) –c square.h point.h
The entire makefile for this example will look as shown below:
# Makefile for Writing Make Files Example # ***************************************************** # Variables to control Makefile operation CC = g++ CFLAGS = -Wall -g # **************************************************** # Targets needed to bring the executable up to date main: main.o Point.o Square.o $(CC) $(CFLAGS) -o main main.o Point.o Square.o # The main.o target can be written more simply main.o: main.cpp Point.h Square.h $(CC) $(CFLAGS) -c main.cpp Point.o: Point.h Square.o: Square.h Point.h
Thus, we see that we have a complete makefile that compiles three C++ files and then generates an executable main from the object files.
Advantages Of Makefiles
- When it comes to big projects, then using makefiles helps us to represent the project in a systematic and efficient way.
- Makefiles make source code more concise and easy to read and debug.
- Makefiles automatically compile only those files that are changed. Thus we need not regenerate the entire project when some of the portions of the project are modified.
- Make tool allows us to compile multiple files at once so that all the files can be compiled in a single step.
Makefiles are a boon to software development. Using a C++ makefile, we can build solutions in lesser time. Also when a part of the project is modified, the makefile recompiles and regenerates only that part without having to regenerate the entire project.
C++ Makefile allows us to represent the project systematically and efficiently thereby making it more readable and easy to debug.
In this C++ Makefile tutorial, we have seen makefile and make tools in detail. We have also discussed how to write a makefile from scratch.