C Interoperability

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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 cleanlyDoes NOT cross
Numbers: int, double, charstd::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-parametersC++ exceptions (throw)
Unmangled extern "C" functionsOverloads, 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.

#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)    { return x * x * x; }
}

int main() {
    // You call them exactly like normal C++ functions.
    cout << "add(2, 3)   = " << add(2, 3) << endl;     // add(2, 3)   = 5
    cout << "square(4)   = " << square(4) << endl;     // square(4)   = 16
    cout << "cube(3)     = " << cube(3) << endl;       // cube(3)     = 27

    // The DIFFERENCE is invisible here but vital at LINK time:
    // 'add' exports the symbol  add  (not a mangled name like _Z3addii),
    // so a C compiler — or any other language — can find and call it.
    return 0;
}

// ✅ Expected output:
//    add(2, 3)   = 5
//    square(4)   = 16
//    cube(3)     = 27

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.

#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, int n);
//
//   #ifdef __cplusplus
//   }                       // close the extern "C" block (C never sees it)
//   #endif
//
//   #endif // MATHLIB_H
//
// A C compiler skips the extern "C" lines entirely and just sees the
// prototypes. A C++ compiler wraps them so the symbols stay unmangled.

// Here is the matching implementation, compiled in this one file:
extern "C" {
    int add(int a, int b) { return a + b; }
    double average(const int* v, int n) {
        int sum = 0;
        for (int i = 0; i < n; i++) sum += v[i];
        return n ? static_cast<double>(sum) / n : 0.0;
    }
}

int main() {
    int nums[] = {10, 20, 30, 40};
    cout << "add(7, 8)      = " << add(7, 8) << endl;          // 15
    cout << "average(nums)  = " << average(nums, 4) << endl;    // 25
    return 0;
}

// ✅ Expected output:
//    add(7, 8)      = 15
//    average(nums)  = 25

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.

#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 negate(int x) { return -x; }
    int absVal(int x) { return x < 0 ? -x : x; }
}

int run() {
    cout << "multiply(6, 7) = " << multiply(6, 7) << endl;
    cout << "negate(5)      = " << negate(5) << endl;
    cout << "absVal(-9)     = " << absVal(-9) << endl;
    return 0;
}

// ✅ Expected output:
//    multiply(6, 7) = 42
//    negate(5)      = -5
//    absVal(-9)     = 9

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.

#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;          // write through the pointer
    p->y = y;
}

// Take an ARRAY as pointer + length — C strings/arrays carry no size.
extern "C" double distanceFromOrigin(const Point* p) {
    // (no <cmath> needed for the idea — just show the data crossed fine)
    return p->x * p->x + p->y * p->y;   // squared distance, kept simple
}

// A C-string out-parameter: caller owns the buffer, you fill it.
extern "C" void describe(const Point* p, char* out, int outSize) {
    snprintf(out, outSize, "(%.1f, %.1f)", p->x, p->y);
}

int main() {
    Point p;
    makePoint(&p, 3.0, 4.0);                 // pass the ADDRESS with &
    cout << "x=" << p.x << " y=" << p.y << endl;          // x=3 y=4
    cout << "dist^2 = " << distanceFromOrigin(&p) << endl; // 25

    char label[32];
    describe(&p, label, sizeof(label));       // caller-owned buffer
    cout << "label  = " << label << endl;     // label  = (3.0, 4.0)
    return 0;
}

// ✅ Expected output:
//    x=3 y=4
//    dist^2 = 25
//    label  = (3.0, 4.0)

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.

#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* result) {
    try {
        *result = cppDivide(a, b);   // call into the C++ world
        return 0;                    // success
    } catch (const exception& e) {
        cout << "  [caught in wrapper] " << e.what() << endl;
        return 1;                    // failure — NO exception escapes
    }
}

int main() {
    double r = 0.0;

    // A C caller would write exactly this style: check the return code.
    if (safe_divide(10, 2, &r) == 0)
        cout << "10 / 2 = " << r << endl;          // 10 / 2 = 5

    if (safe_divide(10, 0, &r) != 0)               // triggers the throw
        cout << "second call reported an error" << endl;

    return 0;
}

// ✅ Expected output:
//    10 / 2 = 5
//      [caught in wrapper] divide by zero
//    second call reported an error

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:

#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 ___ with  -1
    if (n < 0) return ___;

    return cppFactorial(n);   // delegate to the C++ helper
}

int main() {
    cout << "c_factorial(5)  = " << c_factorial(5) << endl;
    cout << "c_factorial(-3) = " << c_factorial(-3) << endl;
    return 0;
}

// ✅ Expected output:
//    c_factorial(5)  = 120
//    c_factorial(-3) = -1

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.

#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 = *static_cast<const int*>(a);
    int y = *static_cast<const int*>(b);
    return (x > y) - (x < y);   // -1, 0, or +1 without overflow
}

