Skip to content
C++
Language
since C++11
Basic

C++ for Python and Java Developers

C++ from a Python/Java perspective: value semantics, no GC, static types, and the idioms that replace what you relied on in managed-runtime languages.

Python and Java give you a runtime that manages memory, resolves types at runtime (Python) or enforces them through a class hierarchy (Java), and treats most objects as references. C++ does none of these things by default β€” and that is exactly where the value comes from. This page focuses on what to unlearn and what to adopt instead.


The most important shift: value semantics

In Python and Java, assigning an object variable copies a reference to the same object. In C++, assigning an object copies the object itself.

python
# Python: b and a point to the same list
a = [1, 2, 3]
b = a
b[0] = 99
print(a)   # [99, 2, 3] β€” a is affected!
java
// Java: b and a reference the same Point
Point a = new Point(1, 2);
Point b = a;
b.x = 99;
System.out.println(a.x);   // 99 β€” a is affected!
cpp
// C++: b is an independent copy of a
#include <vector>

std::vector<int> a = {1, 2, 3};
std::vector<int> b = a;   // deep copy β€” independent object
b[0] = 99;
// a is still {1, 2, 3}

This is value semantics: = copies the value, not the pointer. C++ structs and classes behave like Python's int or Java's int β€” copying creates an independent object.

When you want reference semantics, use a pointer or reference explicitly:

cpp
std::vector<int> a = {1, 2, 3};
std::vector<int>& b = a;   // b is an alias for a
b[0] = 99;
// a is now {99, 2, 3}

No garbage collector β€” but you don't need one

Python uses reference counting + a cycle collector. Java has a generational GC. C++ has neither β€” and this is a feature, not a limitation.

The C++ model: RAII (Resource Acquisition Is Initialization) ties resource lifetimes to object lifetimes. Objects are destroyed when they go out of scope, deterministically:

python
# Python: file closed by GC eventually (or by __exit__ with 'with')
f = open("log.txt", "w")
f.write("hello")
# f is closed... sometime
cpp
// C++: file closed exactly when ofstream goes out of scope
#include <fstream>

void write_log() {
    std::ofstream f("log.txt");   // opens on construction
    f << "hello\n";
    // ~ofstream() closes the file here β€” always, even if an exception is thrown
}

You never call close(), free(), or destroy() in well-written C++. The destructor does it.

For heap allocation, use smart pointers β€” not new/delete:

cpp
#include <memory>

// WRONG (Java habit):
Widget* w = new Widget(id);  // you must delete this manually
// ... easy to forget, easy to double-delete

// RIGHT:
auto w = std::make_unique<Widget>(id);  // sole owner; auto-deleted when w goes out of scope
auto s = std::make_shared<Widget>(id);  // shared ownership; deleted when last owner is gone

