Using Multiple Source Files
Last Updated 12/21/07
As you saw with the last article, program source codes can get pretty big. Here you'll learn how to break your program's source into smaller, easier to manage pieces.
The state machine demo had over 1000 lines of code. Any of you that have tried know it's a pain having to
search through one big source file. By having your program's source split into multiple source files, you no
longer have to sift through one big chunk of code.
When dealing with multiple source files there's basically two types of files you're going to be dealing with: source files and header files. Standard *.cpp source files you already know as you've been using them since you started programming. Headers, however, are a bit tricky.
To try to understand what headers do, look in the SDL_image.h file. Inside, you'll see a bunch of declarations of the functions in the SDL_image API like IMG_Load(), IMG_GetError(), IMG_isPNG(), etc, but you don't actually see the definitions of the functions that load the images, get the errors, and such. Is all that headers do is declare constants/classes/functions/variables?
The answer is yes. All SDL_image.h does is tell the compiler about the functions in SDL_image so it can compile your source file. Is that all it needs? Well, when you try to build your executable, the compiler will compile your source file, then the linker will try to link everything together into one binary. After it compiles your .cpp file, it will try to look up the actual definitions for the SDL_image functions. When the linker doesn't find the function definitions in one of the *.cpp files in your project, it will complain that it can't find the definitions and abort.
Well where is the actual code for SDL_image functions? The functions are compiled inside of the SDL_image binary (*.dll on windows and *.so on *nix). To get the linker to stop complaining, we give it a lib file which tells the linker where function definitions are in the SDL_image binary so it can link dynamically at the program's run time.
This might not make much sense now, but it will after we get our hands dirty by splitting up the motion tutorial source code into mulitple files.
When dealing with multiple source files there's basically two types of files you're going to be dealing with: source files and header files. Standard *.cpp source files you already know as you've been using them since you started programming. Headers, however, are a bit tricky.
To try to understand what headers do, look in the SDL_image.h file. Inside, you'll see a bunch of declarations of the functions in the SDL_image API like IMG_Load(), IMG_GetError(), IMG_isPNG(), etc, but you don't actually see the definitions of the functions that load the images, get the errors, and such. Is all that headers do is declare constants/classes/functions/variables?
The answer is yes. All SDL_image.h does is tell the compiler about the functions in SDL_image so it can compile your source file. Is that all it needs? Well, when you try to build your executable, the compiler will compile your source file, then the linker will try to link everything together into one binary. After it compiles your .cpp file, it will try to look up the actual definitions for the SDL_image functions. When the linker doesn't find the function definitions in one of the *.cpp files in your project, it will complain that it can't find the definitions and abort.
Well where is the actual code for SDL_image functions? The functions are compiled inside of the SDL_image binary (*.dll on windows and *.so on *nix). To get the linker to stop complaining, we give it a lib file which tells the linker where function definitions are in the SDL_image binary so it can link dynamically at the program's run time.
This might not make much sense now, but it will after we get our hands dirty by splitting up the motion tutorial source code into mulitple files.
#ifndef CONSTANTS_H
#define CONSTANTS_H
//The screen attributes
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;
//The frame rate
const int FRAMES_PER_SECOND = 20;
//The dimensions of the dot
const int DOT_WIDTH = 20;
const int DOT_HEIGHT = 20;
#endif
Here's the code inside of the constants.h file. This header file declares the constants we're going to use in the
program.
The preprocessors at the top and the bottom might be new to you. For those of you who don't know what a preprocessor is, it's basically a way to talk to the compiler. For example the #include preprocessor tells the compiler to include a file into the code.
What "#ifndef CONSTANTS_H" does is asks if CONSTANTS_H is not defined. If CONSTANTS_H is not defined, the next line defines CONSTANTS_H. Then we continue on with the code that defines the constants. Then #endif serves as the end of the "#ifndef CONSTANTS_H" condition block.
Now why did we do that?
The preprocessors at the top and the bottom might be new to you. For those of you who don't know what a preprocessor is, it's basically a way to talk to the compiler. For example the #include preprocessor tells the compiler to include a file into the code.
What "#ifndef CONSTANTS_H" does is asks if CONSTANTS_H is not defined. If CONSTANTS_H is not defined, the next line defines CONSTANTS_H. Then we continue on with the code that defines the constants. Then #endif serves as the end of the "#ifndef CONSTANTS_H" condition block.
Now why did we do that?
#include "constants.h"
#include "constants.h"
Let's say we had a situation where we included the same file twice.
In the first line where we include constants.h, the compiler will check if CONSTANTS_H is defined. Since it's not, it will define CONSTANTS_H and use the constants code inside the constants.h normally.
In the second line when we try to include constants.h, the compiler will try check if CONSTANTS_H is defined. Because CONSTANTS_H was defined already, it will skip past the code that defines the constants. This prevents the constants from being defined twice and causing a conflict.
So now you see how this simple but effective safeguard works.
In the first line where we include constants.h, the compiler will check if CONSTANTS_H is defined. Since it's not, it will define CONSTANTS_H and use the constants code inside the constants.h normally.
In the second line when we try to include constants.h, the compiler will try check if CONSTANTS_H is defined. Because CONSTANTS_H was defined already, it will skip past the code that defines the constants. This prevents the constants from being defined twice and causing a conflict.
So now you see how this simple but effective safeguard works.
#ifndef BACON
#define BACON
//The screen attributes
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;
//The frame rate
const int FRAMES_PER_SECOND = 20;
//The dimensions of the dot
const int DOT_WIDTH = 20;
const int DOT_HEIGHT = 20;
#endif
Just for the sake of information, what definition you check for doesn't matter. The above code will work perfectly.
The thing is, using FILENAME_H is the common naming standard and as I have mentioned before it's important to use
a naming standard.
#ifndef CLASSES_H
#define CLASSES_H
//The dot that will move around on the screen
class Dot
{
private:
//The X and Y offsets of the dot
int x, y;
//The velocity of the dot
int xVel, yVel;
public:
//Initializes the variables
Dot();
//Takes key presses and adjusts the dot's velocity
void handle_input();
//Moves the dot
void move();
//Shows the dot on the screen
void show();
};
//The timer
class Timer
{
private:
//The clock time when the timer started
int startTicks;
//The ticks stored when the timer was paused
int pausedTicks;
//The timer status
bool paused;
bool started;
public:
//Initializes variables
Timer();
//The various clock actions
void start();
void stop();
void pause();
void unpause();
//Gets the timer's time
int get_ticks();
//Checks the status of the timer
bool is_started();
bool is_paused();
};
#endif
Here's the classes.h file which declares the Dot and Timer classes.
It would be better if I had seperate header files for the Dot and Timer classes. The general rule is you should have a header file associated with each source file, but for the sake of not having to fumble with more files than we need to, I lumped them together here.
It would be better if I had seperate header files for the Dot and Timer classes. The general rule is you should have a header file associated with each source file, but for the sake of not having to fumble with more files than we need to, I lumped them together here.
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
//The headers
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>
//File Loader
SDL_Surface *load_image( std::string filename );
//Surface blitter
void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination, SDL_Rect* clip = NULL );
//Initialization
bool init();
//File loading
bool load_files();
//Clean up
void clean_up();
#endif
Here you see functions.h which has the declarations for the various utility functions. At the top we include the
SDL and string headers because the functions need to know what a string and SDL_Surface are.
#ifndef GLOBALS_H
#define GLOBALS_H
//The header
#include "SDL/SDL.h"
//The surfaces
extern SDL_Surface *dot;
extern SDL_Surface *screen;
//The event structure
extern SDL_Event event;
#endif
Here's the globals.h which contains the declarations for the global variables in the program. At the top you see
we included SDL.h because the header needs to know what an SDL_Surface is.
Now the extern keyword you see infront of the global variables might be new to you. Well remember that header files are just supposed to inform the compiler that something exists. If we didn't have extern infront of the globals, when the header is included in a source file the compiler will create a copy of the variable for that source file. Then when the linker tries to link everything together, it will find multiple copies of the same variable and it will complain and abort.
The extern keyword just tells the compiler the variable exists somewhere. Now you won't have multiple definitions of the same variable, but where are the actual globals located?
Now the extern keyword you see infront of the global variables might be new to you. Well remember that header files are just supposed to inform the compiler that something exists. If we didn't have extern infront of the globals, when the header is included in a source file the compiler will create a copy of the variable for that source file. Then when the linker tries to link everything together, it will find multiple copies of the same variable and it will complain and abort.
The extern keyword just tells the compiler the variable exists somewhere. Now you won't have multiple definitions of the same variable, but where are the actual globals located?
//The headers
#include "SDL/SDL.h"
#include "globals.h"
//The surfaces
SDL_Surface *dot = NULL;
SDL_Surface *screen = NULL;
//The event structure
SDL_Event event;
When the linker looks for the definitions for the of the globals, it'll find them in the global.cpp source file.
Also notice this is where we initialize the globals which makes sense since these are the actual variables and not
just a declaration.
Just a tip: in case you mess up with the globals and get a multiple definiton error and then you fix the source code but the linker still complains, try rebuilding the whole project. To save time, the compiler will only recompile the source codes that have been changed, and since the source files haven't been changed, the linker stll has them compiled with multiple definitons. Rebuilding them will get rid of the old compiled code.
Just a tip: in case you mess up with the globals and get a multiple definiton error and then you fix the source code but the linker still complains, try rebuilding the whole project. To save time, the compiler will only recompile the source codes that have been changed, and since the source files haven't been changed, the linker stll has them compiled with multiple definitons. Rebuilding them will get rid of the old compiled code.
//The headers
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>
#include "constants.h"
#include "classes.h"
#include "functions.h"
#include "globals.h"
Dot::Dot()
{
//Initialize the offsets
x = 0;
y = 0;
//Initialize the velocity
xVel = 0;
yVel = 0;
}
Here's the top of the dot.cpp file which defines the Dot class.
We include the SDL headers because we use SDL_Event and SDL_Surface types. We use the constants header for the dot dimensions. The classes header has the dot declaration so that's a given. We need the functions header for applying surfaces. Lastly we need the globals header to apply stuff to the screen.
We include the SDL headers because we use SDL_Event and SDL_Surface types. We use the constants header for the dot dimensions. The classes header has the dot declaration so that's a given. We need the functions header for applying surfaces. Lastly we need the globals header to apply stuff to the screen.
#include "SDL/SDL.h"
#include "classes.h"
Timer::Timer()
{
//Initialize the variables
startTicks = 0;
pausedTicks = 0;
paused = false;
started = false;
}
Here's the top of the timer.cpp source file. All we include is the SDL header for the SDL timing functions and
the classes header for the timer declaration.
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>
#include "constants.h"
#include "functions.h"
#include "globals.h"
SDL_Surface *load_image( std::string filename )
{
//The image that's loaded
SDL_Surface* loadedImage = NULL;
//The optimized surface that will be used
SDL_Surface* optimizedImage = NULL;
//Load the image
loadedImage = IMG_Load( filename.c_str() );
//If the image loaded
if( loadedImage != NULL )
{
//Create an optimized surface
optimizedImage = SDL_DisplayFormat( loadedImage );
//Free the old surface
SDL_FreeSurface( loadedImage );
//If the surface was optimized
if( optimizedImage != NULL )
{
//Color key surface
SDL_SetColorKey( optimizedImage, SDL_RLEACCEL | SDL_SRCCOLORKEY, SDL_MapRGB( optimizedImage->format, 0, 0xFF, 0xFF ) );
}
}
//Return the optimized surface
return optimizedImage;
}
Here's the top of functions.cpp file. We use the SDL headers, the constants header, functions header and globals
header.
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>
#include "constants.h"
#include "classes.h"
#include "functions.h"
#include "globals.h"
int main( int argc, char* args[] )
{
//Quit flag
bool quit = false;
//The dot that will be used
Dot myDot;
//The frame rate regulator
Timer fps;
//Initialize
if( init() == false )
{
return 1;
}
//Load the files
if( load_files() == false )
{
return 1;
}
Here's the top of main.cpp which hosts the main() function. At the top we include all the headers we need and then
some. The main function itself is exactly the same as its single source file counterpart.