Careers[careers]

C++ vs C: what differs, what breaks, which to choose

Section
Careers [careers]
Standard
C++23
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
14 min read
In this answer8 clauses
  1. 1What C++ adds, next to the C it replaces
  2. 2Valid C that a C++ compiler rejects
  3. 3Code that compiles as both and means something else
  4. 3.1sizeof(‘a’): an int in C, a char in C++
  5. 3.2int f(): any arguments in C17, none in C23 and C++
  6. 4C23 moved toward C++
  7. 5Calling C from C++ and C++ from C: extern "C"
  8. 6Is C faster than C++?
  9. 7Where C and C++ are used
  10. 8C or C++: which to choose

Abstract

C++ is not a superset of C: some valid C, such as malloc without a cast, is ill-formed in C++, and sizeof(‘a’) is the size of an int in C but 1 in C++. C++ adds classes, destructors, templates, references, overloading, exceptions, namespaces and a library of containers and algorithms; C23 moved C toward C++ with nullptr, constexpr objects and auto. Choose C++ for new application code, C for a C codebase or a C-only platform, and join the two with extern “C”.

Two overlapping rectangles: a wide, short one for C on the left and a larger one for C++ on the right. Their overlap is hatched; the part of C outside C++ is filled magenta.
Figure 1 — C and C++ overlap, and neither contains the other: some valid C is not valid C++.

C++ vs C: C++ grew out of C and compiles most of it, but it is not a superset. Some valid C is an error in C++ (malloc without a cast); some compiles in both with another meaning (sizeof('a') is the size of an int in C, 1 in C++). C++ adds classes, destructors, templates, references, overloading, exceptions, namespaces and a library of containers and algorithms. Choose C++ for new application code, as the C++ Core Guidelines do; choose C when the codebase or the platform is C, give a library that other languages call a C interface, and join the two with extern "C".

This comparison is written for C++23 and C23 (ISO/IEC 9899:2024), with C17 where C23 changed the answer. C++20 added designated initializers, in declaration order only; C23 gave C nullptr, bool as a keyword, constexpr objects and auto.

The samples were built as C17, C23 and C++23 with four compilers on three standard libraries: MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64). The compilers and their libraries have their own page, and Since When gives the first release of each C++ feature.

MSVC took /W4 with /TC /std:c17, /TC /std:clatest or /std:c++23preview /permissive- /EHsc; the others took -Wall -Wextra with -std=c17, -std=c23 or -std=c++23. A .c file built as C++ added /TP or -x c++.

In the figure, the overlap is the common subset. The C-only side is what standard C++ rejects, though GCC and Clang accept some of it in C++ mode as extensions.

Two overlapping boxes, C and C++. Only in C: _Generic, restrict, variable-length arrays and more. In both: functions, structs, pointers. Only in C++: destructors, templates, references and more.
Figure 2 — Most C is also C++, but each language has parts the other rejects.

What C++ adds, next to the C it replaces

Job In C In C++
Release a resource free on every path, goto cleanup, or the cleanup attribute (a GCC and Clang extension) a destructor (the rule of zero)
Grow an array malloc and realloc std::vector
Hold text a char array and strlen std::string
Pass a range a pointer and a length std::span
One function for many types void * and a function pointer, a macro, or _Generic (C11) a template
Dispatch on a type at run time a struct of function pointers a virtual function
Report an error a return code a return code, std::expected or an exception
Avoid name clashes a prefix a namespace

The running example reads sensor readings from text, keeps the distinct ones sorted and prints them. In C, growing the array and freeing it on every path is your code:

[Example 1:
    size_t count = 0, capacity = 4;
    int *readings = malloc(capacity * sizeof *readings);
    if (!readings)
        goto cleanup;
    for (char *end;; text = end) {
        long value = strtol(text, &end, 10);
        if (end == text)
            break;                      // no more numbers
        if (count == capacity) {
            capacity *= 2;
            int *grown = realloc(readings, capacity * sizeof *grown);
            if (!grown)
                goto cleanup;           // readings is still ours
            readings = grown;
        }
        readings[count++] = (int)value;
    }
    qsort(readings, count, sizeof *readings, compare);

— end example]

