Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Multiple Source Files

Last Updated: Jul 13th, 2025

As you saw with the game states tutorial, program source codes can get pretty big pretty fast. Here you'll learn how to break your program's source into smaller, easier to manage pieces.
The state machine demo had over 1500 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.

I knew this personally because when I was making my nasty tetris clone freshman year of college, I didn't properly split up my source file and just included everything into my main.cpp file. If you can properly split your source files for your nasty tetris project, you are in a better position than I was.

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.
From LConst.h
#ifndef LCONST_H
#define LCONST_H

//Screen dimension constants
constexpr int kScreenWidth{ 640 };
constexpr int kScreenHeight{ 480 };
constexpr int kScreenFps{ 60 };

#endif
Here's the code inside of the LConst.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 LCONST_H does is asks if LCONST_H is not defined. If LCONST_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 LCONST_H condition block.

Now why did we do that?
#include "LConst.h"
#include "LConst.h"
Let's say we had a situation where we included the same file twice.

In the first line where we include LConst.h, the compiler will check if LCONST_H is defined. Since it's not, it will define LCONST_H and use the constants code inside the LConst.h normally.

In the second line when we try to include LConst.h, the compiler will try check if LCONST_H is defined. Because LCONST_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.

Note: there is also a method to make sure a header is only included once using the #pragma once preprocessor. It is widely supported but not part of the official standard so we will be doing it the standard way.
#ifndef BACON
#define BACON

//Screen dimension constants
constexpr int kScreenWidth{ 640 };
constexpr int kScreenHeight{ 480 };
constexpr int kScreenFps{ 60 };

#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.
From Dot.h
#ifndef DOT_H
#define DOT_H

#include <SDL3/SDL.h>

class Dot
{
public:
    //The dimensions of the dot
    static constexpr int kDotWidth = 20;
    static constexpr int kDotHeight = 20;

    //Maximum axis velocity of the dot
    static constexpr int kDotVel = 10;

    //Initializes the variables
    Dot();

    //Takes key presses and adjusts the dot's velocity
    void handleEvent( SDL_Event& e );

    //Moves the dot
    void move();

    //Shows the dot on the screen
    void render();

private:
    //The X and Y offsets of the dot
    int mPosX, mPosY;

    //The velocity of the dot
    int mVelX, mVelY;
};

#endif 
Here's the Dot.h file which declares the Dot class. The general rule is you should have a header file associated with each source file and one class per header/source pair. It's a loose rule that often gets broken in real professional games, but I recommend sticking to it when you can.
From LUtil.h
#ifndef LUTIL_H
#define LUTIL_H

//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

//Frees media and shuts down SDL
void close();

#endif
Here are the declarations for our utility functions. Having a bunch of stray functions can be awkward to manage which is why functions are typically tied to some class.
From Globals.h
#ifndef LGLOBALS_H
#define LGLOBALS_H

#include <SDL3/SDL.h>
#include "LTexture.h"

//The window we'll be rendering to
extern SDL_Window* gWindow;

//The renderer used to draw to the window
extern SDL_Renderer* gRenderer;

//Dot texture
extern LTexture gDotTexture;

#endif
Here's the LGlobals.h which contains the declarations for the global variables in the program. At the top you see we included LTexture.h because the header needs to know what an LTexture is.

Now the extern keyword you see infront of the global variables might be new to you. Remember that header files are just supposed to inform the compiler that something exists because the compiler compiles each cpp file independently of one another. Each cpp file is known as a translation unit and C++ compilers compile them one at a time and they aren't aware of each other until they are linked together in the linking stage.

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 translation unit and when the linker tries to link the translation units together it will complain that there are multiple copies of the same variable.

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?
From LGlobals.cpp
#include "LGlobals.h"

//Instatiate globals
SDL_Window* gWindow{ nullptr };
SDL_Renderer* gRenderer{ nullptr };
LTexture gDotTexture;
When the linker looks for the definitions for the of the globals, it'll find them in the LGlobals.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.

Now some of you may familiar with modern C++ may be asking why we aren't using inline global variables. While that will work, we lose control as to where the global variable is put into memory, which is something that may affect performance. If you're wondering how it could affect performance, you'll learn that in your computer architecture class.

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.
From Dot.cpp
#include "Dot.h"
#include "LConst.h"
#include "LGlobals.h"

Dot::Dot() :
    mPosX{ 0 },
    mPosY{ 0 },
    mVelX{ 0 },
    mVelY{ 0 }
{

}mVelY = 0;
}
Here's the top of the Dot.cpp file which defines the Dot class.

As a general rule, make sure to only include the files you need to. Just including everything can cause unnecessary overhead and dependencies when compiling.
From LUtil.cpp
#include "LUtil.h"
#include "LConst.h"
#include "LGlobals.h"

bool init()
{
    //Initialization flag
    bool success{ true };

    //Initialize SDL
    if( SDL_Init( SDL_INIT_VIDEO ) == false )
    {
        SDL_Log( "SDL could not initialize! SDL error: %s\n", SDL_GetError() );
        success = false;
    }
    else
    {
        //Create window with renderer
        if( SDL_CreateWindowAndRenderer( "SDL3 Tutorial: Multiple Source Files", kScreenWidth, kScreenHeight, 0, &gWindow, &gRenderer ) == false )
        {
            SDL_Log( "Window could not be created! SDL error: %s\n", SDL_GetError() );
            success = false;
        }
    }

    return success;
}
Here's the top of LUtil.cpp file. Another common convention to follow is to include the matching header file at the top of the include block, and then include the supplementary headers afterward. Remember how header files are included and processed sequentially, so you want to maintain a consistent order to prevent inconsistent behavior.
From main.cpp
#include "LConst.h"
#include "LTimer.h"
#include "Dot.h"
#include "LUtil.h"
#include "LGlobals.h"

int main( int argc, char* args[] )
{
    //Final exit code
    int exitCode{ 0 };

    //Initialize
    if( init() == false )
    {
        SDL_Log( "Unable to initialize program!\n" );
        exitCode = 1;
    }
    else
    {
        //Load media
        if( loadMedia() == false )
        {
            SDL_Log( "Unable to load media!\n" );
            exitCode = 2;
        }
        else
        {
            //The quit flag
            bool quit{ false };

            //The event data
            SDL_Event e;
            SDL_zero( e );

            //Timer to cap frame rate
            LTimer capTimer;

            //Dot we will be moving around on the screen
            Dot dot;

            //The main loop
            while( quit == false )
            {
                //Start frame time
                capTimer.start();

                //Get event data
                while( SDL_PollEvent( &e ) == true )
                {
                    //If event is quit type
                    if( e.type == SDL_EVENT_QUIT )
                    {
                        //End the main loop
                        quit = true;
                    }

                    //Process dot events
                    dot.handleEvent( e );
                }

                //Update dot
                dot.move();

                //Fill the background
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );

                //Render dot
                dot.render();

                //Update screen
                SDL_RenderPresent( gRenderer );

                //Cap frame rate
                constexpr Uint64 nsPerFrame = 1000000000 / kScreenFps;
                Uint64 frameNs{ capTimer.getTicksNS() };
                if( frameNs < nsPerFrame )
                {
                    SDL_DelayNS( nsPerFrame - frameNs );
                }
            }
        }
    }

    //Clean up
    close();

    return exitCode;
}
Finally The main function itself is exactly the same as its single source file counterpart. Even though this could be considered a utility function, it is convention to put the main function in its own source file.

And, yes, we did skip over the LTimer and LTexture header/source pairs. It's more of the same so there's no need to go over them specifically.