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
- 1What C++ adds, next to the C it replaces
- 2Valid C that a C++ compiler rejects
- 3Code that compiles as both and means something else
- 3.1sizeof(‘a’): an int in C, a char in C++
- 3.2int f(): any arguments in C17, none in C23 and C++
- 4C23 moved toward C++
- 5Calling C from C++ and C++ from C:
extern "C" - 6Is C faster than C++?
- 7Where C and C++ are used
- 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”.

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.

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:
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:
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:
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:
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:
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
constin C++, and since C++11 it no longer converts tochar *, sochar *unit = "mV";is ill-formed there. Only MSVC rejected it, and only under/permissive-(which/std:c++20and/std:c++latestimply); with/permissiveit 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-errorsit 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:
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++
printf("sizeof('a') = %zu\n", sizeof('a'));
printf("sizeof(char) = %zu\n", sizeof(char));— end example]
Built as C17 and C23, all four printed:
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++
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
constobject at file scope that is not declaredexternhas 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:
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:
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:
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-errorsobjects 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
#embedin 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:
#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:
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:
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:
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 underCONFIG_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.
Sources
- [diff.iso] C++ and ISO CC++ standard working draft
- C23cppreference.com
- P1967R14: #embed, a simple, scannable preprocessor-based resource acquisition methodWG21
- Language linkagecppreference.com
- /EH (Exception handling model)Microsoft Learn
- Bjarne Stroustrup’s FAQBjarne Stroustrup
- CPL: C-style programmingC++ Core Guidelines
- Programming LanguageThe Linux kernel documentation