The duplicates are skipped while printing, and cleanup: calls free. In C++, the vector grows itself and its destructor frees the memory:

[Example 2:
    std::vector<int> readings;
    for (auto word : std::views::split(text, ' ')) {
        int value = 0;
        auto parsed = std::from_chars(word.data(),
                                      word.data() + word.size(), value);
        if (parsed.ec != std::errc{})
            break;                      // no more numbers
        readings.push_back(value);
    }
    std::sort(readings.begin(), readings.end());
    auto duplicates = std::unique(readings.begin(), readings.end());
    readings.erase(duplicates, readings.end());

— end example]

It parses with std::from_chars (C++ string to int) and prints with std::print (std::println and std::print). The C file built as C17 and C23 and the C++ file as C++23 all printed:

[Example 3:
7 12 18 30

— end example]

The C file is not C++: its malloc line is the first row of the next table, and its goto cleanup jumps past the initialization of sep, the separator it declares after the loop for printing: the goto row.

Valid C that a C++ compiler rejects

Standard C++ rejects each construct in this section’s table; the compilers do not all hold to it. The smallest case:

[Example 4:
    size_t count = 8;
    int *readings = malloc(count * sizeof *readings);

— end example]

C converts a void * to any object pointer implicitly; C++ needs a cast ([diff.expr], one of the standard’s differences from ISO C). Built as C++23, all four rejected it. GCC 16.2 said:

[Example 5:
02-malloc.c:7:27: error: invalid conversion from 'void*' to 'int*' [-fpermissive]

— end example]

Every row compiled as C17 and C23 on all four, except the variable-length array on MSVC: VLAs have been optional since C11, C23 made variably modified types mandatory but still leaves automatic VLAs optional, and MSVC defines __STDC_NO_VLA__ in both modes. Built as C++23 (MSVC with /permissive-; with /permissive it also accepted the string literal and the goto), “error” means rejected, “warns” compiled with a warning, “silent” compiled without one:

Valid C MSVC 19.44 GCC 16.2 Clang 23.1
malloc without a cast error error error
char *unit = "mV"; error warns warns
char tag[4] = "temp"; error error error
new, class, template as names error error error
enum unit u = 1; error error error
int count; twice at file scope error error error
goto past int readings = 3; error error error
designators out of order, nested, mixed or for an array error error warns
compound literal (struct reading){1, 42} error silent silent
variable-length array int readings[count]; error, as in C silent warns
restrict error error error
_Generic error error silent
flexible array member int readings[]; warns silent silent
  • Clang 19.1 matches Clang 23.1 on every row except the goto, which it accepts on its Windows target as “a Microsoft extension”.
  • A string literal is const in C++, and since C++11 it no longer converts to char *, so char *unit = "mV"; is ill-formed there. Only MSVC rejected it, and only under /permissive- (which /std:c++20 and /std:c++latest imply); with /permissive it compiled silently. GCC and both Clangs warn.
  • With -pedantic-errors, GCC 16.2 and Clang 23.1 rejected every row.
  • GCC rejected {[2] = 5} with “sorry, unimplemented”, yet compiled {[0] = 5, [1] = 6, [2] = 7}, in order, without a warning; with -pedantic-errors it rejected that too: “ISO C++ does not allow C99 designated initializers”.
  • GCC’s errors for malloc, char tag[4] and the enum end in [-fpermissive], and that flag turns all three into warnings. Do not port C with it: it lets those three through with only a warning.

The table is not the whole list: the standard’s annex, [diff.iso], has the rest, such as a register variable, ill-formed since C++17.

