Causes of Multiple Definition Linker Errors in C
Multiple definition linker errors occur during the final stage of compilation when the linker encounters identical global identifiers—such as functions or global variables—defined across more than one translation unit. This article examines the core mechanics of the C compilation model that trigger these errors, detailing common mistakes like defining variables inside header files, misinterpreting header guards, ignoring symbol linkage, and changes in modern compiler defaults, along with the precise methods to prevent them.
Defining Global Variables in Header Files
The most frequent cause of a multiple definition error is declaring
and initializing a variable inside a header file (.h). When
a C preprocessor encounters an #include directive, it
pastes the entire contents of the header file directly into the source
file (.c), creating an independent translation unit.
If config.h contains:
int max_connections = 100;And both server.c and client.c include
config.h, the compiler generates an object file for each
(server.o and client.o), both containing a
strong definition for the symbol max_connections. When the
linker attempts to combine these object files into a single binary, it
detects two conflicting symbols with the same name and halts with a
"multiple definition" error.
Solution: Declare the variable as
extern in the header file and define it in exactly one
.c file:
// config.h
extern int max_connections;
// config.c
int max_connections = 100;Function Definitions in Header Files
Just like variables, defining non-static and non-inline functions directly in header files creates duplicate symbol definitions in every translation unit that includes that header.
// math_utils.h
int add(int a, int b) {
return a + b;
}If multiple .c files include math_utils.h,
each resulting object file exports the symbol add, causing
a collision at link time.
Solution: Separate the function declaration from its
definition by placing the prototype in the header and the body in a
dedicated .c file:
// math_utils.h
int add(int a, int b);
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}Alternatively, if a function must stay in a header file (for
instance, small helper functions), mark it static inline.
The static keyword limits the symbol's visibility
exclusively to the current translation unit, preventing external linkage
collisions.
Misunderstanding Header Guards
A common misconception among C developers is that include guards
(#ifndef, #define, #endif) or
#pragma once prevent multiple definition linker errors.
Header guards only prevent a header file from being included multiple
times within the same translation unit (a single
.c file). They have no effect across different
translation units. When the compiler runs on fileA.c and
then on fileB.c, preprocessor macros are completely reset
between the two runs. Consequently, guards cannot prevent the linker
from encountering identical symbols defined across different object
files.
Missing
static on Unit-Local Identifiers
In C, variables and functions declared at file scope have external
linkage by default. If two completely independent .c files
define a global variable or helper function using the same name without
intending to share it, a collision will occur.
// file_a.c
int buffer_index = 0;
// file_b.c
int buffer_index = 0;Solution: Use the static storage class
specifier for any file-scope variable or function that is intended to
remain private to that file:
// file_a.c
static int buffer_index = 0;
// file_b.c
static int buffer_index = 0;The static specifier forces internal linkage, which
instructs the compiler not to export the symbol to the linker's global
symbol table.
The Modern
-fno-common Compiler Default
Historically, older versions of GCC and Clang used the
-fcommon flag by default. Under this model, an
uninitialized global variable (a "tentative definition," such as
int counter; in a header) was treated as a weak symbol. The
linker would automatically merge multiple instances into a single memory
location without raising an error.
Starting with GCC 10 and Clang 11, the default changed to
-fno-common. In modern compilers, tentative definitions in
headers are treated as strong definitions, causing legacy code bases
that relied on -fcommon to fail with multiple definition
errors upon compilation. Projects migrating to modern toolchains must
replace these tentative definitions with proper extern
declarations to resolve the conflict.