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.