Lesson • Advanced
C / C++ Interoperability
By the end of this lesson you'll be able to call C libraries from C++ and expose C++ functions to C using extern "C", write a header both languages can include, pass structs and pointers across the boundary safely, and avoid the traps — name mangling, escaping exceptions, and handing C++ objects to C.
What You'll Learn
- Explain name mangling and why C and C++ symbols differ
- Use extern "C" to give functions plain C linkage
- Write the #ifdef __cplusplus header both languages can include
- Call classic C APIs (qsort, strcpy, malloc) from C++
- Expose C++ to C and pass structs and pointers across the boundary
- Avoid the pitfalls: escaping exceptions, overloading, C++ objects in C
💡 Real-World Analogy
Think of a function name as a label on a parcel. C++ writes a detailed label that encodes the argument types — add(int,int) becomes something like _Z3addii — so it can tell two functions called add apart (that's overloading). C writes a plain label: just add. When a C courier goes looking for the parcel labelled add but C++ filed it under _Z3addii, delivery fails — that's a linker error. extern "C" tells the C++ side to use the plain label, so both couriers agree on the address. Everything in this lesson is about keeping the labels matched.
📊 What Crosses the Boundary — and What Doesn't
| Crosses cleanly | Does NOT cross |
|---|---|
Numbers: int, double, char | std::string, std::vector (C++ types) |
Pointers: int*, const char*, void* | References (int&) — C has none |
| POD structs (plain data, no methods) | Classes with methods / virtuals |
| Return codes, out-parameters | C++ exceptions (throw) |
Unmangled extern "C" functions | Overloads, templates, namespaces |
Rule of thumb: anything C understands is plain data and plain functions. The C++ features that need name mangling (overloading, templates, namespaces) are exactly the ones that can't cross.
1. Name Mangling & extern "C"
Name mangling is how C++ encodes a function's argument types into its symbol name so that print(int) and print(double) can coexist — that's what makes overloading possible. C doesn't mangle: a function is just its bare name. So if C++ and C try to link to the same function, the names won't match. extern "C" is the fix: it tells the C++ compiler to give a function C linkage — the plain, unmangled name and C calling convention — so both languages can find it. Run this and notice the functions behave normally; the difference is invisible until link time.
Worked example: extern "C" for single functions and blocks
See C linkage on one function and on a whole block.
#include <iostream>
using namespace std;
// C++ "mangles" function names to encode their argument types, which is
// how overloading works. C does NOT mangle — a function is just its name.
// extern "C" says: "give this C-linkage, the plain unmangled name."
// A single function with C linkage:
extern "C" int add(int a, int b) {
return a + b;
}
// A whole BLOCK of functions with C linkage — the usual style:
extern "C" {
int square(int x) { return x * x; }
int cube(int x) { retu
...In a real project the declarations live in a header shared by both languages. The trick is the __cplusplus macro, which only a C++ compiler defines. You wrap the prototypes in extern "C" only when C++ is reading the header, and a C compiler skips those lines entirely. This is the interop pattern — memorise its shape.
Worked example: the #ifdef __cplusplus header pattern
The portable header both C and C++ can include.
#include <iostream>
using namespace std;
// THE portable header pattern. A real project puts this in mathlib.h so
// BOTH a C compiler and a C++ compiler can include the same file.
//
// #ifndef MATHLIB_H
// #define MATHLIB_H
//
// #ifdef __cplusplus // __cplusplus is defined ONLY by C++ compilers
// extern "C" { // so C++ sees: wrap these in C linkage
// #endif
//
// int add(int a, int b); // plain C declarations
// double average(const int* v, in
...Your turn. The program below won't link cleanly for a C caller because two functions are still mangled. Add C linkage where the comments point — fill in the two ___ blanks, then run it.
🎯 Your turn: give functions C linkage
Add extern "C" to a single function and to a block.
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — give two functions C linkage so a C program could
// link against them. Replace each ___ then press "Try it Yourself".
return run();
}
// 1) Wrap this function so its name is NOT mangled.
// 👉 put extern "C" in front of the return type
___ int multiply(int a, int b) {
return a * b;
}
// 2) Wrap a whole block of two functions with C linkage.
// 👉 the block opens with extern "C" {
___ {
int ne
...2. Passing Structs & Pointers Across the Boundary
Once the names line up, you still have to pass data C understands. C has no references and no std::string, so you use pointers and POD structs — "plain old data", a struct of fields with no methods or constructors, laid out the same way in both languages. To return a value you write through a pointer (an out-parameter), and arrays travel as a pointer plus a length, because a raw array carries no size of its own.
Worked example: structs, pointers & out-parameters
Pass a POD struct by pointer and fill it in across the boundary.
#include <iostream>
#include <cstring>
using namespace std;
// Across the C boundary you may only pass PLAIN DATA: numbers, pointers,
// and "POD" structs (plain-old-data — no methods, no constructors).
// A POD struct both C and C++ agree on, byte for byte:
struct Point {
double x;
double y;
};
// C-style API: take a POINTER to the struct (C has no references),
// fill it in by writing through the pointer.
extern "C" void makePoint(Point* p, double x, double y) {
p->x = x;
...3. Exposing C++ to C (Safely)
Going the other way — letting C call your C++ code — adds one hard rule: no C++ exception may escape into C. A C stack frame has no idea how to unwind one, so an escaping throw means undefined behaviour (usually a crash). The pattern is a thin extern "C" wrapper that try/catches everything and reports problems the C way: a return code plus an out-parameter for the result. The rich C++ logic stays safely behind the wrapper.
Worked example: a C-safe wrapper around throwing C++
Catch every exception and report errors with a return code.
#include <iostream>
#include <stdexcept>
using namespace std;
// A C++ function that can THROW. C code cannot survive a C++ exception,
// so we never let it cross the boundary.
double cppDivide(int a, int b) {
if (b == 0) throw runtime_error("divide by zero");
return static_cast<double>(a) / b;
}
// The C-facing wrapper: it CATCHES everything and reports errors the
// C way — a return code (0 = ok, non-zero = error) plus an out-param.
extern "C" int safe_divide(int a, int b, double* re
...Now you try. Below is a C++ helper and a half-finished C-facing wrapper. Give the wrapper C linkage and make it report bad input with a return value instead of throwing — fill in the two blanks:
🎯 Your turn: wrap C++ for a C caller
Add C linkage and return -1 instead of throwing.
#include <iostream>
using namespace std;
// A C++ helper. It uses C++ features, so it must stay behind a wrapper.
int cppFactorial(int n) {
int result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}
// 🎯 YOUR TURN — finish the C-facing wrapper.
// 1) Give it C linkage so a C program can call it.
// 👉 prefix the line with extern "C"
___ int c_factorial(int n) {
// 2) Report invalid input the C way: return -1 instead of throwing.
// 👉 replace ___ wit
...4. Calling Classic C APIs from C++
You'll use C libraries constantly — SQLite, OpenSSL, zlib, and POSIX are all C. The C++ standard library even ships the C headers for you: <string.h> becomes <cstring>, <stdlib.h> becomes <cstdlib>, and they already wrap their declarations in extern "C". A classic example is qsort, which takes a function pointer as a callback. Note a quirk: a capturing lambda has hidden state and cannot become a plain function pointer; a stateless lambda (no captures) can.
Worked example: strcpy, qsort & atoi from C++
Drive pure-C APIs, including a function-pointer callback.
#include <iostream>
#include <cstring> // C string functions: strlen, strcpy, strcmp...
#include <cstdlib> // C memory + utilities: malloc, free, qsort, atoi
using namespace std;
// C library headers in C++ get a 'c' prefix and drop the .h:
// <string.h> -> <cstring>, <stdlib.h> -> <cstdlib>
// They already wrap their declarations in extern "C" for you.
int compareInts(const void* a, const void* b) {
// qsort hands you void*; cast back to the real type, then compare.
int x = *st
...🔎 Deep Dive: what extern "C" changes — and what it doesn't
extern "C" affects linkage only — the symbol name and calling convention. It does not turn off C++ inside the function body: you can still use std::string, classes, and the STL in there, as long as none of it leaks across the boundary as a parameter, return type, or escaping exception.
Because the name is unmangled, you lose the features that depend on mangling: no overloading (two functions would share one symbol), no namespaces in the symbol, no templates. Each C-facing function needs one unique, bare name.
extern "C" int parse(const char* s); // ✅ plain name "parse"
extern "C" int parse(double d); // ❌ same symbol — collision!
extern "C" int run() {
std::string log = "ok"; // ✅ C++ INSIDE is fine
return (int)log.size(); // ✅ only an int crosses out
}Pro Tips
- 💡 Guard every public header: the
#ifdef __cplusplus/extern "C"pattern makes one header work for both languages — make it your default for any C-facing API. - 💡 Keep wrappers thin and total: a C-facing function should
try/catcheverything and never let an exception out. Report errors with a return code. - 💡 Pass data, not objects: hand C a
const char*fromstr.c_str()and a pointer+length fromvec.data()/vec.size()— never thestd::stringorstd::vectoritself. - 💡 Match the allocator: memory from C's
mallocmust be released withfree, neverdelete. Mixing the pair is undefined behaviour.
Common Errors (and the fix)
- Forgetting
extern "C"→ linker error: the linker saysundefined reference to 'add'(oradd referenced but not defined) because C++ looked for the mangled name_Z3addiiwhile the C object exported plainadd. Wrap the declaration inextern "C"so both sides use the same symbol. - Throwing across the C boundary: letting a C++ exception escape an
extern "C"function is undefined behaviour — it typically callsstd::terminateand crashes. Wrap the body intry/catchand return an error code instead of throwing. - Passing C++ objects to C: handing a
std::stringorstd::vectorstraight to a C function makes C read a layout it doesn't understand. Passstr.c_str()andvec.data()+vec.size()— plain pointers and lengths. - Overloading an
extern "C"function:error: conflicting declaration/ duplicate symbol, because both overloads compile to the same unmangled name. Give each C-facing function a unique name. - Mismatched allocator: calling
deleteonmallocmemory (orfreeonnewmemory) is undefined behaviour. Always match the pair:malloc/free,new/delete.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| One C-linkage function | extern "C" int add(...) | Unmangled name |
| A block of them | extern "C" { ... } | All get C linkage |
| Dual-language header | #ifdef __cplusplus | Wrap prototypes |
| C++ string → C | str.c_str() | const char* |
| C vector data → C | vec.data(), vec.size() | ptr + length |
| Return a value to C | void f(T* out) | Out-parameter |
| Report an error to C | return errorCode; | Never throw |
Frequently Asked Questions
Q: What does extern "C" actually do?
It tells the C++ compiler to use C linkage for the names inside it — no name mangling and the C calling convention. The result is a symbol the C linker recognises, so C++ code can call a C function and C code can call a C++ function you have exposed.
Q: Why do I get "undefined reference" only at link time, not while compiling?
Compiling checks that a declaration exists; linking checks that the matching definition exists. If a C++ caller looks for a mangled name like _Z3addii but the C object only defines add, compilation passes and the linker fails. Wrapping the C declaration in extern "C" makes both sides agree on the symbol name.
Q: Do I need extern "C" when I compile everything as C++?
Only when you call into something that was compiled as C — a precompiled .o/.a/.so or a C source file built by a C compiler. If every file is built by the C++ compiler, the names already match. But marking your public headers anyway is good practice, since it keeps them usable from real C callers.
Q: Can I overload a function that is declared extern "C"?
No. C has no name mangling, so two extern "C" functions would produce the same symbol and collide. Overloading, namespaces, and templates all rely on mangling, so they cannot cross the C boundary. Give each C-facing function a unique name.
Q: What happens if a C++ exception escapes into C code?
Behaviour is undefined — a C frame has no idea how to unwind a C++ exception, so it typically calls std::terminate and crashes. Any function exposed to C must catch everything and report errors through a return code or an out-parameter instead.
Q: Can I pass a C++ std::string or std::vector to a C function?
Not as the object itself — C does not know their layout, and it depends on your compiler's ABI. Pass plain data instead: str.c_str() and vec.data() / vec.size() give you the const char* and pointer+length that C understands.
Mini-Challenge: C-Callable Temperature Converter
No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Build a POD struct and an extern "C" function that fills it in through a pointer, then run it and check your output against the example in the comments.
🎯 Mini-Challenge: build a C-callable converter
Write a POD struct and a pointer-based extern "C" function.
#include <iostream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: a C-callable temperature converter
//
// 1. Write a POD struct Reading with two fields:
// double celsius;
// double fahrenheit;
//
// 2. Write an extern "C" function:
// void toFahrenheit(Reading* r);
// It reads r->celsius and fills in r->fahrenheit
// using f = c * 9.0 / 5.0 + 32.0; (write THROUGH the pointer).
//
// 3. In main: make a Readi
...🎉 Lesson Complete
- ✅ Name mangling encodes C++ argument types into the symbol; C uses plain names
- ✅
extern "C"gives a function (or a whole block) C linkage — the unmangled name - ✅ The
#ifdef __cplusplusheader lets one file serve both C and C++ callers - ✅ Across the boundary pass plain data: numbers, pointers, and POD structs — not
std::stringor classes - ✅ A C-facing wrapper must
try/catcheverything and report errors with a return code, never athrow - ✅ No overloading across the boundary, and match allocators (
malloc/free) - ✅ Next lesson: Advanced Debugging — find and fix bugs across your C and C++ code
Sign up for free to track which lessons you've completed and get learning reminders.