Most rows have a spelling that is valid in both languages. These lines compiled without a warning as C17, C23 and C++23 on all four:

[Example 6:
    int *readings = (int *)malloc(4 * sizeof *readings);
    const char *unit_name = "mV";
    char tag[5] = "temp";               // room for the '\0'
    int fresh = 1;                      // not "new"
    enum unit u = VOLT;                 // an enumerator, not an int
    struct reading r = {.id = 1, .value = 42};  // declaration order
    struct reading next = {2, 43};      // a named object

— end example]

For restrict, the __restrict spelling built on all four, as C and as C++. No spelling of a variable-length array, _Generic or a flexible array member is valid in both languages; in C++, overloading or a template does what _Generic does.

Code that compiles as both and means something else

sizeof(‘a’): an int in C, a char in C++

[Example 7:
    printf("sizeof('a')  = %zu\n", sizeof('a'));
    printf("sizeof(char) = %zu\n", sizeof(char));

— end example]

Built as C17 and C23, all four printed:

[Example 8:
sizeof('a')  = 4
sizeof(char) = 1

— end example]

Built as C++23, all four printed sizeof('a') = 1. A character literal has type int in C and char in C++ ([diff.lex]).

int f(): any arguments in C17, none in C23 and C++

[Example 9:
int count();
// ...
    printf("%d\n", count(3, 4));
// ...
int count(int first, int last)

— end example]

In C17, int count(); declares a function without saying its parameters. C23 made it mean int count(void) (N2841, which removed declarations without a prototype), as it does in C++ ([diff.dcl]):

Built as GCC 16.2 and the Clangs MSVC 19.44
C17 prints 2; the Clangs warn -Wdeprecated-non-prototype prints 2
C23 error: too many arguments prints 2
C++23 error error

Two more, from the standard and cppreference rather than from a build:

  • A const object at file scope that is not declared extern has internal linkage in C++ and external linkage in C ([diff.basic]).
  • Reading a union member other than the one last written reinterprets its bytes in C (since C99 TC3, DR 283; bytes past the member last written are unspecified) and is undefined behavior in C++, though many compilers implement it as an extension.

C23 moved toward C++

C23 added these features, most of them already in C++ (cppreference’s C23 list). Built as C23:

C23 feature MSVC 19.44 GCC 16.2, both Clangs Built as C17
nullptr no yes error on all four
bool, true, false as keywords no yes error on all four
constexpr objects no yes error on all four
auto no: implicit int yes MSVC implicit int, others error
[[nodiscard]], [[maybe_unused]], [[fallthrough]] yes yes MSVC error, others silent
static_assert without a message yes yes builds on all four; the Clangs warn
typeof yes yes error on all four
digit separators, 1'000'000 yes yes MSVC silent, others error
binary literals, 0b1010 yes yes silent on all four
enum unit : unsigned char no yes MSVC error, others silent
_BitInt(12) no yes MSVC error, others silent

Several features compiled silently in C17 mode too (the “Built as C17” cells), so a clean build does not tell you which C standard a line needs. The auto row is the one that compiles on MSVC and gives a different answer:

[Example 10:
    auto count = 3;                     // C23: deduced as int
    auto ratio = 2.5;                   // C23: deduced as double
    printf("%zu %zu\n", sizeof count, sizeof ratio);

— end example]

As C23, GCC 16.2 and the Clangs printed 4 8. MSVC 19.44 read both as an implicit int, warned, and printed:

[Example 11:
04-c23-auto.c(6): warning C4431: missing type specifier - int assumed. Note: C no longer supports default-int
04-c23-auto.c(7): warning C4431: missing type specifier - int assumed. Note: C no longer supports default-int
04-c23-auto.c(7): warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data
4 4

— end example]

#embed (C23) puts a file’s bytes into an initializer:

[Example 12:
static const unsigned char head[] = {
#embed __FILE__ limit(8)
};

— end example]

