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: 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: 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!// 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:
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: file closed by GC eventually (or by __exit__ with 'with')
f = open("log.txt", "w")
f.write("hello")
# f is closed... sometime// 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:
#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 gonestd::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: type error at runtime
def double(x):
return x * 2
double("hello") # returns "hellohello" β silently wrong
double(3) # returns 6// 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); // 6auto deduces types β it does not make C++ dynamic:
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 + stringTemplates are compile-time polymorphism:
# Python: one function works for any type (duck typing)
def square(x):
return x * x// 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 compilerStack 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.
// 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 stackDefault 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: everything is newed
Widget w = new Widget(42);// 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-deletedThe 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 list: heterogeneous, reference-based
items = [1, "hello", 3.14, [1,2]]// 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 HashMap<K,V>: K and V are object references
Map<String, Integer> m = new HashMap<>();// 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 firstImportant: in Java/Python, iterating a collection and having another reference doesn't surprise you. In C++, watch out for iterator invalidation:
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: 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)#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: exceptions as flow control (idiomatic there, not in C++)
try:
value = my_dict["key"]
except KeyError:
value = default_value// 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: always reference semantics, polymorphism works
Animal a = new Dog();
a.speak(); // "Woof" β correct// 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:
#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-bitAlso: C++ signed integer overflow is undefined behavior (the compiler can assume it never happens and optimize accordingly). Python integers never overflow; Java wraps silently.
int x = INT_MAX;
x + 1; // UB in C++ β do NOT rely on this wrapping to 0
// use unsigned or check before addingThe Python/Java β C++ transition checklist
| Habit from Python/Java | C++ replacement | Why |
|---|---|---|
b = a for objects (shared reference) | b = a (deep copy) OR auto& b = a (alias) | C++ has value semantics by default |
| GC / reference counting | RAII + destructors + smart pointers | Deterministic, zero-cost cleanup |
new Widget() everywhere | Stack: Widget w; / Heap: make_unique<Widget>() | Stack is faster; unique_ptr prevents leaks |
null / None | std::optional<T> for optional values, nullptr for raw pointers | Optional is type-safe and explicit |
| Dynamic types / duck typing | Templates + Concepts (compile-time polymorphism) | Zero-overhead, compiler-checked |
| Exceptions for flow control | Return values, std::optional, std::expected | C++ exceptions are for exceptional conditions |
instanceof / isinstance | dynamic_cast, std::holds_alternative, Concepts | Each has its use case |
| Deep inheritance hierarchies | Flat classes + composition + free functions | Prefer composition; avoid "programming Java in C++" |
HashMap/dict with any key | std::unordered_map<K,V> (K must be hashable, V is a value) | Homogeneous; check for missing keys before access |
| Arbitrary-precision integers | int32_t/int64_t, or a big-integer library | C++ integers have fixed size and can overflow |
print(x) / System.out.println | std::println("{}", x) (C++23) or std::cout << x | Type-safe, no format string mismatch |