Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

5.5 Separate Compilation

C++ supports separate compilation, where different parts of a program can be compiled independently. This follows the two-stage approach of compilation and linking. With separate compilation, changes to one class do not necessarily require recompilation of all the other classes.

The compiler generates intermediate object files (.o on Linux/macOS, .obj on Windows), which are then combined by the linker into the final executable. With g++, the linking step is handled automatically as part of the compilation command.

This approach allows:

  • Classes to be compiled and tested independently.
  • Source code to be organised into reusable libraries.
  • Large projects to be maintained more efficiently.

It is therefore good practice to place each class in its own source and header files to take advantage of separate compilation.

The source code for each class is normally split into two files:

  • Header file (.h) – contains the class declaration (including method signatures).
  • Source file (.cpp) – contains the method definitions (implementations).

A separate application file will typically contain the main() function. For example, the Account class might be structured as:

  1. Account.h – class declaration.
  2. Account.cpp – class method implementations.
  3. Application.cpp – main application file.
Quiz
Select 0/1

In the context of C++ separate compilation, what is the primary role of a header (.h) file?

To contain the full implementation code for every member method to ensure the compiler can inline them easily.
To act as a temporary storage space for intermediate object code generated during the compilation phase.
To provide the class declaration and method signatures so that other parts of the program can use the class.
To store the main() function and serve as the primary entry point for the entire compiled application.
Code Cloze
C++

Standard Header File Structure

Example: Account.h:

#include <string>
using std::string; // only import what is needed
class Account {
protected:
int accountNumber;
float balance;
string owner;
public:
Account(string owner, float aBalance, int anAccountNumber);
Account(float aBalance, int anAccountNumber);
Account(int anAccountNumber);
Account(const Account &sourceAccount);
// Other member methods...
};

Example: Account.cpp:

#include "Account.h"
using namespace std;
Account::Account(string anOwner, float aBalance, int anAccNumber) :
accountNumber(anAccNumber), balance(aBalance), owner(anOwner) {}
Account::Account(float aBalance, int anAccNumber) :
accountNumber(anAccNumber), balance(aBalance), owner("Not Defined") {}
Account::Account(int anAccNumber) :
accountNumber(anAccNumber), balance(0.0f), owner("Not Defined") {}
Account::Account(const Account &sourceAccount) :
accountNumber(sourceAccount.accountNumber + 1),
balance(0.0f),
owner(sourceAccount.owner) {}
// Other method definitions...

Example: Application.cpp:

#include "Account.h"
int main() {
Account a = Account("Derek Molloy", 35.00, 34234324);
// Application logic here...
return 0;
}

Compiling Separate Compilation Projects with g++

Section titled “Compiling Separate Compilation Projects with g++”

When using g++ at the command prompt, you must compile all source files that are part of the program. For example:

Terminal window
g++ Application.cpp Account.cpp -o Application

Where:

  • Application.cpp contains main().
  • Account.cpp contains the implementation of the Account class.
  • The -o Application flag specifies the output executable name.

You can also compile in two stages:

  1. Compile each .cpp file into an object file:
Terminal window
g++ -c Account.cpp # produces Account.o
g++ -c Application.cpp # produces Application.o
  1. Link the object files into an executable:
Terminal window
g++ Account.o Application.o -o Application

This second approach is particularly useful for large projects, as only modified files need to be recompiled. With this structure, your program is modular, maintainable, and can easily scale to larger projects.

Concept Match

Match Compilation Stages

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Compiler
drag a definition here…
Linker
drag a definition here…
Object File
drag a definition here…
g++ -c
drag a definition here…

Definition Pool

Combines multiple object files and libraries into a single final executable.
An intermediate file containing machine code that has not yet been linked.
A command-line flag used to compile a source file without performing the linking step.
Translates individual source files into intermediate object files (.o or .obj).
Quiz
Select 0/1

What is a major advantage of using separate compilation for large software projects?

VS Code automates this build process through its integrated task system. When you open a C++ project and use Run and Debug, VS Code uses a tasks.json file to invoke g++ with the correct source files and compiler flags. The C/C++ Extension Pack (set up in Chapter 3) can auto-generate this file, or you can create it manually via Terminal → Configure Tasks. For multi-file projects, each additional .cpp file must be added to the args list in tasks.json so the compiler sees all translation units.

