[2026] C++ extern Linkage — External vs Internal Linkage, extern "C", and Practical Library Design
이 글의 핵심
A practical guide to C++ extern linkage: external vs internal linkage, deep coverage of extern "C", how linkage interacts with headers and the ODR, and library design patterns—with runnable examples.
Introduction
In C++, extern is the keyword you use when you want to share global variables or functions across multiple files. Understanding linkage is essential to using it correctly.
Internal linkage vs external linkage
| Concept | Meaning | Typical cases |
|---|---|---|
| External linkage | The same name in another translation unit (.cpp) can refer to one entity | Non-static functions at file scope, extern global variable declarations/definitions |
| Internal linkage | The name is visible only inside that translation unit | static file-scope variables/functions; default linkage rules for const objects (watch legacy rules); names inside an anonymous namespace |
| No linkage | Local variables, class members, etc. | Resolved by scope rules, not by the linker |
Practical angle: “If I put this in a header, do I violate the ODR?” usually comes down to linkage plus inline rules. You can put inline function definitions in a header, but definitions of globals normally live in a single .cpp.
Going deeper on extern "C"
C++ uses name mangling to implement overloading. The C ABI does not, so to link against C library symbols you must mark them with an extern "C" block—C language linkage.
- Pointer types: When you pass a C++-written callback to an
extern "C"function, the callback should also beextern "C", or you must document the calling convention explicitly. - No overloading: Under C linkage there is only one function with that linker-visible name per translation unit.
- Header idiom: Wrap declarations in
#ifdef __cplusplusso one header works from both C and C++ (same pattern as Example 3 below).
extern "C" int plugin_init(void* ctx); // symbol exported with C naming rules
Header guards, #pragma once, and linkage
Header guards (#ifndef / #define) and #pragma once prevent the preprocessor from including the same header twice in one translation unit. That is a different layer from linkage: guards prevent duplicate parsing; linkage is about how the linker merges symbols.
In practice you still treat these as one checklist:
- Declarations must be safe to include many times →
#pragma onceplus declarations only. - Definitions must appear once → put them in
.cpp, or satisfyinline/constexpr/ similar rules. externglobals:extern int x;in the header,int x = 0;in exactly one.cpp.
Guards do not “change linkage”; they let you include headers safely everywhere, which makes obeying the ODR easier.
Practical library design tips
- Wrap the public API in a namespace and avoid sprinkling globals with
extern. - Put implementation details behind internal linkage (
static, anonymous namespace) or adetailnamespace. - If you need a C API, use
extern "C"and document a stable ABI (calling convention, thread safety). - Prefer C++20 modules or a clear include hierarchy to limit what leaks from headers.
1. Basics of extern
Concept
Example C/C++ code:
// file1.cpp
int globalVar = 10;
// file2.cpp
extern int globalVar;
int x = globalVar;
Basic usage
// globals.h
extern int counter;
extern void increment();
// globals.cpp
#include "globals.h"
#include <iostream>
int counter = 0;
void increment() {
counter++;
}
// main.cpp
#include "globals.h"
#include <iostream>
int main() {
increment();
std::cout << counter << std::endl; // 1
}
2. Practical examples
Example 1: Global configuration
// config.h
#ifndef CONFIG_H
#define CONFIG_H
#include <string>
extern int maxConnections;
extern std::string serverName;
#endif
// config.cpp
#include "config.h"
int maxConnections = 100;
std::string serverName = "MyServer";
// main.cpp
#include "config.h"
#include <iostream>
int main() {
std::cout << "Max: " << maxConnections << std::endl;
std::cout << "Server: " << serverName << std::endl;
}
Example 2: Function declarations
// utils.h
#ifndef UTILS_H
#define UTILS_H
#include <string>
extern void printMessage(const std::string& msg);
extern int calculate(int a, int b);
#endif
// utils.cpp
#include "utils.h"
#include <iostream>
void printMessage(const std::string& msg) {
std::cout << msg << std::endl;
}
int calculate(int a, int b) {
return a + b;
}
// main.cpp
#include "utils.h"
int main() {
printMessage("Hello");
int result = calculate(5, 3);
printMessage(std::to_string(result));
}
Example 3: extern "C"
// c_library.h
#ifdef __cplusplus
extern "C" {
#endif
void c_function();
int c_calculate(int a, int b);
#ifdef __cplusplus
}
#endif
// c_library.c
#include "c_library.h"
#include <stdio.h>
void c_function() {
printf("C function\n");
}
int c_calculate(int a, int b) {
return a + b;
}
// main.cpp
#include "c_library.h"
#include <iostream>
int main() {
c_function();
int result = c_calculate(5, 3);
std::cout << result << std::endl;
}
3. extern vs static
Comparison
// extern (external linkage)
extern int globalVar;
// static (internal linkage)
static int localVar;
Example
Sample main setup:
// file1.cpp
int externVar = 10;
static int staticVar = 20;
// file2.cpp
extern int externVar;
// extern int staticVar; // error: not accessible
int main() {
std::cout << externVar << std::endl; // 10
}
4. Common pitfalls
Pitfall 1: Multiple definitions
Example:
// ❌ definition in header
// header.h
int globalVar = 10;
// ✅ declaration in header, definition in source
// header.h
extern int globalVar;
// source.cpp
int globalVar = 10;
Pitfall 2: Initialization order
Example:
// file1.cpp
int a = 10;
// file2.cpp
extern int a;
int b = a * 2;
// ✅ function-local static
int& getA() {
static int a = 10;
return a;
}
int b = getA() * 2;
Pitfall 3: const and extern
Example:
// ❌ const defaults to internal linkage at namespace scope (in typical cases)
// header.h
const int MAX = 100;
// ✅ explicit extern
// header.h
extern const int MAX;
// source.cpp
const int MAX = 100;
Pitfall 4: Namespaces
Example:
// ❌ polluting the global namespace
extern int counter;
// ✅ prefer a namespace
namespace MyApp {
extern int counter;
}
5. extern template
Explicit instantiation
// header.h
template<typename T>
class MyClass {
public:
void print(T value);
};
extern template class MyClass<int>;
// source.cpp
template class MyClass<int>;
// main.cpp
#include "header.h"
int main() {
MyClass<int> obj;
}
6. Another practical example
Example 1: Logger system
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <string>
extern int logLevel;
extern void log(const std::string& message);
#endif
// logger.cpp
#include "logger.h"
#include <iostream>
int logLevel = 1;
void log(const std::string& message) {
if (logLevel > 0) {
std::cout << "[LOG] " << message << std::endl;
}
}
// main.cpp
#include "logger.h"
int main() {
logLevel = 2;
log("Application started");
}
Summary
Key points
extern: participates in external linkage.- Declaration vs definition: declare in headers, define in one
.cpp(unlessinline/ etc. apply). extern "C": C language linkage for C interop.constat namespace scope: often needsexternif you want a single shared definition across TUs.staticat file scope: internal linkage.
extern vs static
| Property | extern | static |
|---|---|---|
| Linkage | External | Internal |
| Visible in other TUs | Yes | No |
| Multiple definitions | No (one definition rule) | Yes (per TU) |
| Typical use | Shared globals | File-local helpers |
Practical checklist
- Headers: declarations only (
externwhere needed). - Sources: definitions and initialization.
constglobals: addexternwhen sharing one object.- Prefer namespaces over flat globals.
- Use
extern "C"for stable C-facing APIs.