C++ and C produce object files that look almost identical to a linker, but the symbol names inside them follow different rules. A C++ compiler decorates function names with their parameter types — name mangling — so that overloaded functions can coexist as distinct symbols. A C compiler does no such thing; a function called strlen is exported under the plain name strlen. The moment you try to mix the two languages in the same program, the mismatch surfaces as unresolved-symbol errors at link time.
The fix is extern "C": a per-declaration switch that tells the C++ compiler to use C’s linkage rules for a particular function or block of functions, so the names it emits are exactly the names C code expects to find.
Three things to take away:
extern "C"is language linkage, not storage class — it controls how the compiler emits and looks up the function’s symbol name across the language boundary.- Language linkage is part of the function’s type. A
"C"linkage function pointer cannot be assigned a"C++"linkage function, even if the parameter and return types match. - The standard
extern "C++"linkage is the default; only"C"is portable beyond it. Other linkages ("Fortran","Java", etc.) are implementation-defined.
Why the problem exists
C++ allows function overloading, so the same name can refer to different functions distinguished by their parameter types. The linker, which works one symbol at a time, has no concept of overloading — every function must end up with a unique symbol. The compiler bridges the gap by mangling the name, encoding the parameter types into the symbol. The standard reference puts the example bluntly: in many C++ implementations, strlen(const char*) might be exported as something like strlen__FCcP, “making it hard to call the function from a C program, which does not know about C++ name-mangling rules.”
extern "C" turns the mangling off for the affected declarations. The compiler emits the plain, unmangled name, and a C compiler elsewhere in the build can find it.
The two directions
extern "C" is used in two scenarios. The common one is declaring a C function so C++ can call it:
// Declaring a function defined in some C library
extern "C" int strlen(const char* s);
int main(){
return static_cast<int>(strlen("hello"));
}
The C++ compiler sees the declaration, knows not to mangle the call site, and emits a reference to the bare strlen symbol that the C runtime provides. The function itself doesn’t change — it was never compiled by a C++ compiler — but the C++ side now speaks the same linkage dialect.
The less common direction is exporting a C++ function for C to call:
// shared.h — usable from both C and C++
extern "C" void process_buffer(char* data, int size);
// shared.cpp
extern "C" void process_buffer(char* data, int size){
std::vector<char> work(data, data + size); // C++ inside is fine
// ...
}
The function body can use any C++ feature it likes — templates, exceptions, the standard library — because that’s an implementation detail of the C++ side. What extern "C" constrains is the interface: the symbol name and the calling convention seen at the boundary. The signature itself must be expressible in C, which means no references in parameters, no default arguments visible to C, and no exceptions allowed to escape (a thrown exception crossing into C is undefined behaviour).
Block syntax for headers
Wrapping each declaration individually gets noisy in a multi-function header. The block form applies one linkage specification to a group of declarations:
extern "C" {
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr* addr, unsigned len);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr* addr, unsigned* len);
}
The braces here do not introduce a scope — names declared inside are at the same scope they would have been outside. The braces only delimit the linkage region.
The dual-use header pattern
A header meant to be included from both C and C++ source files needs to add the extern "C" wrapper only when compiled as C++. The standard predefined macro __cplusplus is defined by C++ compilers and undefined by C compilers, which makes the guard straightforward:
/* api.h — usable from both C and C++ */
#ifndef API_H
#define API_H
#ifdef __cplusplus
extern "C" {
#endif
int api_init(void);
void api_shutdown(void);
#ifdef __cplusplus
}
#endif
#endif
When a C compiler reads the file, the __cplusplus-guarded lines vanish entirely and what’s left is plain C. When a C++ compiler reads it, the declarations sit inside an extern "C" block. Every C library header you’ve ever used — <stdio.h>, <string.h>, <unistd.h> — uses some variant of this pattern.
Language linkage is part of the type
A subtlety with consequences: the linkage is not just a hint at the declaration site. It becomes part of the function’s type. This means function pointers carry linkage too, and assignments between mismatched linkages are rejected:
extern "C" void cfunc(int);
void cppfunc(int);
extern "C" { void (*cptr)(int); } // pointer with "C" linkage
cptr = cfunc; // OK
cptr = cppfunc; // error: function has "C++" linkage,
// but pointer expects "C" linkage
The signatures match — both take int and return void — but the linkages don’t, and the assignment fails. This matters when passing C++ functions as callbacks to C APIs that expect a "C" function pointer. The callback must itself be declared extern "C":
extern "C" void on_signal(int sig){ // matches signal()'s expected type
/* handler body */
}
std::signal(SIGINT, on_signal); // OK
A bare C++ function with the same body would fail to compile at the signal call.
The overloading constraint
C has no notion of overloading, so there can be at most one extern "C" function with a given name in the entire program. This holds even across namespaces:
namespace alpha { extern "C" void log(const char*); }
namespace beta { extern "C" void log(const char*); } // same function!
Both declarations name the same symbol at link time. If there are two definitions, the linker rejects the program; if there is one, both namespaces refer to it. This is occasionally a useful property — C symbols are global by nature — but it’s a source of surprise when a library author tries to scope a C-callable helper inside a namespace and discovers the namespace isn’t actually hiding anything from the linker.
What extern "C" does not do
A short list of common misconceptions worth getting straight:
- It does not make the function body C-compatible. The body is still C++. Templates, exceptions, RAII, and the standard library all work inside.
- It does not change calling convention on its own. On most modern platforms C and C++ already share a calling convention. Where they differ (some legacy Windows ABIs), additional attributes like
__cdeclor__stdcallmay also be needed. - It does not protect against ABI breakage. If a C++ struct’s layout changes between compilations, an
extern "C"function that takes one by pointer will still see the new layout — language linkage controls names, not memory layouts.
Takeaway
extern "C" is the bridge that lets C and C++ share symbols. It tells the C++ compiler to suppress name mangling, encodes the expected linkage into the function’s type, and gives C-callable function pointers their own incompatible-with-C++ identity. Wrap C declarations in an extern "C" block when calling them from C++; declare C++ functions extern "C" when exposing them to C; and use the #ifdef __cplusplus guard to make a single header file usable from both languages.
The rule is simple: at the language boundary, the name in the binary has to match the name the other language is looking for, and extern "C" is how C++ agrees to the C side’s spelling.