Before continuing, it is worth briefly discussing the C++ preprocessor. Preprocessor directives are instructions that are handled by the preprocessor before compilation begins. They are not part of the program itself, and they:

  • Must appear on a single line.
  • Do not end with a semicolon (;).

Common Preprocessor Directives: #include – inserts the contents of a header file.

#include <iostream>
#include "Account.h"

#define – defines a macro (often used for constants before C++11).

#define PI 3.14159

#undef – removes a macro definition.

Conditional compilation (#if, #ifdef, #ifndef, #else, #elif, #endif) – allows parts of the program to be included or excluded depending on conditions.

#ifndef MAX_WIDTH
#define MAX_WIDTH 1000
#endif

#line – changes the line number and filename shown in compiler error messages. #error – generates a compiler error and aborts compilation.

#ifndef __cplusplus
#error A C++ compiler is required for this code!
#endif

#pragma – provides compiler-specific options. Commonly used but not standardised across compilers.

Include Guards
When multiple classes depend on the same header file, the preprocessor must ensure headers are not included more than once (otherwise redefinitions would occur). This is typically done with include guards:

#ifndef CURRENTACCOUNT_H
#define CURRENTACCOUNT_H
#include "Account.h"
class CurrentAccount : public Account {
protected:
float overdraftLimit;
// ...
public:
CurrentAccount(int theNumber, string theOwner, float theOverdraftLimit);
// ...
};
#endif // CURRENTACCOUNT_H

Here, the macro CURRENTACCOUNT_H is defined the first time the header is included, and subsequent attempts to include it in the same translation unit are ignored. This enforces the One Definition Rule (ODR): declarations may appear many times, but a definition must appear only once.

Quiz
Select 0/1

What occurs if a header file is included multiple times in the same translation unit without include guards?

The linker will automatically resolve any name collisions by merging identical class definitions together.
The compiler will issue errors because the class and its members would be defined more than once (ODR violation).
The compiler will silently ignore the duplicate inclusions and only process the first instance it encounters.
The preprocessor will compress the file into a single line to save memory space during the compilation phase.
Code Cloze
C++

Implementing Include Guards

Alternative: #pragma once \n An alternative to include guards is the non-standard but widely supported:

#pragma once
#include <iostream>
class Student {
// ...
};

This instructs the compiler to include the file only once. Although simpler, it is not part of the official C++ standard and may be unreliable in unusual filesystem setups (e.g. symbolic links). Unfortunately, Include guards remain the most portable solution.

The key benefits are their simplicity and reduced chance of error as there’s no need to invent a unique macro name, and thus no risk of name collisions or typos. While technically a compiler extension, #pragma once has been adopted by virtually every major compiler (including GCC, Clang, and MSVC) for decades, making it a safe and pragmatic choice for modern C++ projects. It effectively streamlines the process of preventing multiple-inclusion errors, a common hurdle in separate compilation.

Weaknesses of Macros for Constants
The preprocessor is essentially a text substitution tool. It does not understand C++. Using #define for constants can lead to subtle and hard-to-find errors. For example:

#define PI 3.14159;

The preprocessor simply replaces PI with 3.14159;, including the semicolon. This would turn:

area = PI * radius * radius;

into:

area = 3.14159; * radius * radius; // invalid!

leading to obscure compiler errors (as the substitution is not visible in the editor window!).

Modern Alternatives to Macros
Since C++11, you should prefer constexpr or const for constants, as they are type-safe and understood by the compiler:

#include <iostream>
using namespace std;
constexpr double PI = 3.14159;
constexpr const char* EEN1097 = "EEN1097 Edge Programming";
int main() {
cout << "Welcome to " << EEN1097 << "\n";
cout << "The value of pi is " << PI << "\n";
}

This gives the output:

Welcome to EEN1097 Edge Programming
The value of pi is 3.14159

constexpr ensures that the value is a compile-time constant, and unlike #define, it participates in type checking. It can also be used in contexts such as switch statements, where true compile-time constants are required.

Quiz
Select 0/1

Why is using #define for constants generally discouraged in modern C++ projects?

The preprocessor cannot handle macros that are defined across multiple files in a separate compilation setup.
The use of macros forces the operating system to allocate significant amounts of extra RAM during execution.
Macros are restricted to only storing integer values and cannot be used for floating-point or string constants.
Macros do not participate in the compiler's type-checking system, which can lead to subtle and dangerous bugs.
Quiz
Select 0/1

Which C++11 keyword is preferred for defining constants that can be evaluated at compile time?

Important Points about the C++ Preprocessor:

  • Preprocessor directives are processed before compilation.
  • Use include guards (or #pragma once) to prevent multiple inclusions.
  • Avoid #define for constants, instead use constexpr or const.
  • The preprocessor is powerful, but its role is limited: it does not understand C++, only text substitution and conditional inclusion.

In C and early C++, programmers often used macros (#define) to implement short functions. These were expanded by the preprocessor directly into the code, effectively “inlining” the function body wherever it was called. For example:

#define square(x) x*x
int main() {
int y = square(5); // expands to y = 5*5;
}

This produces the expected result of 25.

However, macros come with serious pitfalls. For instance:

int y = square(3+4); // expands to 3+4*3+4

This evaluates as 3 + (4*3) + 4 = 19, not the expected 49, due to operator precedence rules. Macros are simply text substitutions and have no understanding of C++ types or evaluation order. They also provide no type checking, can generate confusing compiler errors, and can cause code bloat (the macro body is duplicated everywhere it is used).

The inline Keyword
Modern C++ replaces function-like macros with inline functions. An inline function behaves like an ordinary function but suggests to the compiler that the function body should be substituted directly at the call site, avoiding the overhead of a function call.

inline int square(int x) {
return x * x;
}
int main() {
int y = square(3+4); // correctly evaluates to 49
}

Unlike macros, inline functions:

  • Respect operator precedence.
  • Are type-safe (the compiler checks argument types).
  • Integrate with namespaces and scoping.
  • Produce clearer compiler error messages.

Compiler Control of Inlining (Advanced)
It is important to understand that the compiler ultimately decides whether or not to inline a function. The inline keyword is a hint, not a command. The compiler may choose not to inline a function if:

  • Optimisation settings do not allow it.
  • The function is too large or complex.
  • The function is recursive or called indirectly.

Typically, very small functions such as accessors and mutators are inlined automatically because the cost of a function call (stack setup, jumps, returns) outweighs the actual computation.

Forcing Inlining
Most compilers (including g++) support attributes that can force inlining, although this is rarely recommended.

int square_forced(int) __attribute__((always_inline));
int square_forced(int x) { // always inlined
return x * x;
}
inline int square_suggested(int x) { // suggested, not forced
return x * x;
}
int main() {
int y = square_forced(5);
int z = square_suggested(5);
cout << "y = " << y << " and z = " << z << "\n";
}

Output:

y = 25 and z = 25

On some compilers, forcing inlining may still produce warnings, especially if optimisation is disabled. For example, g++ may warn: “always inlinable function might not be inlinable”

According to the GCC documentation:

  • Functions are not normally inlined unless optimisation is enabled.
  • inline is a hint.
  • always_inline forces inlining where possible, but if the compiler cannot inline, it issues an error or warning.

Best Practice for Inlining

  • Prefer inline (or constexpr inline) functions over macros.
  • Let the compiler decide inlining based on optimisation settings.
  • Use always_inline only in very performance-critical code after profiling.
  • Avoid over-inlining: excessive inlining increases code size (“bloat”), which can hurt performance due to instruction cache misses.

Rule of thumb: Trust the compiler unless you have hard evidence (from profiling) that manual inlining is needed.

Concept Match

Match Inlining Concepts

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

inline Keyword
drag a definition here…
Code Bloat
drag a definition here…
Macro Pitfall
drag a definition here…
Type Safety
drag a definition here…

Definition Pool

A hint to the compiler to substitute the function body directly at the call site.
A benefit of inline functions over macros where the compiler verifies argument types.
An increase in binary size caused by duplicating function code in many different locations.
A text-substitution issue where operator precedence can lead to incorrect evaluation results.
Quiz
Select 0/1

How does the C++ compiler handle the 'inline' keyword in modern applications?