As C23, GCC 16.2 and both Clangs printed 8 bytes, starting "//". C++26 has it too: P1967R14 was voted into the C++ working draft at the February 2025 meeting in Hagenberg ([cpp.embed]). As C++, the compilers split:

  • GCC 16.2 accepts it, and under -pedantic-errors objects only before C++26: “‘#embed’ before C++26 is a GCC extension”.
  • Clang 23.1 accepts it, but still warns “#embed is a Clang extension” in C++26 mode, as it does in C++23.
  • MSVC 19.44 has no #embed in any mode (fatal error C1021).

_BitInt has no standard C++ spelling either; both Clangs compiled _BitInt(12) as C++23, GCC did not.

Calling C from C++ and C++ from C: extern "C"

extern "C" gives a function C language linkage, the calling convention and name decoration that let code in another language link to it; how the name is represented is up to the implementation ([dcl.link]). Link the mixed program with the C++ compiler: the isocpp.org FAQ on mixing C and C++ says “Your C++ compiler should direct the linking process (e.g., so it can get its special libraries)”. A header that both languages include hides the extern "C" from C:

[Example 13:
#ifdef __cplusplus
extern "C" {
#endif

int count_readings(const char *text);

#ifdef __cplusplus
}
#endif

— end example]

The one file built as C17 and as C++23 on all four, and printed 3 readings both ways. C linkage has no overloading:

[Example 14:
extern "C" int parse(const char *text);
extern "C" int parse(const char *text, int base);

— end example]

All four rejected the second declaration. MSVC:

[Example 15:
05b-no-overload.cpp(4): error C2733: 'parse': you cannot overload a function with 'extern "C"' linkage

— end example]

Inside, the function is C++. This one parses with std::string, std::vector and std::stoi, and catches every exception before it reaches a C caller:

[Example 16:
extern "C" int sum_readings(const char *text)
{
    try {
        std::vector<int> readings;
        std::string word;
        // ...
        int total = 0;
        for (int reading : readings)
            total += reading;
        return total;
    } catch (...) {
        return -1;                      // no exception reaches C
    }
}

— end example]

On all four, sum_readings("12 7 30") returned 49, and sum_readings("12 x 30") returned -1 after std::stoi threw on the x. Microsoft documents why the catch matters: under /EHsc, the setting these samples used, MSVC assumes an extern "C" function never throws, and an exception thrown from one is undefined behavior. GCC compiles C without -fexceptions unless told to, and its manual says C code that must work with C++ exception handlers may need the option.

Between two C++ compilers that do not share an ABI, rule I.26 of the C++ Core Guidelines asks for a C-style subset, because “Different compilers implement different binary layouts for classes, exception handling, function names, and other implementation details”; it adds that common ABIs are emerging on some platforms.

Is C faster than C++?

Stroustrup’s FAQ says: “Every C program can be written in essentially the same way in C++ with the same run-time and space efficiency.” It adds that “C” there means Classic C and C89; C99’s restrict has no standard C++ spelling, only the compilers’ __restrict. The Core Guidelines’ rules are meant to follow the zero-overhead principle, “what you don’t use, you don’t pay for”. The samples timed nothing; they measured sizes, the same on all four (64-bit targets):

Job C C++ Bytes, C and C++
four ints int[4] std::array<int, 4> 16 and 16
an owning pointer int * std::unique_ptr<int> 8 and 8
a pointer and a length struct int_buffer std::span<int> 16 and 16
two int fields struct reading a class with two member functions 8 and 8

What you add, you pay for: a std::unique_ptr with a function-pointer deleter was 16 bytes, and a class with one virtual function 16. A std::span<int, 4>, whose length is in its type, was 8.