std::unique_ptr replaces Java/Python object references in most cases. std::shared_ptr replaces shared references (with ref-counting β€” similar to Python's internal model).


Static types β€” compile-time correctness

Python is dynamically typed: type errors appear at runtime. Java is statically typed but everything is a reference behind an interface. C++ is statically typed with value semantics: types are checked at compile time, and the compiler knows the full layout of every object.

python
# Python: type error at runtime
def double(x):
    return x * 2

double("hello")   # returns "hellohello" β€” silently wrong
double(3)         # returns 6
cpp
// C++: type error at compile time
int double_val(int x) { return x * 2; }

double_val("hello");   // compile error: cannot convert const char* to int
double_val(3);         // 6

auto deduces types β€” it does not make C++ dynamic:

cpp
auto x = 42;          // x is int β€” deduced at compile time
auto s = std::string{"hello"};   // s is std::string

// auto doesn't erase types; it deduces them
auto result = x + s;  // compile error: can't add int + string

Templates are compile-time polymorphism:

python
# Python: one function works for any type (duck typing)
def square(x):
    return x * x
cpp
// C++: template generates a version for each type at compile time
template<typename T>
T square(T x) { return x * x; }

square(3);    // instantiates square<int>
square(3.0);  // instantiates square<double>
// No virtual dispatch, no type erasure β€” inlined by the compiler

Stack vs heap β€” where objects live

In Python, all objects live on the heap. In Java, primitives live on the stack; objects live on the heap. In C++, you control where objects live.

cpp
// Stack allocation β€” fast, automatic cleanup
void process() {
    std::string name = "Alice";      // on the stack
    std::vector<int> data = {1,2,3}; // vector control block on stack, data on heap
    // Both destroyed automatically when process() returns
}

// Heap allocation β€” when lifetime must outlive the scope
auto widget = std::make_unique<Widget>(id);   // on the heap, owned by unique_ptr on stack

Default to stack allocation. This is idiomatic C++. Stack allocation is faster (no allocator call, no GC scan), has deterministic cleanup, and the compiler can optimize it aggressively.

Heap allocation (new, make_unique, make_shared) is for:

  • Objects whose lifetime must outlive a function
  • Objects too large for the stack (~8 MB typical limit)
  • Polymorphic objects used through base class pointers

The new keyword β€” almost never

Java uses new for all object construction. In C++, new means "allocate on the heap and leak unless you remember to delete." Modern C++ avoids new:

java
// Java: everything is newed
Widget w = new Widget(42);
cpp
// C++ β€” stack (preferred)
Widget w{42};           // on the stack, destroyed automatically

// C++ β€” heap with ownership (use when needed)
auto w = std::make_unique<Widget>(42);   // heap, auto-deleted

The rule: if you find yourself typing new Widget(...), ask "does this need to outlive the current scope?" If no, put it on the stack. If yes, use make_unique.


Containers β€” typed and value-based

Python's list holds any type. Java's ArrayList<T> holds references to T. C++ containers hold values of type T.

python
# Python list: heterogeneous, reference-based
items = [1, "hello", 3.14, [1,2]]
cpp
// std::vector<T>: homogeneous, value-based
std::vector<int>    ints   = {1, 2, 3};
std::vector<std::string> words = {"hello", "world"};

// Heterogeneous: use std::variant or std::any
std::vector<std::variant<int, std::string>> mixed = {1, "hello", 3};
java
// Java HashMap<K,V>: K and V are object references
Map<String, Integer> m = new HashMap<>();
cpp
// std::unordered_map<K,V>: K and V are values (no boxing)
std::unordered_map<std::string, int> m;
m["key"] = 42;
// Accessing a missing key creates a default-initialized value (0 for int)
// Use m.find() or m.contains() to check first

Important: in Java/Python, iterating a collection and having another reference doesn't surprise you. In C++, watch out for iterator invalidation:

cpp
std::vector<int> v = {1, 2, 3};
for (int x : v) {
    v.push_back(x * 2);  // UB! push_back may reallocate, invalidating the range-for iterator
}

null / None β€” replaced by std::optional

Python's None and Java's null are the "billion-dollar mistake." C++ pointers can be nullptr, but C++ also gives you a type-safe alternative:

python
# Python: return None to signal "no value"
def find_user(id):
    if id not in db:
        return None
    return db[id]

user = find_user(42)
if user is not None:
    print(user.name)
cpp
#include <optional>

std::optional<User> find_user(int id) {
    if (!db.contains(id)) return std::nullopt;
    return db[id];
}

auto user = find_user(42);
if (user) {                    // bool conversion
    std::println("{}", user->name);
}

// Or with a default:
User u = find_user(42).value_or(default_user);

std::optional<T> makes the "might not have a value" concept explicit in the type system. No null pointer dereferences, no NoneType has no attribute at runtime.


Exceptions β€” not for flow control

Python heavily uses exceptions for control flow (StopIteration, KeyError, catching Exception broadly). C++ has exceptions but they are expensive; use them only for genuinely exceptional conditions.

python
# Python: exceptions as flow control (idiomatic there, not in C++)
try:
    value = my_dict["key"]
except KeyError:
    value = default_value
cpp
// C++: check before accessing
auto it = my_map.find("key");
if (it != my_map.end()) {
    value = it->second;
} else {
    value = default_value;
}

// Or C++20:
value = my_map.contains("key") ? my_map.at("key") : default_value;

When to use exceptions in C++: constructor failures, I/O errors, truly unrecoverable situations. Not for "key not found" or "item is empty."

Checked exceptions don't exist in C++. Java forces you to declare throws IOException. C++ exceptions are always unchecked β€” callers aren't forced to catch. Design APIs that communicate errors through return values (std::optional, std::expected) when errors are expected.


Object slicing (Java devs: must-know)

Java is entirely reference-based for objects, so polymorphism always works. In C++, assigning a derived object to a base value slices away the derived portion:

java
// Java: always reference semantics, polymorphism works
Animal a = new Dog();
a.speak();   // "Woof" β€” correct
cpp
// C++: value semantics slices the object
struct Animal {
    virtual std::string speak() const { return "..."; }
    virtual ~Animal() = default;
};

struct Dog : Animal {
    std::string speak() const override { return "Woof"; }
};

Dog d;
Animal a = d;       // SLICED: a is an Animal value, Dog portion discarded
a.speak();          // returns "..." β€” not "Woof"!

// FIX: use pointer or reference for polymorphism
Animal& ar = d;     // reference β€” not sliced
ar.speak();         // "Woof"

std::unique_ptr<Animal> pa = std::make_unique<Dog>();
pa->speak();        // "Woof"

Rule: if you need polymorphism, store derived objects behind a pointer or reference β€” never by value in a base-typed variable.


Integer sizes β€” not what Java promises

Java: int = 32-bit, long = 64-bit, guaranteed. Python: integers are arbitrary precision.

C++: sizes depend on the platform. Use fixed-width types when size matters:

cpp
#include <cstdint>

int32_t  i32 = -1'000'000;   // guaranteed 32-bit signed
uint64_t u64 = 18'446'744'073'709'551'615ULL;  // guaranteed 64-bit unsigned
int8_t   byte = 127;

// Platform-size types (avoid in serialization/networking):
int    x;    // at least 16-bit; usually 32-bit
long   y;    // 32-bit on Windows 64-bit, 64-bit on Linux 64-bit

Also: C++ signed integer overflow is undefined behavior (the compiler can assume it never happens and optimize accordingly). Python integers never overflow; Java wraps silently.

cpp
int x = INT_MAX;
x + 1;   // UB in C++ β€” do NOT rely on this wrapping to 0
          // use unsigned or check before adding

The Python/Java β†’ C++ transition checklist

Habit from Python/JavaC++ replacementWhy
b = a for objects (shared reference)b = a (deep copy) OR auto& b = a (alias)C++ has value semantics by default
GC / reference countingRAII + destructors + smart pointersDeterministic, zero-cost cleanup
new Widget() everywhereStack: Widget w; / Heap: make_unique<Widget>()Stack is faster; unique_ptr prevents leaks
null / Nonestd::optional<T> for optional values, nullptr for raw pointersOptional is type-safe and explicit
Dynamic types / duck typingTemplates + Concepts (compile-time polymorphism)Zero-overhead, compiler-checked
Exceptions for flow controlReturn values, std::optional, std::expectedC++ exceptions are for exceptional conditions
instanceof / isinstancedynamic_cast, std::holds_alternative, ConceptsEach has its use case
Deep inheritance hierarchiesFlat classes + composition + free functionsPrefer composition; avoid "programming Java in C++"
HashMap/dict with any keystd::unordered_map<K,V> (K must be hashable, V is a value)Homogeneous; check for missing keys before access
Arbitrary-precision integersint32_t/int64_t, or a big-integer libraryC++ integers have fixed size and can overflow
print(x) / System.out.printlnstd::println("{}", x) (C++23) or std::cout << xType-safe, no format string mismatch