First | Next | Previous | Last | Glossary | About |
Take a look at figure 1, you can't miss it. It is a chart of the files in a project and represents, in a graphical way, how the various components of an application relate to each other. The project represents a simple word-processor called myword.
Perhaps you have been working on this project for a couple weeks and after many hours of design work you have arrived at the structure of figure 1. myword, include, main, and lib all represent directories which contain the files you have carefully crafted.
The main directory contains the main files for the application. The lib directory contains source files which you have written and which contain functions used by the main files. The include directory contains a header file which is common to some or all of the other files.
To build this project you would have to compile each source file and them link them to build the final executable, for instance:
If you made changes to a source file after it had all been built then you would have to rebuild to incorporate the changes, clean up again and install again. All this could get tedious and can also be error prone. What if you make an important change to search.cc, compile it and then get interrupted and forget to link the application?
Naturally there is a better way and that is to use make, a powerful program which will take care of all of these things for you.
# a comment APPNAME = bldg TARGET = $(APPNAME).exe OBJECTS = desc.o building.o $(APPNAME).o CC = g++ $(TARGET): $(OBJECTS) $(CC) -o $(APPNAME) $(OBJECTS) desc.o: desc.cc building.o: building.cc building.h declares.h $(APPNAME).o: $(APPNAME).cc building.h declares.h .PHONY: clean clean: -rm *.o -rm *.$$$$$$ strip $(TARGET)
Make uses a script file which contains rules on how to build an application. The script is usually called Makefile or makefile but can be given any name.
Here is a simple example of a makefile script which shows some of the features of make and its scripts.
The makefile contains:
It can contain other things and can be a very simple makefile or a very complex makefile.
To use make you:
Makefiles contain rules which are composed of a target, a list of dependencies and a list of commands.
target ... : dependencies ... command ... ...
The target is usually the name of a file but can be some other kind of target. We see this later. The dependencies list is a list of the files that make up the target. For instance the source files, header files and library files. The list of commands is a list of command lines which will build the target or carry out some action like copying files or deleting files.
Note that the command line or lines are offset from the left by one TAB space. This is important! and I will mention it again.
The first example is a simple makefile which shows how we use the rules.
menu1: menuex1.o menuex1res.o g++ -o menu1 menuex1.o menuex1res.o -mwindows menuex1.o: menuex1.cc menuex1.h g++ -c -w menuex1.cc menuex1res.o: menuex1res.rc menuex1res.h windres menuex1res.rc menuex1res.o
There are three rules with the targets menu1:, menuex1.o: and menuex1res.o:. Note that each target starts in the leftmost column and is terminated by a colon.
A target is a file name which results from some command action and is followed by its dependencies:
menu1: menuex1.o menuex1res.o
This states that menu1 depends on the object files menuex1.o and menuex1res.o
A target has a command for building purposes, like
menu1: menuex1.o menuex1res.o g++ -o menu1 menuex1.o menuex1res.o -mwindows
This states that to build the target menu1 we need to execute the command line
g++ -o menu1 menuex1.o menuex1res.o -mwindows
NOTA BENE: there is a TAB before the command line
The target, dependencies and the command constitute a MAKE RULE.
If menu1 doesn't exist then the command will be executed and menu1 will be created. If either menuex1.o or menuex1res.o doesn't exist then the appropriate rule is invoked to create the missing target. Make does more than this though. If a target dependency changes then the rule is invoked so that the target is rebuilt. A change in a dependency is obvious from the file date/time stamp. If the target is older than one of its dependents then the target needs to be rebuilt.
Make starts with the first rule unless directed to some other rule.
Often we want to direct make to do something other than create a target, for example delete some files, or copy some files.
We can use non-file targets to do this.
You can see that the clean target has no dependencies and has a simple command.
# Makefile1 menu1: menuex1.o menuex1res.o g++ -o menu1 menuex1.o menuex1res.o -mwindows menuex1.o: menuex1.cc menuex1.h g++ -c -w menuex1.cc menuex1res.o: menuex1res.rc menuex1res.h windres menuex1res.rc menuex1res.o clean: rm *.o
This is used like this:
make clean
NB This Makefile is called Makefile1 and is called like this
make -f Makefile1 clean
Make normally looks for a file called Makefile or makefile but can be directed to look for some other file by using the -f switch. The target is shown on the command line. The make command above uses Makefile1 and generates the target - clean.
There is a problem with using targets that don't have any dependencies. If there is a file called clean then, since a target is a file, make will see no dependencies, report that clean is up to date and NOT execute the command.
To get around the problem we can use the directive PHONY. This directive allows us to list a number of targets that are not files.
# Makefile2 menu1: menuex1.o menuex1res.o g++ -o menu1 menuex1.o menuex1res.o -mwindows menuex1.o: menuex1.cc menuex1.h g++ -c -w menuex1.cc menuex1res.o: menuex1res.rc menuex1res.h windres menuex1res.rc menuex1res.o .PHONY : clean clean: rm *.o
.PHONY is an example of a special target which states that there will be a target of the given name, in this case clean, but it is not to be treated as a file target. The action for make is to execute the command.
As your projects grow and the number of targets and dependencies increases it can become quite tedious maintaining the makefile. To overcome the tedium and lessen the possibility of errors we can use macros to represent the different things we use in the makefile.
A makefile macro has the form:
MACRONAME = string
used as $(MACRONAME) or ${MACRONAME} unless the macro is a single character in which case we can use $MACRONAME. Like a macro definition for CPP, int make the macro is expanded as it occurs in the makefile.
# Makefile3 OBJECTS = menuex1.o menuex1res.o RESCOMP = windres.exe CC = g++ menu1: $(OBJECTS) $(CC) -o menu1 ${OBJECTS} -mwindows menuex1.o: menuex1.cc menuex1.h $(CC) -c -w menuex1.cc menuex1res.o: menuex1res.rc menuex1res.h $(RESCOMP) menuex1res.rc menuex1res.o .PHONY : clean clean: echo cleaning up rm $(OBJECTS) rm *.$$$$$$
In this example you can see how useful macros are. If we need to add another object file we can just add it to the list of objects which is assigned to the macro OBJECTS. We don't have to find every line that contains object files and modify it.
You can see to that the compiler is defined as a macro.
A NOTE: I have been using PFE which generates backup files with the extension $$$. I also use a DOS version of the rm command to delete files. The $ symbol has special meaning for make, as you'll see later, so to use a $ symbol we need to use $$.
Many of the compile and link operations are quite repetitive and quite simple, for instance to generate an object file:
$(CC) -c menuex1.cc
We can use implied rules to do these sort of things. Such rules are built into make.
# Makefile4 OBJECTS = menuex1.o menuex1res.o RESCOMP = windres.exe CC = g++ menu1: $(OBJECTS) $(CC) -o menu1 ${OBJECTS} -mwindows menuex1.o: menuex1.h menuex1res.o: menuex1res.rc menuex1res.h $(RESCOMP) menuex1res.rc menuex1res.o ...
You can see that the line which builds target menuex1.o doesn't have a command. There is however an inbuilt command. If we want and object file from a C or C++ source file then it implies that we will run the compiler and give it the -c switch.
This simple project consists of three C++ source files and two header files. If you have done the lessons on inheritance you will probably recognise them.
APPNAME = bldg TARGET = $(APPNAME).exe OBJECTS = desc.o building.o $(APPNAME).o CC = g++ $(TARGET): $(OBJECTS) $(CC) -o $(APPNAME) $(OBJECTS) desc.o: desc.cc building.o: building.cc building.h declares.h $(APPNAME).o: $(APPNAME).cc building.h declares.h .PHONY: clean clean: -rm *.o -rm *.$$$$$$ strip $(TARGET)
You can see that there is nothing in the makefile you haven't seen before except for the dash in front of rm.
The dash in front of rm is supposed to suppress error messages. For example if there were .o files then -rm is supposed to suppress the error messages it would normally emit. It seems not to work under DOS but that is probably me misusing some feature again. You can use rm -f to force rm to ignore non-existent files.
The source files follow below.
A NOTE ON THE SOURCE CODE: If some of the source puzzles you, particularly the comments on compilation units in the building.cc file, then you should read the lesson on compilation units.
bldg.cc
/* This example uses an array of domestic objects which are not initialised. It demonstrates the need to define the default constructor for domestic. It is already declared in the domestic class definition. */ #include <iostream> #include "building.h" #include "declares.h" extern const char * RoofDesc[]; extern const char * ConstructionDesc[]; extern const char * CommercialDesc[]; int main() { commercial skyscraper(FLAT, CONCRETE, 120, OFFICE); domestic home(PITCHED, CLAYBRICK, 1, 8, 3, 1); domestic estate[5]; home.inspect(); skyscraper.inspect(); return 1; }
desc.cc
const char * RoofDesc[] = {"flat", "pitched", "skillion"}; const char * ConstructionDesc[] = {"stone", "claybrick", "steel", "wooden"}; const char * CommercialDesc[] = { "office", "factory", "shop", "garage"};
declares.h
/* Declarations for the building project */ #ifndef _DECLARES_H_ #define _DECLARES_H_ #include <iostream> #define MINFLOORS 1 typedef enum rooftype {FLAT, PITCHED, SKILLION}; typedef enum constructiontype {STONE, CLAYBRICK, STEEL, WOODEN, CONCRETE, MUDBRICK}; typedef enum commercialtype {OFFICE, FACTORY, SHOP}; #endif
building.h
#ifndef _BUILDING_H_ #define _BUILDING_H_ #include <iostream> #include "declares.h" #define MINFLOORS 1 //A simple abstract class. We don't normally directly access this class //it will be used by the derived classes. class building { public: building(); building(rooftype, constructiontype, int); void inspect(); protected: rooftype roof; constructiontype walls; int floors; private: }; class commercial : public building { public: commercial(); commercial(rooftype, constructiontype, int, commercialtype); void inspect(); protected: commercialtype usage; }; class domestic : public building { public: domestic(); domestic(rooftype, constructiontype, int, int, int, int); void inspect(); protected: int allrooms, bedrooms, bathrooms; }; #endif
building.cc
#include "building.h" /* These are used by both this file, which is one compilation unit, and bldg.cc, which is another compilation unit. We tell each unit that the identifiers are available externally. */ extern const char * RoofDesc[]; extern const char * ConstructionDesc[]; extern const char * CommercialDesc[]; building::building() { cout << "Don't interrupt, I'm building a big estate." << endl; } building::building(rooftype r, constructiontype w, int f) { roof = r; walls = w; if (f > 0) floors = f; else floors = MINFLOORS; } void building::inspect() { cout << RoofDesc[roof] << "," << ConstructionDesc[walls] << "," << floors << " floors, "; } commercial::commercial(rooftype r, constructiontype w, int f, commercialtype u ) : building(r, w, f) { usage = u; } void commercial::inspect() { building::inspect(); cout << CommercialDesc[usage] << endl; } domestic::domestic() { } void domestic::inspect() { building::inspect(); cout << allrooms << " rooms, with " << bedrooms << " bedrooms and " << bathrooms << " bathrooms." << endl; } domestic::domestic(rooftype r, constructiontype w, int f, int a, int be, int ba ) : building(r, w, f) { allrooms = a; bedrooms = be; bathrooms = ba; }
Copy all of the building project files, including the makefile and try the example to make sure it works.
Exercise 1.2Create a function of your own that does anything you like, display the time for instance. Make sure that it uses a header file of its own and any other header files it needs. Now add it to your makefile and call the function from main().
Most of the example makefiles were based on sources that you use in this tutorial. The source files follow the tutorial. NOTE: This is a windows program so don't try to build it in Linux. If you haven't completed the windows programming sessions then you might like to skip this tutorial.
Exercise 2.1Get copies of Makefile4 and the following source files and see if you can get it all to build.
menuex1.h
#define CM_FILE_EXIT 9001 #define CM_FILE_OPEN 9002 #define CM_FILE_CLOSE 9003 #define CM_CHOICE1 9010 #define CM_CHOICE2 9011 #define CM_CHOICE3 9012 #define CM_CHOICE4 9020 #define CM_CHOICE5 9022 #define CM_CHOICE6 9023
menuex1res.cc
#include <windows.h> #include "menuex1.h" #include "menuex1res.h" LRESULT CALLBACK WndProc( HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { static char szClassName[] = "MenuEx1WindowClass"; WNDCLASSEX WndClass; HWND hwnd; MSG Msg; WndClass.cbSize = sizeof(WNDCLASSEX); WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = WndProc; WndClass.cbClsExtra = 0; WndClass.cbWndExtra = 0; WndClass.hInstance = hInstance; WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); WndClass.hCursor = LoadCursor(NULL, IDC_ARROW); WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); WndClass.lpszMenuName = "MENUEX1"; WndClass.lpszClassName = szClassName; WndClass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if(!RegisterClassEx(&WndClass)) { MessageBox(0, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK | MB_SYSTEMMODAL); return 0; } hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, szClassName, TITLE_STRING, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 320, 180, NULL, NULL, hInstance, NULL); if(hwnd == NULL) { MessageBox(0, "Window Creation Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK | MB_SYSTEMMODAL); return 0; } ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while(GetMessage(&Msg, NULL, 0, 0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } return Msg.wParam; } LRESULT CALLBACK WndProc( HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) { switch(Message) { case WM_COMMAND: switch(LOWORD(wParam)) { case CM_FILE_EXIT: PostMessage(hwnd, WM_CLOSE, 0, 0); break; case CM_CHOICE1: MessageBox(hwnd, "Choice 1", "Menu 2", MB_OK); break; case CM_CHOICE2: MessageBox(hwnd, "Choice 2", "Menu 2", MB_OK | MB_ICONINFORMATION); break; case CM_CHOICE3: MessageBox(hwnd, "Choice 3", "Menu 2", MB_OK | MB_ICONEXCLAMATION); break; case CM_CHOICE4: MessageBox(hwnd, "Choice 4", "Menu 3", MB_ABORTRETRYIGNORE | MB_ICONERROR); break; case CM_CHOICE5: MessageBox(hwnd, "Choice 5", "Menu 3", MB_OK | MB_YESNO | MB_ICONSTOP); break; case CM_CHOICE6: MessageBox(hwnd, "Choice 6", "Menu 3", MB_YESNOCANCEL | MB_ICONSTOP ); break; } break; case WM_CLOSE: DestroyWindow(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, Message, wParam, lParam); } return 0; }
menuex1res.rc
#include <windows.h> #include "menuex1res.h" #include "menuex1.h" MENUEX1 MENU { POPUP "&File" { MENUITEM "File &Open", CM_FILE_OPEN MENUITEM "File &Close", CM_FILE_CLOSE MENUITEM "E&xit", CM_FILE_EXIT } POPUP "Menu &2" { MENUITEM "Choice &1", CM_CHOICE1 MENUITEM "Choice &2", CM_CHOICE2 MENUITEM "Choice &3", CM_CHOICE3 } POPUP "Menu &3" { MENUITEM "Choice &4", CM_CHOICE4 MENUITEM "Choice &5", CM_CHOICE5 MENUITEM "Choice &6", CM_CHOICE6 } }
menuex1res.h
#define TITLE_STRING "This is the window title - (DB 1999)"
First | Next | Previous | Last | Glossary | About |
Copyright © 1999 - 2001
David Beech