Skip to content
VLSI Mentor

C++ Part I · Module 2 · Building and Running C++ Programs

Source Files and Header Files

Every program so far has lived in a single file, and no real program does. Splitting one across two files is the first step toward a project, and it immediately raises a question that a single file never asks: when the code in one file calls a function written in another, how does the compiler know that function exists, what it is called, and what it expects? The answer is the header file, and headers are where a lot of C++ beginners form a vague mental model that costs them later. So this chapter builds the split from the problem rather than from the convention. We will write a two-file program, see exactly what each file is responsible for, and then meet the one rule that explains the whole arrangement — that you may say a thing exists as many times as you like, but you may only say what it is once.

Foundation15 min readC++Part IToolchainHeaders

Part I · Module 2 · Chapter 2.4 · Source Files and Header Files

1. The Problem One File Does Not Have

Here is a program in one file. It works out how many clock cycles a burst of transfers takes:

Azvya Education Pvt. Ltd.VLSI Mentor
one-file version — complete program
#include <iostream>

int cycles_for(int transfers, int cycles_per_transfer)
{
    const int setup_cycles = 3;
    return setup_cycles + transfers * cycles_per_transfer;
}

int main()
{
    std::cout << "cycles = " << cycles_for(8, 2) << '\n';
    return 0;
}

Nothing here is new except the shape of a second function, which Module 6 teaches properly. cycles_for takes two numbers and returns one. main calls it.

This is fine at twelve lines and impossible at twelve thousand. Real programs split up for reasons that are practical rather than aesthetic:

  • Rebuild cost. Change one line and the whole file is recompiled. When that file is the whole program, every edit costs a full rebuild.
  • Reuse. cycles_for is useful to more than one program. Trapped inside this file, it can only ever serve this one.
  • People. Two engineers editing the same file conflict constantly. Two engineers editing two files usually do not.

So we split it. And the moment we do, a new problem appears that did not exist before.

2. Two Files, and the Question They Raise

Put cycles_for in its own file:

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.cpp
int cycles_for(int transfers, int cycles_per_transfer)
{
    const int setup_cycles = 3;
    return setup_cycles + transfers * cycles_per_transfer;
}

And leave main in another:

Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp — will not compile yet
#include <iostream>

int main()
{
    std::cout << "cycles = " << cycles_for(8, 2) << '\n';
    return 0;
}

This fails, and the reason is the single most important fact about how C++ is built:

The compiler reads one source file at a time, and knows nothing about any other.

Chapter 1.7 established this at orientation level. Here is the first time it costs you something. When the compiler reads main.cpp, it has never seen cycles.cpp. It reaches the call to cycles_for and has no idea whether that name exists, what it takes, or what it gives back. So it stops.

The fix is to tell it. Not to give it the function — just to tell it the function exists and what shape it is.

3. Declaration and Definition

Two words that get used loosely and mean different things. Getting them straight now saves a great deal of confusion later.

A declaration says this exists, and here is its shape:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int cycles_for(int transfers, int cycles_per_transfer);

Note the semicolon where a body would be. That is a promise: somewhere, a function with this name, taking these two ints, returning an int, exists.

A definition says and here is what it does:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int cycles_for(int transfers, int cycles_per_transfer)
{
    const int setup_cycles = 3;
    return setup_cycles + transfers * cycles_per_transfer;
}

The compiler needs the declaration to check a call. It needs to know the name, the parameter types and the return type, and nothing else. It does not need the body — the body is somebody's problem later, and Chapter 2.6 shows you whose.

So this compiles:

Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp — compiles now
#include <iostream>

int cycles_for(int transfers, int cycles_per_transfer);   // declaration

int main()
{
    std::cout << "cycles = " << cycles_for(8, 2) << '\n';
    return 0;
}

The call is now checked. Pass one argument instead of two and the compiler objects. Pass a value of a type that cannot convert and it objects. The declaration is what makes checking possible.

4. Why Not Just Write the Declaration in Every File

You could. It compiles. It is also a defect waiting to happen, and the reason is worth more than the rule.

Imagine five source files each calling cycles_for, each with its own hand-typed declaration. Now the function changes — it gains a third parameter. You update the definition and four of the five declarations.

The fifth file still compiles. It still believes the old shape. Chapter 1.7 §5 showed exactly what happens next and it is not pretty: in C++ the mismatch usually shows up as a link error, which is survivable; in C the equivalent linked silently and produced a wrong answer.

The real problem is structural: five copies of one fact can disagree. The fix is to have one copy.

5. The Header

A header file is a file that holds declarations, so every source file that needs them can include the same copy.

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.hpp
int cycles_for(int transfers, int cycles_per_transfer);

And both source files include it:

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.cpp
#include "cycles.hpp"

int cycles_for(int transfers, int cycles_per_transfer)
{
    const int setup_cycles = 3;
    return setup_cycles + transfers * cycles_per_transfer;
}
Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp
#include "cycles.hpp"
#include <iostream>

