Intuition What this page is
The parent note told you what C++ adds and why . This page does the opposite: it hands you concrete code snippets, one per scenario cell , and forces you to predict what the compiler does — then walks the reasoning. By the end you should never meet a C-vs-C++ situation you haven't already seen the shape of.
Before any code: we are comparing two compilers on the same source text . Call them:
cc — a C compiler (the C standard, e.g. C11).
c++ — a C++ compiler (the C++ standard, e.g. C++17).
"Compiles" means the compiler accepts the text and produces a program. "Error" means it refuses. That is the only measurement we make in most cells below — accept vs reject — plus, where a program runs, what it prints .
Every C-to-C++ question this topic can throw falls into one of these cells . Think of it as a checklist of "edge directions" — the corners where C and C++ disagree, plus the corners where they agree.
Cell
Axis being tested
Concrete trigger
A
Pure agreement
plain arithmetic C code — compiles identically in both
B
Strictness: implicit void* cast
int* p = malloc(4); with no cast
C
Strictness: reserved word
using new, class, bool as an identifier
D
Strictness: struct tag as type
struct S{}; S x; without repeating struct
E
Allocation semantics
new/delete vs malloc/free — does a constructor run?
F
Reference vs pointer
int& alias — degenerate case: can it be null? reseated?
G
Templates vs macro
MAX macro double-evaluation vs maxv<T>
H
Overloading resolution
which print overload gets picked; the ambiguous (degenerate) case
I
Namespaces
two init() colliding; the using shortcut
J
Word problem / exam twist
a mixed snippet that trips several cells at once
Cells A–D are the "does it even compile?" corners (backward-compatibility limits). E–I are the "it compiles in both, but behaves differently / more safely" corners. J is the combined stress test. The eight worked examples below cover all ten cells.
See the Compilation Model — C vs C++ note for how the same text reaches two different verdicts.
Worked example Does this compile the same in C and C++?
int add ( int a , int b ) { return a + b; }
int main ( void ) { return add ( 2 , 3 ); }
Forecast: guess before reading — same result in both, or different?
Steps
Read the code: only int, +, function call, return.
Why this step? We first classify which cell we're in. Nothing here uses a C++-only or C-only feature, so this is the agreement corner (Cell A).
Check for any implicit conversion, reserved word, or tag issue.
Why this step? Those are the four ways C and C++ disagree (Cells B–D). None are present.
Conclude: both cc and c++ accept it; add(2,3) returns 2 + 3 = 5 .
Why this step? This establishes the baseline : ~99% of clean C really is valid C++.
Verify: 2 + 3 = 5 . Both compilers agree; the exit code is 5 . ✅ This is the "you keep everything that already worked" promise made concrete.
Worked example Same text, two verdicts.
#include <stdlib.h>
int* p = ( int* ) malloc ( 4 * sizeof ( int )); /* version with cast */
int* q = malloc ( 4 * sizeof ( int )); /* version without */
Forecast: which of p, q compiles under c++? Under cc?
Steps
Recall what malloc returns: a void* — a "pointer to untyped bytes".
Why this step? The whole disagreement lives in converting that void* into an int*.
In C , a void* converts to any object pointer silently . So both p and q compile under cc.
Why this step? C's rule is permissive by design — it trusts the programmer.
In C++ , an implicit void* → int* conversion is an error . So q fails under c++; p (with the explicit (int*)) compiles.
Why this step? C++ made the type system stricter: a silent widening from "untyped bytes" to a typed pointer is exactly the kind of accident C++ wants to catch at compile time.
Better still, prefer new int[4] in C++.
Why this step? See new and delete vs malloc and free — new is type-aware, so no cast is ever needed.
Verify: Truth table — q (no cast): cc ✅, c++ ❌. p (cast): cc ✅, c++ ✅. This is the canonical proof that C++ is a near -superset, not a true superset.
Worked example Two tiny C snippets that C++ rejects.
/* Snippet 1 (Cell C) */ int new = 5 ;
/* Snippet 2 (Cell D) */ struct S { int x; };
struct S a; /* C requires 'struct' */
S b; /* C: error C++: fine */
Forecast: for each line, mark cc ✅/❌ and c++ ✅/❌.
Steps
int new = 5; — new is a keyword in C++ (the allocation operator), but an ordinary word in C.
Why this step? A keyword can never be a variable name. So cc ✅, c++ ❌. C++ added keywords, which "steals" a few names C programs might have used.
struct S a; — legal in both . In C the type's real name is struct S; C++ keeps this spelling working for compatibility.
Why this step? Shows C++ didn't break the old form — it added a shorter one.
S b; (no struct) — in C the tag S alone is not a type name, so cc ❌. In C++ a struct/class tag is directly a type, so c++ ✅.
Why this step? This is a case where C++ is the more permissive one — a reminder the strictness gap runs both directions.
Verify: int new: cc ✅ / c++ ❌. struct S a: ✅ / ✅. S b: ❌ / ✅. Every cell accounted for.
new vs malloc on a type with a constructor.
struct Widget {
int id;
Widget () { id = 42 ; } // constructor sets id = 42
};
Widget * a = new Widget; // path 1
Widget * b = (Widget * ) malloc ( sizeof (Widget)); // path 2
Forecast: what is a->id? What is b->id?
Steps
new Widget does two things: allocate memory and call Widget().
Why this step? That constructor is the whole point — it initialises id to 42 .
So a->id == 42. Guaranteed.
Why this step? Because the constructor ran, the invariant "a fresh Widget has id 42" holds.
malloc only allocates raw bytes; it never calls the constructor.
Why this step? malloc predates classes — it knows nothing about Widget().
So b->id is indeterminate (whatever garbage was in those bytes). Reading it is undefined behaviour.
Why this step? This is the practical danger of "new is just a fancy malloc" — it is not .
Clean-up must match: delete a; (runs destructor), free(b); (does not). Crossing them is undefined behaviour.
Why this step? Pairing rule: new↔delete, malloc↔free.
Verify: a->id == 42 (deterministic). b->id is not guaranteed to be 42 — it is garbage. The observable difference = 42 vs undefined is exactly the constructor call. See new and delete vs malloc and free .
Worked example Alias behaviour and the two things a reference
cannot do.
int x = 5 ;
int& r = x; // r is another name for x
r = 9 ; // does this change x?
int y = 100 ;
// r = &y ?? can we point r at y instead?
Forecast: after r = 9;, what is x? Can r later be made to refer to y?
Steps
int& r = x; binds r as an alias — the same storage as x, not a copy, not a pointer.
Why this step? Understanding "same storage" predicts everything else.
r = 9; writes 9 into that shared storage, so x == 9.
Why this step? Reads/writes through r are reads/writes to x. No * or & needed.
Degenerate case — reseating: r = &y; does not rebind r; there is no syntax to make r refer to a different variable. r = y; just copies y's value into x.
Why this step? A reference is fixed at birth — this eliminates a whole family of pointer bugs.
Degenerate case — null: there is no int& r = nullptr;. A reference must bind to a real object.
Why this step? "Always valid, never null" is precisely the safety a pointer lacks. See Pointers vs References .
Verify: After r = 9, x == 9. After r = y; (with y == 100), x == 100 and y == 100 (a copy, not a rebind). A reference can be neither null nor reseated. ✅
Worked example The classic
MAX(i++, j) trap.
#define MAX ( a , b ) ((a) > (b) ? (a) : (b))
int i = 5 , j = 3 ;
int m = MAX (i ++ , j); // what is m? what is i afterwards?
versus
template < typename T > T maxv ( T a , T b ) { return a > b ? a : b; }
int i = 5 , j = 3 ;
int m = maxv (i ++ , j); // same question
Forecast: predict m and final i in both versions.
Steps
Macro: textual substitution gives ((i++) > (j) ? (i++) : (j)).
Why this step? A macro is copy-paste before compiling — the arguments literally appear twice.
Evaluate: i++ yields 5 (then i becomes 6 ); since 5 > 3 is true we take the then branch i++ again, yielding 6 (then i becomes 7 ). So m == 6 and i == 7.
Why this step? i++ fired twice — the bug. (Exact result is technically unspecified, but the double increment is the point.)
Template: maxv is a real function . Arguments are evaluated once at the call: i++ yields 5 (i becomes 6 ), j is 3 .
Why this step? Function arguments are computed exactly once, then passed by value.
Inside: max ( 5 , 3 ) = 5 . So m == 5 and i == 6.
Why this step? One evaluation → predictable result, and it's type-checked.
Verify: Macro → m == 6, i == 7 (side effect fired twice). Template → m == 5, i == 6 (fired once). The template is safer because arguments evaluate once . See Templates and Generic Programming .
print runs — and when the compiler refuses to choose.
void print ( int x ); // (1)
void print ( double x ); // (2)
print ( 7 ); // which one?
print ( 7.0 ); // which one?
print ( 'A' ); // which one?
print ( 7 L ); // long — the tricky one
Forecast: match each call to overload (1) or (2), or "error".
Steps
print(7) — 7 is int, exact match to (1).
Why this step? An exact type match always wins over any conversion.
print(7.0) — 7.0 is double, exact match to (2).
Why this step? Same rule, other overload.
print('A') — char promotes to int (a standard integral promotion ), so (1) wins.
Why this step? Promotions rank above other conversions; char→int is the natural one.
Degenerate/ambiguous case print(7L) — long is neither int nor double exactly. Converting long→int and long→double are both standard conversions of equal rank ⇒ ambiguous , compile error.
Why this step? This is the "tie" corner: when no candidate is strictly better, C++ refuses rather than guess. Fix by adding void print(long); or casting.
Verify: print(7)→(1); print(7.0)→(2); print('A')→(1) via promotion; print(7L)→ error (ambiguous) . Overloading replaces C's print_int/print_double naming — but the tie case shows it isn't magic.
Worked example Two libraries collide — then a mixed C-ish snippet.
namespace net { int init () { return 1 ; } }
namespace gfx { int init () { return 2 ; } }
// In C, two global init() would be a link error. Here:
int a = net :: init (); // = ?
int b = gfx :: init (); // = ?
using namespace net ;
int c = init (); // now which init()?
Then the exam twist — will this whole thing compile under c++?
#include <cstdlib>
int main () {
int new_ = 3 ; // (line 1)
int* p = std :: malloc ( sizeof ( int )); // (line 2)
* p = new_;
std :: free (p);
return * ( & new_) - new_; // (line 3)
}
Forecast: values of a, b, c; and does the twist compile? What does main return?
Steps
net::init() returns 1 ; gfx::init() returns 2 . No clash because the namespace qualifier disambiguates.
Why this step? This is exactly the collision C cannot solve (one global namespace). See Namespaces and the std namespace .
After using namespace net;, the bare init() resolves to net::init, so c == 1.
Why this step? using imports names, but only net's here, so no ambiguity.
Twist line 1: new_ (with underscore) is a legal identifier — new alone would fail (Cell C), but new_ is fine.
Why this step? Tests whether you remember it's the exact word new that's reserved.
Twist line 2: std::malloc returns void*; assigning to int* p without a cast is the Cell B error!
Why this step? This is the trap: even qualified std::malloc still yields void*, so c++ rejects line 2.
Conclusion: the twist does not compile (needs (int*)std::malloc(...) or, better, new int).
Why this step? Combines Cells B + C into one exam-style gotcha.
Verify: a == 1, b == 2, c == 1. The twist snippet: compile error on line 2 (implicit void*→int*). Had we cast it, main would compute *(&new_) - new_ == 3 - 3 == 0. ✅
Recall Cover the answers — reproduce each cell's verdict.
Cell B: int* q = malloc(4); — verdict in C vs C++? ::: C ✅, C++ ❌ (no implicit void*→int*).
Cell C: is int new = 5; legal? ::: C ✅, C++ ❌ (new is a keyword). But int new_ is fine in both.
Cell D: struct S{}; S b; (no struct)? ::: C ❌, C++ ✅ (tag is a type in C++).
Cell E: malloc-ed Widget, is id guaranteed 42? ::: No — no constructor runs; garbage. new Widget gives 42.
Cell F: can an int& be null or reseated? ::: No to both — always bound, always valid.
Cell G: macro MAX(i++,j) vs maxv(i++,j) — final i? ::: Macro increments twice (i=7), template once (i=6).
Cell H: print(7L) with int/double overloads? ::: Ambiguous → compile error.
Mnemonic The four "compatibility break" corners
V-K-T-A : V oid* cast (B), K eyword clash (C), T ag-as-type (D, the reverse case), A mbiguous overload (H). If a C snippet fails in C++, it's almost always one of these.
← Back to the parent topic
new and delete vs malloc and free
Pointers vs References
Templates and Generic Programming
Namespaces and the std namespace
Compilation Model — C vs C++
Classes and Objects in C++