int main() {
    // C string functions still work on char arrays:
    char buf[32];
    strcpy(buf, "Hello");           // copy into the buffer
    strcat(buf, ", C!");            // append
    cout << "buf = " << buf << " (len " << strlen(buf) << ")" << endl;

    // qsort: a pure-C sort that takes a function POINTER as its callback.
    int data[] = {42, 17, 93, 5, 68};
    int n = sizeof(data) / sizeof(data[0]);
    qsort(data, n, sizeof(int), compareInts);
    cout << "sorted: ";
    for (int i = 0; i < n; i++) cout << data[i] << " ";   // 5 17 42 68 93
    cout << endl;

    // atoi: a C helper to turn text into a number.
    cout << "atoi(\"123\") + 1 = " << (atoi("123") + 1) << endl; // 124
    return 0;
}

// ✅ Expected output:
//    buf = Hello, C! (len 9)
//    sorted: 5 17 42 68 93
//    atoi("123") + 1 = 124

🔎 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

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
One C-linkage functionextern "C" int add(...)Unmangled name
A block of themextern "C" { ... }All get C linkage
Dual-language header#ifdef __cplusplusWrap prototypes
C++ string → Cstr.c_str()const char*
C vector data → Cvec.data(), vec.size()ptr + length
Return a value to Cvoid f(T* out)Out-parameter
Report an error to Creturn errorCode;Never throw

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.

#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 Reading with celsius = 100, pass its ADDRESS
    //    with &, then print both fields.
    //
    // Remember: pass the struct by POINTER (C has no references), and
    // keep the function exception-free so a C caller could use it.
    //
    // ✅ Expected output:
    //    100C = 212F

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What does extern "C" actually change about a function?

  • It rewrites the body in C
  • It makes the function faster
  • It gives the function C linkage: a plain, unmangled symbol name
  • It disables the C++ standard library inside it

Answer: It gives the function C linkage: a plain, unmangled symbol name. extern "C" affects linkage only — the symbol name and calling convention — so C and C++ agree on the name.

Why does a missing extern "C" cause an error only at link time, not while compiling?

  • Compiling checks the declaration exists; linking checks the matching definition's symbol
  • The compiler ignores C functions
  • C++ never compiles C calls
  • Linking happens first

Answer: Compiling checks the declaration exists; linking checks the matching definition's symbol. The C++ caller compiles fine against a declaration but the linker can't find the mangled name the C object never exported.

Can you overload a function declared extern "C"?

  • Yes, always
  • Only with different return types
  • Only inside a namespace
  • No — C has no mangling, so two overloads would share one symbol and collide

Answer: No — C has no mangling, so two overloads would share one symbol and collide. Overloading needs mangling to make distinct symbols; with C linkage the names collide, so each C-facing function needs a unique name.

What happens if a C++ exception escapes into C code?

  • C catches it normally
  • Behaviour is undefined — typically std::terminate and a crash
  • It is silently ignored
  • It converts to a return code automatically

Answer: Behaviour is undefined — typically std::terminate and a crash. A C frame can't unwind a C++ exception, so a C-facing wrapper must catch everything and report errors via a return code.

In the dual-language header pattern, what is the role of the __cplusplus macro?

  • It is defined only by C++ compilers, so only C++ wraps the prototypes in extern "C"
  • It enables optimizations
  • It is defined only by C compilers
  • It includes the C standard library

Answer: It is defined only by C++ compilers, so only C++ wraps the prototypes in extern "C". A C compiler skips the extern "C" lines entirely; a C++ compiler sees __cplusplus and wraps the declarations.

How should you pass a std::string's text to a C function?

  • Pass the std::string object directly
  • Cast it to void*
  • Pass str.c_str() — a const char*
  • Pass &str

Answer: Pass str.c_str() — a const char*. C doesn't know std::string's layout; pass plain data: str.c_str() for the text, vec.data()/vec.size() for arrays.

Across the C boundary, how does a C-style API return a value into a struct?

  • By reference (Point&)
  • By writing through a pointer (an out-parameter)
  • By throwing it
  • By returning a std::string

Answer: By writing through a pointer (an out-parameter). C has no references, so you pass a pointer and write through it, e.g. void makePoint(Point* p, ...).

What kind of struct is safe to pass across the C boundary?

  • Any C++ class
  • A class with virtual functions
  • A template struct
  • A POD struct: plain fields, no methods, no constructors

Answer: A POD struct: plain fields, no methods, no constructors. Only plain-old-data structs are laid out identically in both languages and safe to share byte-for-byte.

When calling C's qsort from C++, the comparator must be:

  • A capturing lambda
  • A plain function pointer (a stateless/no-capture lambda is acceptable)
  • A std::function
  • A member function

Answer: A plain function pointer (a stateless/no-capture lambda is acceptable). qsort takes a function pointer; a capturing lambda has hidden state and can't convert, but a stateless lambda can.

Memory allocated with C's malloc must be released with:

  • delete
  • delete[]
  • free
  • Either delete or free

Answer: free. Always match allocators: malloc/free and new/delete. Mixing them is undefined behaviour.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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.

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.

Related lessons