int main()
{
    const int transfers = 8;
    const int cycles_per_transfer = 2;

    std::cout << "transfers = " << transfers << '\n';
    std::cout << "cycles    = " << cycles_for(transfers, cycles_per_transfer) << '\n';

    return 0;
}

Build both and link them together:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c cycles.cpp -o cycles.o
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c main.cpp   -o main.o
$ c++ cycles.o main.o -o app
$ ./app

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
transfers = 8
cycles    = 19

Three setup cycles plus eight transfers at two cycles each. That command has three steps and Chapter 2.6 explains each one; for now, notice only that each .cpp was compiled separately and something joined them afterwards.

Notice also that cycles.cpp includes its own header. That is not redundant. It is how the compiler checks that the definition matches the declaration — if they ever disagree, you find out while compiling cycles.cpp rather than at link time or, worse, at run time.

6. What Goes in Which File

The rule, in one line:

Headers say what exists. Source files say what it does.

Put in the header (.hpp)Put in the source file (.cpp)
Function declarationsFunction definitions (the bodies)
Type definitions — struct, class, enumVariables the rest of the program should not see
Constants other files needHelper functions nobody else calls
Comments explaining how to use the thingComments explaining how it works

The asymmetry in the first row catches people out and leads directly to the next section: a function goes in the header as a declaration only, but a type must be defined in the header. The compiler cannot check Burst b; without knowing what a Burst is — its members, its size. A declaration would not be enough.

That matters because it means headers contain real definitions, and definitions have a rule that declarations do not.

Module 24.2 develops this into a full policy, including the cases this simple table does not cover. The one-line rule is enough for now and is right most of the time.

7. The Rule That Explains Include Guards

Here is a measurement rather than an assertion. Declare the same function three times in one file:

Azvya Education Pvt. Ltd.VLSI Mentor
decl_twice.cpp — three identical declarations
int cycles_for(int transfers, int cycles_per_transfer);
int cycles_for(int transfers, int cycles_per_transfer);
int cycles_for(int transfers, int cycles_per_transfer);

int main() { return 0; }

Compiles clean, no warnings, exit 0. Repeating a declaration is legal. You are saying the same true thing three times, and the compiler has no objection.

Now define the same type twice:

Azvya Education Pvt. Ltd.VLSI Mentor
def_twice.cpp — INCORRECT. Two definitions of one type.
struct Burst { int transfers; };
struct Burst { int transfers; };

