What Are External Linkage and Variable Forward Declarations?

External linkage is the property that lets a name compiled into one object file be wired up to a definition compiled into a different one. An identifier that has it can be used in the file that defines it and in any other file that declares it first, so it is genuinely program-wide rather than file-wide.

The Internal linkage lesson covered the opposite arrangement, where an identifier is sealed inside a single translation unit and two files may each own an independent entity of the same name. With external linkage there is exactly one entity in the entire program, and every file that wants to touch it has to announce that the name exists.

Key Concept
External linkage puts a name in front of the linker, and the linker then has two jobs for it. The first is matching: a use of the name in one translation unit gets wired to the definition sitting in another. The second is deduplication: when a name is inline and several translation units each carry a copy, those copies get folded down so that one canonical definition survives. The Inline functions and variables lesson picks up that second job.

Two Halves of a Shared Name

Every name shared between files is made of two pieces that live in different places.

Piece Where it goes What it does
The definition exactly one source file creates the entity and reserves storage for it
A declaration every other file that uses the name tells the compiler that the name exists, and what type it has

This split exists because the compiler processes one translation unit at a time and never peeks into another file. The declaration is what satisfies the compiler. Matching that declaration to a real definition happens later, when the linker stitches the compiled object files together. Almost everything in this lesson follows from that division of labor.

Try It Locally
The examples below span two or three files, so the in-browser runner (which compiles a single file) cannot run them. Save the files side by side and compile them together, for example `g++ -std=c++20 main.cpp chemistry.cpp -o darkroom`.

Functions Already Cross File Boundaries

Functions have external linkage unless you deliberately take it away, which is why the multi-file programs earlier in the course worked at all. A function declaration in the calling file is enough for the compiler; the linker supplies the body.

chemistry.cpp:

#include <iostream>

void announceBath() // external linkage by default, so other files may call this
{
    std::cout << "Stop bath poured" << '\n';
}

main.cpp:

void announceBath(); // forward declaration; the definition lives in another file

int main()
{
    announceBath(); // the linker connects this call to the definition

    return 0;
}

Compiled together, the program prints:

Stop bath poured

Had announceBath been given internal linkage with static, the compiler would still accept main.cpp, because the declaration looks perfectly reasonable on its own. The failure would arrive at link time instead.

Which Globals Already Have External Linkage

External variable is the usual shorthand for a global that carries external linkage. Whether a global already qualifies depends on one thing only: whether it is const.

Definition at namespace scope Linkage To make it external
int g_sheetsExposed{12}; external nothing to do
const int g_developerTempC{20}; internal add extern to the definition
constexpr int g_trayCount{4}; internal add extern to the definition, though this is rarely worth it

Non-const globals are external from birth, so writing extern on their definitions adds nothing. Const and constexpr globals are internal from birth, so extern on the definition is the only way to expose them:

#include <iostream>

int g_sheetsExposed{12};               // non-const global: external linkage already
extern const int g_developerTempC{20}; // const global: extern gives it external linkage

int main()
{
    std::cout << g_sheetsExposed << ' ' << g_developerTempC << '\n';

    return 0;
}

This prints:

12 20

Declaring a Variable That Lives Elsewhere

To reach an external variable from another file you write a variable forward declaration: the extern keyword, the type, the name, and no initializer. The missing initializer is what makes it a declaration rather than a definition.

chemistry.cpp:

int g_sheetsExposed{12};               // non-const: external linkage by default
extern const int g_developerTempC{20}; // const: extern makes it external

void exposeSheet()
{
    ++g_sheetsExposed;
}

main.cpp:

#include <iostream>

extern int g_sheetsExposed;        // forward declaration of a variable defined elsewhere
extern const int g_developerTempC; // forward declaration of a const defined elsewhere

void exposeSheet();                // functions need no extern here

int main()
{
    std::cout << "Developer at " << g_developerTempC << " C" << '\n';
    std::cout << "Sheets exposed: " << g_sheetsExposed << '\n';

    exposeSheet();
    exposeSheet();

    std::cout << "Sheets exposed: " << g_sheetsExposed << '\n';

    return 0;
}