Where C and C++ are used

  • The Linux kernel is C: its documentation (7.4.0-rc2) names GCC with -std=gnu11, Clang, and Rust under CONFIG_RUST, and not C++.
  • GCC has been written in C++ since GCC 4.8 (2013), whose release notes say “GCC now uses C++ as its implementation language”.
  • TIOBE index, November 2026: C second (11.48%), C++ fourth (8.35%). A year earlier, C was second and C++ third.
  • Stack Overflow Developer Survey 2025: 22% of all respondents had done extensive development work in C in the past year, 23.5% in C++; among professional developers, 19.1% and 21.8%.

C or C++: which to choose

The C++ Core Guidelines’ C-style rules (CPL) set the defaults:

Situation Choose Why
New application or tool code C++ CPL.1: “Prefer C++ to C”
Code that calls a C library C++ CPL.3: C for the interface, C++ in the calling code
A library other languages call a C interface, C++ behind it extern "C" gives it C linkage
An existing C codebase, such as a kernel or a driver C its build and its rules are C’s
A platform with only a C compiler C nothing else builds
C you must keep the common subset, also built as C++ CPL.2: the C++ build finds what breaks
Learning for a C++ job C++, directly the C it needs is the subset inside C++

More answers for working C++ developers are in Careers, and answers on the core language in Language.

Questions

Is C++ a superset of C?

No. Stroustrup’s FAQ says: “In the strict mathematical sense, C isn’t a subset of C++.” int *readings = malloc(count * sizeof *readings); and char tag[4] = “temp”; are valid C, but ill-formed in C++, and MSVC 19.44, GCC 16.2, Clang 19.1 and Clang 23.1 all rejected both when built as C++23. Stroustrup’s FAQ says C++ “retains almost all of C as a subset”; idiomatic C such as that malloc line is in the part it does not.

Can a C++ compiler compile C code?

As C++, it compiles the C that is also valid C++, plus some C99 features GCC and Clang accept as extensions: built as C++23, MSVC 19.44, GCC 16.2, Clang 19.1 and Clang 23.1 all rejected malloc without a cast, C++ keywords as names and int count; written twice at file scope. The same tools also compile C as C: gcc and cl go by the file extension (g++ compiles .c as C++), and -x c (GCC, Clang) or /TC (MSVC) forces C.

What is the difference between .c, .cc and .cpp files?

The extension tells the compiler driver which language a file is. The gcc driver compiles .c as C and .cc, .cp, .cxx, .cpp, .CPP, .c++ and .C as C++, so .cc and .cpp are the same to it; g++ compiles .c as C++ too, and -x overrides the suffix. Microsoft’s documentation names .c as C and .cpp and .cxx as C++, with /TC and /TP to compile every file as C or as C++.

Should I learn C or C++ first?

C++ can be learned directly. Stroustrup’s FAQ answers this question: “Knowing C is a prerequisite for learning C++, right? Wrong. The common subset of C and C++ is easier to learn than C.” Rule CPL.1 of the C++ Core Guidelines is “Prefer C++ to C”, and if you must write C, CPL.2 says to keep to the common subset and compile it as C++ too.

Is C still used?

Yes: according to its documentation, the Linux kernel is written in C and compiled with GCC under -std=gnu11 or with Clang. TIOBE’s November 2026 index ranks C second and C++ fourth.

Can C code call C++?

Yes, through a function the C++ code declares extern “C”, which gives it C language linkage, so C calls it like a C function. Its body is still C++, but classes, templates, overloads and exceptions stay on the C++ side: catch every exception inside the function and return an error code, because MSVC under /EHsc treats an exception leaving an extern “C” function as undefined behavior. A header both languages include wraps its declarations in extern “C” { } under #ifdef __cplusplus.

Read next

  1. C++ generators: co_yield and std::generatorConcurrency · Miriam Castellanos · written for C++23
  2. std::println in C++23: print without std::cout or printfStandard Library · Miriam Castellanos · written for C++23
  3. Clang vs GCC: what differs for a C++ projectTooling · Tobias Mwangi · written for C++23