int main() { return 0; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def_twice.cpp:2:8: error: redefinition of 'Burst'

Repeating a definition is not legal — even when the two are identical. The compiler will not try to work out whether two definitions agree; one definition is the rule.

Hold those two results together, because they explain everything in the next section:

You may say a thing exists as often as you like. You may say what it is only once.

8. Why a Header Needs a Guard

Now the failure this causes. Here is a header holding a type:

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.hpp — no guard
struct Burst {
    int transfers;
    int cycles_per_transfer;
};

int cycles_for(int transfers, int cycles_per_transfer);

And a file that includes it twice:

Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp — INCORRECT. Does not compile.
#include "cycles.hpp"
#include "cycles.hpp"          // included twice, on purpose

int main() { return 0; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./cycles.hpp:1:8: error: redefinition of 'Burst'
main.cpp:1:10: note: './cycles.hpp' included multiple times, additional include site here
main.cpp:2:10: note: './cycles.hpp' included multiple times, additional include site here
./cycles.hpp:1:8: note: unguarded header; consider using #ifdef guards or #pragma once

Read what the compiler did there. It reported the error at the header, then named both places the header was included from, then told you the category of mistake and suggested the fix. Diagnostics are frequently this helpful and frequently not read.

Nobody writes that main.cpp on purpose. What happens in practice is indirect: your file includes two headers, and both of them include a third. You never see it coming, and the error points at a header you did not touch.

The fix is a mechanism that makes the second inclusion do nothing:

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.hpp — with an include guard
#ifndef CYCLES_HPP
#define CYCLES_HPP

struct Burst {
    int transfers;
    int cycles_per_transfer;
};

int cycles_for(int transfers, int cycles_per_transfer);

#endif

Including this twice now compiles cleanly. In plain language:

  • #ifndef CYCLES_HPP — if the name CYCLES_HPP has not been defined, keep reading.
  • #define CYCLES_HPP — define it now.
  • #endif — end of the conditional part.

The first time through, the name is not defined, so the contents are used and the name gets defined. The second time, the name is already defined, so everything down to #endif is skipped. The header is read twice and its contents arrive once.

This is the include guard, and you put one in every header you write. Three practical notes:

  • The name must be unique in the program. Two headers sharing a guard name means the second one silently produces nothing, which is a genuinely horrible bug to find. Name it after the file.
  • These are preprocessor directives, not C++. They are handled before the compiler sees the file, which is exactly why they can make text disappear. Chapter 2.5 is about that machinery.
  • There is another way to do this, which the compiler's own note mentioned: #pragma once. It is shorter and almost universally supported, and it is not standard C++. Module 24.3 compares the two properly. Use guards for now; they work everywhere.

9. Mistakes Worth Making Once

Putting a function definition in a header. It compiles as long as exactly one source file includes it. Include it from two and you get a duplicate-symbol error at link time — Chapter 2.7 shows that message. The rule in §6 exists for this reason.

Forgetting the guard. Works until a second header reaches yours indirectly, then fails pointing at a file you did not edit.

Copy-pasting a guard name. Copy cycles.hpp to burst.hpp and forget to change CYCLES_HPP, and whichever is included second contributes nothing. The symptom is "this type is undefined" in a file that clearly includes the header that defines it.

Not including the header from its own source file. cycles.cpp includes cycles.hpp so the compiler can check that the definition matches the declaration. Skip it and a mismatch survives until link time.

Assuming the header's declarations are the program. A declaration is a promise. Compiling main.cpp succeeds on the promise alone; something still has to keep it. That is Chapter 2.6.

10. What You Should Be Able to Say Now

  1. The compiler reads one source file at a time and knows nothing about the others. That is why a split program needs headers at all.
  2. A declaration says a thing exists and gives its shape; a definition says what it does. A call can be checked from the declaration alone.
  3. Hand-writing the same declaration in several files means several copies of one fact that can disagree. A header gives you one copy.
  4. #include "x.hpp" is for your headers; #include <x> is for the compiler's and libraries'. The exact search order is a toolchain matter.
  5. Headers say what exists; source files say what it does. Functions go in headers as declarations; types must be defined there, because the compiler cannot use a type it only knows the name of.
  6. Measured: repeating a declaration three times is legal; defining one type twice is an error, even when identical.
  7. Therefore a header containing a type definition and reached twice is an error — which is what include guards prevent.
  8. A guard is #ifndef / #define / #endif with a name unique to the file. #pragma once is the common alternative and is not standard; Module 24.3 compares them.
  9. A source file should include its own header, so a mismatch between definition and declaration is caught while compiling rather than later.
  10. Defining a function in a header works until two source files include it, and then fails at link time.

11. Exercises

Exercise 1 — sort them. For each, say whether it belongs in the header or the source file, and give the reason in one phrase: (a) int cycles_for(int, int); (b) the body of cycles_for (c) struct Burst { int transfers; }; (d) a helper function only cycles.cpp uses.

Exercise 2 — explain the asymmetry. A function goes in a header as a declaration only, but a struct must be fully defined there. Explain why, in terms of what the compiler needs in order to use each one.

Exercise 3 — predict two results. Without compiling: (a) the same function declared four times in one file; (b) the same struct defined twice in one file. State the outcome of each and the single rule that covers both.

Exercise 4 — trace the indirect failure. a.hpp and b.hpp both include types.hpp, which has no guard. main.cpp includes a.hpp and b.hpp. Describe what the compiler reports, which file it names, and why the engineer's first instinct will be to look in the wrong place.

Exercise 5 — the duplicated guard name. You copy cycles.hpp to burst.hpp and forget to change the guard name. main.cpp includes cycles.hpp then burst.hpp. Describe exactly what the compiler sees and what error eventually appears. Say why this is harder to diagnose than a missing guard.

Exercise 6 — defend a convention. Why should cycles.cpp include cycles.hpp even though it does not need any declaration from it? Give the specific failure this prevents and say when that failure would otherwise be discovered.

Exercise 7 — the promise. main.cpp compiles successfully against the header alone, with no access to cycles.cpp. Explain what the compiler accepted on trust, and name the tool whose job it is to check that the trust was warranted.

12. Summary

  • One file does not scale — rebuild cost, reuse and people all push toward a split.
  • The compiler reads one source file at a time. A call in one file cannot be checked without being told, in that file, what it is calling.
  • A declaration says a thing exists; a definition says what it does. Calls are checked from declarations.
  • A header is one copy of the declarations, so several source files cannot disagree about one fact.
  • Quotes for your headers, angle brackets for the system's.
  • Headers say what exists; source files say what it does — but types must be defined in the header, because a name alone is not usable.
  • Declarations may repeat; definitions may not. Measured: three declarations fine, two identical struct definitions an error.
  • That is why headers need include guards. #ifndef / #define / #endif, named uniquely after the file.
  • A source file should include its own header, so a definition that drifts from its declaration is caught early.

Include guards work by making part of a file disappear before the compiler ever sees it. That is a strange power for something that looks like ordinary code, and it only makes sense once you know that #include and #define are not C++ at all — they are handled by a separate step with its own rules, running over your file as plain text. Chapter 2.5 opens that step up and measures what it did to the twelve-line program from Chapter 2.2.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.