Compiled together, this prints:

Developer at 20 C
Sheets exposed: 12
Sheets exposed: 14

Notice what the count proves. main.cpp contains no definition of g_sheetsExposed, and exposeSheet increments a variable it can see directly in its own file, yet the number main prints changes. There is one object in the program, and the two files are both naming it.

When the Linker Cannot Find the Definition

The compiler believes any forward declaration you write. If nothing in the program actually defines the name, nobody finds out until link time. A common way to hit this is to forget that a const global starts out internal.

The following pair is broken and does not link:

chemistry.cpp:

const int g_developerTempC{20}; // no extern: this const has internal linkage

main.cpp:

#include <iostream>

extern const int g_developerTempC; // forward declaration

int main()
{
    std::cout << g_developerTempC << '\n';

    return 0;
}

Both files compile without complaint. Linking them produces:

/usr/bin/ld: main.o: in function `main':
main.cpp:(.text.startup+0x4): undefined reference to `g_developerTempC'
/usr/bin/ld: main.cpp:(.text.startup+0xc): undefined reference to `g_developerTempC'
collect2: error: ld returned 1 exit status

Linker diagnostics look different from compiler diagnostics because they come from a different tool. There is no source line to underline and no caret; the message names an object file and a byte offset inside it, since the source text is long gone by the time the linker runs. Adding extern to the definition in chemistry.cpp fixes it.

One Keyword, Two Jobs

Read extern by position. Sitting on a definition it upgrades linkage to external; sitting on a declaration it promises that the entity belongs to some other file. Which of the two you have written is decided by the presence of an initializer, not by the keyword. The full set of forms is short enough to memorize:

Statement Definition or declaration Effect
int g_sheetsExposed; definition uninitialized global, external linkage
int g_sheetsExposed{12}; definition initialized global, external linkage
extern int g_sheetsExposed; declaration the definition is in another file
extern int g_sheetsExposed{12}; definition legal, but suspicious enough that compilers warn
const int g_developerTempC{20}; definition internal linkage
extern const int g_developerTempC{20}; definition external linkage
extern const int g_developerTempC; declaration the definition is in another file

Function forward declarations need no equivalent keyword because a function definition carries a body and a function declaration ends in a semicolon, so the compiler can always tell them apart. Variables have no such tell. Without extern, the text int g_sheetsExposed; would have to mean both "create one of these here" and "one of these exists elsewhere", and only one of those can reserve storage.

Warning
That ambiguity cuts the other way too. If you want an uninitialized non-const global, write `int g_sheetsExposed;` with no `extern`. Adding the keyword turns your intended definition into a forward declaration, and the program will fail to link.

Marking a Non-Const Definition extern

Taken as alternatives, these two lines mean exactly the same thing, since the initializer makes each of them a definition and non-const globals are external anyway:

int g_sheetsExposed{12};        // external already
extern int g_sheetsExposed{12}; // explicitly extern, and likely to draw a warning

Compilers are allowed to flag anything they find suspicious, and this qualifies. Writing extern signals an intent to declare, while the initializer forces a definition, so the statement contradicts itself. The following program compiles and runs, but not quietly:

#include <iostream>

extern int g_sheetsExposed{12}; // extern plus an initializer

int main()
{
    std::cout << g_sheetsExposed << '\n';

    return 0;
}

GCC reports:

main.cpp:3:12: warning: 'g_sheetsExposed' initialized and declared 'extern'
    3 | extern int g_sheetsExposed{12}; // extern plus an initializer
      |            ^~~~~~~~~~~~~~~

The repair depends on which half you meant. Wanted a forward declaration? Take the initializer away. Wanted a definition? Take the keyword away.

Best Practice
Reserve `extern` for two situations: forward declaring a variable that another file defines, and defining a const global that other files must be able to reach. A non-const global definition is external on its own, so leave the keyword off it.

constexpr Refuses To Be Forward Declared

constexpr variables can be given external linkage with extern, but a constexpr forward declaration is not a thing the language offers. The following file is broken:

#include <iostream>

extern constexpr int g_trayCount; // attempt to forward declare a constexpr

int main()
{
    std::cout << g_trayCount << '\n';

    return 0;
}

GCC rejects it outright:

main.cpp:3:22: error: declaration of 'constexpr' variable 'g_trayCount' is not a definition
    3 | extern constexpr int g_trayCount; // attempt to forward declare a constexpr
      |                      ^~~~~~~~~~~

The reason is timing. constexpr is a promise that the compiler knows the value while compiling, and the compiler is looking at this file only. A value written in some other source file is not available to it, so the promise cannot be kept.

You can forward declare such a variable as plain const instead. It links, and it prints, but the compiler treats it as an ordinary runtime const:

chemistry.cpp:

extern constexpr int g_trayCount{4}; // constexpr definition with external linkage

main.cpp:

#include <iostream>

extern const int g_trayCount; // forward declared as const, not constexpr

int main()
{
    std::cout << "Trays in the sink: " << g_trayCount << '\n';

    return 0;
}

Which prints:

Trays in the sink: 4
Warning
The constant-ness that makes `constexpr` worth having does not survive the trip. In `main.cpp` above, `g_trayCount` cannot be used where a constant expression is required, such as an array length. If you need a compile-time constant in several files, share it through a header instead. The Sharing global constants across multiple files lesson covers how.

Where These Declarations Belong

Retyping extern declarations by hand in every file that needs them is one opportunity per file to write the wrong type. Put them in a header instead, and include that header in the defining file as well, so the compiler can compare the declaration against the definition.

chemistry.h:

#pragma once

extern int g_sheetsExposed;
extern const int g_developerTempC;

void exposeSheet();

chemistry.cpp:

#include "chemistry.h"

int g_sheetsExposed{12};
extern const int g_developerTempC{20};

void exposeSheet()
{
    ++g_sheetsExposed;
}

main.cpp:

#include "chemistry.h"

#include <iostream>

int main()
{
    std::cout << "Developer at " << g_developerTempC << " C" << '\n';
    std::cout << "Sheets exposed: " << g_sheetsExposed << '\n';

    exposeSheet();
    exposeSheet();

    std::cout << "Sheets exposed: " << g_sheetsExposed << '\n';

    return 0;
}

The output is identical to the hand-declared version above, but the declarations now exist once. If someone later changes the definition to long g_sheetsExposed{12};, the compiler reports a conflicting declaration in chemistry.cpp immediately, rather than leaving two files disagreeing about the type of the same object.

Being able to share a non-const global does not mean you should. The advice from the Why non-const global variables are evil lesson still applies: prefer passing values as parameters, and reserve external variables for the rare case where a single program-wide object genuinely is the design.

Summary

External linkage: a name that carries it is usable across the whole program rather than inside its own file, because the compiled files can be joined up afterwards.

What the linker does with them: two jobs. It connects identifiers across translation units, and it deduplicates inline identifiers so that one canonical definition is left.

Functions: external by default. A forward declaration in the calling file is all the compiler needs, and no keyword is required because a body distinguishes a definition from a declaration.

Variable defaults: non-const globals are external already; const and constexpr globals are internal, and need extern on the definition to become external.

Variable forward declarations: extern, type, name, no initializer. The keyword is mandatory here, because without it an uninitialized definition and a declaration would be the same text.

Two meanings of extern: with an initializer it grants external linkage to a definition; without one it declares a name defined elsewhere. Writing it on a non-const definition with an initializer is redundant and usually earns a warning.

constexpr: there is no constexpr forward declaration. A compile-time value has to be visible where it is used, and a definition parked in another source file is not. Declaring it const instead links successfully, but demotes it to a runtime constant.

Practical advice: keep extern declarations in a header, include that header in the defining file so mismatches are caught, and remember that a shared non-const global carries all the usual costs of a global.