Skip to main content
    Courses/C++/C Interoperability

    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 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.

    Worked example: extern "C" for single functions and blocks

    See C linkage on one function and on a whole block.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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/catch everything and never let an exception out. Report errors with a return code.
    • 💡 Pass data, not objects: hand C a const char* from str.c_str() and a pointer+length from vec.data()/vec.size() — never the std::string or std::vector itself.
    • 💡 Match the allocator: memory from C's malloc must be released with free, never delete. Mixing the pair is undefined behaviour.

    Common Errors (and the fix)

    • Forgetting extern "C" → linker error: the linker says undefined reference to 'add' (or add referenced but not defined) because C++ looked for the mangled name _Z3addii while the C object exported plain add. Wrap the declaration in extern "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 calls std::terminate and crashes. Wrap the body in try/catch and return an error code instead of throwing.
    • Passing C++ objects to C: handing a std::string or std::vector straight to a C function makes C read a layout it doesn't understand. Pass str.c_str() and vec.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 delete on malloc memory (or free on new memory) is undefined behaviour. Always match the pair: malloc/free, new/delete.

    📋 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

    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.

    Try it Yourself »
    C++
    #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 __cplusplus header lets one file serve both C and C++ callers
    • ✅ Across the boundary pass plain data: numbers, pointers, and POD structs — not std::string or classes
    • ✅ A C-facing wrapper must try/catch everything and report errors with a return code, never a throw
    • ✅ 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service