C++ vs Java Interview Questions: Live Questions & Comparison
Reviewed by Mark Dickie · Last updated
C++ and Java are both statically typed, general-purpose programming languages with long histories in systems and enterprise development, differing mainly in memory management model, compilation strategy, and runtime guarantees. In an interview context, C++ tends to probe your grasp of manual memory management, pointer semantics, undefined behavior, and low-level performance tradeoffs. Java questions more often center on object-oriented design, the JVM's garbage collection model, concurrency utilities, and standard library knowledge. Both languages appear frequently in coding rounds, but they test different mental models: C++ asks you to reason about what the machine is doing, while Java asks you to reason about abstraction layers and runtime behavior.
The table below summarizes where each language tends to come up and what it tests hardest:
| Dimension | C++ | Java |
|---|---|---|
| Memory model | Manual allocation and deallocation; RAII | Garbage-collected; no explicit frees |
| Typical interview domain | Systems, game engines, embedded, HFT | Backend services, Android, large enterprise apps |
| Hardest topics to prepare | Templates, move semantics, undefined behavior | Concurrency, JVM internals, generics erasure |
| Runtime behavior | Predictable but error-prone | Portable but GC pauses can surprise |
| Speed to write a solution | Slower — more boilerplate and memory bookkeeping | Faster — standard library covers most needs |
Deciding which to focus on for interview prep comes down to the roles you are targeting:
- If you are interviewing for systems, game development, or quantitative trading roles, prioritize C++.
- If you are aiming for backend or Android positions at large companies, Java is the more common ask.
- If you already know one well and have limited time, sharpen that language rather than splitting effort — most interviewers accept either for general algorithmic rounds.
- If you are choosing which to learn from scratch for interviews, Java's simpler memory story lets you spend more time on algorithms and less on language pitfalls.
- If you want to keep both warm, alternate practice sets so pointer logic and OO design patterns stay fresh.
Below you will find live interview questions drawn from both languages, followed by a side-by-side decision table built from real candidate attempt data.
C++ vs Java, side by side
How C++ and Java compare on Tarmac’s interview questions.
| Metric | C++ | Java |
|---|---|---|
| Practice questions | 6 | 6 |
| Average score | — | — |
| Hardest question (% who miss it) | — | — |
| Average time per question | — | — |
Practice questions
What problem does RAII (Resource Acquisition Is Initialization) solve?#
Options
- It ties a resource's lifetime to an object's lifetime, so the destructor releases it automatically — even when an exception unwinds the stack
- It makes all class members
constby default so they can't be modified after construction - It guarantees a class has no memory overhead compared to a plain struct
- It replaces the need for a destructor by freeing resources at program exit
Show answer
RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime: the constructor acquires the resource and the destructor releases it. Because C++ guarantees a local object's destructor runs when it goes out of scope — including during stack unwinding from a thrown exception — RAII gives automatic, exception-safe cleanup with no explicit cleanup block needed. std::unique_ptr, std::lock_guard, and std::fstream are standard-library RAII wrappers built on this pattern.
RAII binds a resource — heap memory, a file handle, a mutex lock, a socket — to the lifetime of a stack-allocated object: the constructor acquires it, the destructor releases it. Because C++ guarantees local objects' destructors run during stack unwinding (including when an exception propagates through the scope), RAII gives automatic, exception-safe cleanup with no explicit finally-style block. std::unique_ptr, std::lock_guard, and std::fstream are all RAII wrappers around a raw resource. The other options describe unrelated or incorrect properties: RAII says nothing about constness, doesn't guarantee zero overhead, and specifically exists so cleanup happens at scope exit, not merely at program exit.
Given String a = "hi"; String b = "hi"; String c = new String("hi");, what are the results of a == b and a == c?#
Options
a == bistrue,a == cisfalse- Both are
true—==compares the characters - Both are
false—==never works for strings a == bisfalse,a == cistrue
Show answer
a == b is true but a == c is false. String literals are interned into the constant pool, so two identical literals share one object and == (a reference comparison) returns true. new String("hi") forces a separate heap object, so a == c is false. Use .equals() to compare string contents — == only checks whether two references point at the same object.
String literals are interned into the string constant pool, so a and b reference the exact same pooled object and a == b is true. new String("hi") is explicitly told to allocate a fresh object on the heap, so c is a different reference and a == c is false even though the characters match. == compares references, not contents — .equals() is what compares the actual characters and would return true for all three. The trap is that == appears to work for literals because of interning, then silently breaks for strings built at runtime (from input, concatenation, or new), which is why you should always use .equals() to compare string values.
You're storing a pointer to a heap object that has exactly one owner for its entire lifetime, and you want zero reference-counting overhead. Which smart pointer fits?#
Options
std::unique_ptr— exclusive ownership, movable but not copyable, no atomic reference countstd::shared_ptr— reference-counted, so any number of owners can share itstd::weak_ptr— a non-owning observer that doesn't keep the object alive on its own- A raw pointer with manual
delete— the fastest option since it has no smart-pointer overhead at all
Show answer
std::unique_ptr is the right fit for a single, exclusive owner: it's movable but not copyable, and because exactly one unique_ptr ever owns the object, it needs no reference count — just a raw pointer and a deleter, so it's effectively as cheap as a raw pointer while still freeing automatically. std::shared_ptr pays for an atomic reference count to support multiple simultaneous owners, which this scenario doesn't need. std::weak_ptr doesn't own the object at all — it observes a shared_ptr without keeping it alive.
std::unique_ptr models exclusive ownership: it can be moved (transferring ownership) but not copied, and because only one unique_ptr ever owns the object, it needs no reference count — just a raw pointer and a deleter, making it effectively as cheap as a raw pointer while still freeing automatically. std::shared_ptr is for the different case of multiple simultaneous owners, and pays for an atomically-updated control block to track them. std::weak_ptr doesn't own anything — it exists to observe a shared_ptr-managed object without extending its lifetime, typically to break reference cycles. A raw pointer with manual delete reintroduces exactly the leak/double-free risk smart pointers exist to remove, so it's never the right answer to a memory-safety question even though it technically avoids smart-pointer bookkeeping.
You override equals() on a class but forget to override hashCode(). You then use instances as HashMap keys. What goes wrong?#
Options
- Two objects that are
equalscan land in different buckets, so a lookup with an equal-but-distinct key can fail to find the entry - It is a compile error — overriding one forces you to override the other
- Nothing;
HashMapusesequals()alone to locate keys - Every key collides into one bucket, but lookups still always succeed
Show answer
Lookups break: two objects that are equals can produce different hash codes, land in different buckets, and a get() with an equal-but-distinct key returns null. HashMap uses hashCode() to choose a bucket and equals() only within it, and the contract requires equal objects to have equal hash codes. The default identity-based hashCode() violates this, so always override both together using the same fields.
The equals/hashCode contract requires that equal objects return equal hash codes. HashMap first uses hashCode() to pick a bucket, then equals() to find the entry within it. The default Object.hashCode() is identity-based, so two distinct-but-equals objects almost always produce different hash codes, route to different buckets, and a get() with an equal-but-not-same key returns null — the entry is effectively lost. It is not a compile error: the compiler never forces you to override both, which is exactly why this bug is so common. Always override hashCode() whenever you override equals() (and prefer the same fields in both).
Which of these are true about std::move and move construction in C++? Select all that apply.#
Options
std::move(x)doesn't move anything by itself — it's a cast that produces an rvalue reference tox, making it eligible to bind to a move constructor/assignment overload- After
T b = std::move(a);runs a move constructor,ais left in a valid but unspecified state — safe to destroy or reassign, but its value shouldn't be assumed - Moving is guaranteed to be faster than copying for every type, because the standard requires move constructors to run in constant time
- A move constructor typically transfers ownership of a resource (e.g. a heap buffer) by copying the pointer and nulling it out in the source, avoiding a deep copy
std::movealways triggers a move; if the type has no move constructor, the code fails to compile
Show answer
std::move(x)doesn't move anything by itself — it's a cast that produces an rvalue reference tox, making it eligible to bind to a move constructor/assignment overload- After
T b = std::move(a);runs a move constructor,ais left in a valid but unspecified state — safe to destroy or reassign, but its value shouldn't be assumed - A move constructor typically transfers ownership of a resource (e.g. a heap buffer) by copying the pointer and nulling it out in the source, avoiding a deep copy
std::move is purely a cast (static_cast<T&&>) — it has no runtime effect of its own; it just makes the expression an rvalue so overload resolution can select a move constructor or move-assignment operator if one exists. A well-behaved move constructor steals the source's internal resource (pointer, buffer, handle) rather than deep-copying it, typically by copying the pointer and nulling the source's, which is what makes moves cheap for resource-owning types like std::vector or std::string. That's why the moved-from object is left 'valid but unspecified' — it's safe to destroy, reassign, or call methods with no preconditions on it, but its value is not to be relied upon. The claim that moving is guaranteed to be faster than copying for every type and the claim that std::move always triggers a move are wrong: nothing in the standard requires move to be O(1) for every type (a type with no move constructor falls back to its copy constructor, which may be O(n)), and passing an rvalue to a type with no move constructor simply resolves to the copy constructor instead of failing to compile.
Because of type erasure, which of these is legal at runtime in Java generics?#
Options
list instanceof List<?>— an unbounded wildcard check is allowedlist instanceof List<String>— a parameterizedinstanceofchecknew T[10]— creating an array of the type parameternew ArrayList<String>[5]— an array of a parameterized type
Show answer
Only list instanceof List<?> is legal. Java generics use type erasure, so type arguments are gone at runtime — instanceof List<String> is a compile error because there is no List<String> type to test. Erasure also bans new T[10] and generic array creation like new ArrayList<String>[5], since arrays are reified and covariant while generics are not. Use a List<T> or an Object[] cast when you need a generic container.
Generics are implemented by erasure: type arguments exist only at compile time and are erased to their bounds (or Object) in the bytecode, so the runtime has no List<String> type to check against. That makes instanceof List<String> a compile error, and only the reifiable unbounded wildcard form instanceof List<?> is permitted. Erasure also forbids new T[10] (the runtime can't know T's reified type) and new ArrayList<String>[5] (generic array creation), because arrays are covariant and reified while generics are not — allowing them would let a String[]-shaped store silently accept the wrong element type and defeat the type system. The usual workaround for generic arrays is an Object[] cast or, better, a List<T>.
A const member function can still modify a data member of the class if that member is declared mutable.#
Options
- True
- False
Show answer
True. A const member function makes this a pointer-to-const, so it normally can't assign to any data member. mutable is the deliberate exception: it marks a specific member — typically an internal cache, a lazily-computed value, or a synchronization primitive — as modifiable even through a const object, so a class can expose a logically-const public interface while still doing internal bookkeeping.
True. const on a member function is a promise not to modify the object's logical state through that function — the compiler enforces this by making this a pointer-to-const inside the function, so any member normally can't be assigned to. mutable is the deliberate escape hatch: it marks a member (typically bookkeeping like a cache, a lazily-computed value, or a mutex used only for internal synchronization) as modifiable even through a const object or const member function. It exists precisely so a class can present a const, logically-immutable public interface while still doing internal caching under the hood.
A volatile int counter is incremented with counter++ from many threads. Why can the final value be wrong?#
Options
volatileguarantees visibility but not atomicity;counter++is read-modify-write, so concurrent increments interleave and updates are lostvolatilemakes the field thread-confined, so other threads never see itvolatileis ignored for primitives, so it has no effect at all- It is always correct —
volatilemakescounter++atomic
Show answer
volatile guarantees visibility and ordering but not atomicity. counter++ is a read-modify-write: two threads can read the same value, each add one, and both store the same result, losing an update. For an atomic counter use AtomicInteger.incrementAndGet(), a synchronized block, or a lock. volatile is correct only for simple flags where one thread writes and others read — never for compound updates or check-then-act.
volatile provides a visibility/ordering guarantee — every read sees the latest write and writes aren't reordered around it — but it does NOT make compound operations atomic. counter++ is three steps (read, add one, write back), and two threads can both read the same value, each add one, and both write back the same result, so one increment is lost. To increment atomically use AtomicInteger.incrementAndGet(), a synchronized block, or a lock. volatile is the right tool for a simple flag (one thread writes, others read), but never for a counter or any check-then-act. Confusing visibility with atomicity is one of the most common Java concurrency mistakes.
This function compiles cleanly but callers see garbage or crashes. Which line is the bug?#
1| const std::string& firstWord(const std::string& sentence) {
2| std::string word = sentence.substr(0, sentence.find(' '));
3| return word;
4| }
5|
6| void printFirstWord(const std::string& s) {
7| const std::string& w = firstWord(s);
8| std::cout << w << std::endl;
9| }Options
- Line 3 —
wordis a local variable; returning a reference to it produces a dangling reference the momentfirstWordreturns - Line 2 —
substrnever null-terminates its result, sowordis malformed - Line 7 — binding a
const std::string&to a function's return value is illegal in C++ - Line 1 — a function can't take
const std::string&as both its parameter type and its return type
Show answer
Line 3 — word is a local variable; returning a reference to it produces a dangling reference the moment firstWord returns
word is a local variable with automatic (stack) storage duration — it's destroyed when firstWord returns. Returning word by reference (line 3) hands the caller a reference to storage that no longer holds a valid std::string; that's a dangling reference, and using it (line 8's std::cout << w) is undefined behavior — it might print garbage, print the old value by luck (if the memory hasn't been reused yet), or crash, and the exact outcome can differ by compiler, optimization level, and what else is on the stack. Note that line 7 itself is legal C++ — binding a reference to a temporary would normally extend a temporary's lifetime, but that extension does not apply here because the dangling reference was already produced inside firstWord, before the caller ever sees it. The fix is to return std::string by value (not by reference); modern C++'s move semantics and guaranteed copy elision (C++17) make that no more expensive than the broken reference version, so there's no performance reason to reach for the reference here.
A Stream pipeline does no work until a terminal operation runs. Which of these are terminal operations (they trigger execution and consume the stream)? Select all that apply.#
Options
collect(Collectors.toList())forEach(System.out::println)map(String::trim)filter(s -> !s.isEmpty())count()
Show answer
collect, forEach, and count are terminal operations: they trigger the pipeline, produce a result or side effect, and consume the stream so it can't be reused. map and filter are intermediate and lazy — they only describe a transformation and run nothing until a terminal op pulls elements through. A pipeline with no terminal op does zero work. Other terminal ops include reduce, findFirst, anyMatch, and toArray.
Stream operations split into intermediate (lazy, return a new Stream) and terminal (eager, produce a result or side effect and consume the stream). collect, forEach, and count are terminal — they trigger the pipeline and you cannot reuse the stream afterward. map and filter are intermediate: they only describe a transformation and do nothing until a terminal op pulls elements through. The practical consequence of laziness is that a pipeline of map/filter with no terminal op runs zero times — a common 'my forEach printed nothing' confusion — and that intermediate ops are fused and applied element-by-element rather than as separate passes. Other terminal ops include reduce, findFirst, anyMatch, toArray, and min/max.
Circle overrides Shape::area(). This code always prints the base class's area(), never the derived override. Which line causes it?#
1| struct Shape {
2| virtual double area() const { return 0.0; }
3| };
4| struct Circle : Shape {
5| double radius;
6| explicit Circle(double r) : radius(r) {}
7| double area() const override { return 3.14159 * radius * radius; }
8| };
9| void printArea(Shape s) {
10| std::cout << s.area() << std::endl;
11| }
12| Circle c(2.0);
13| printArea(c);Options
- Line 9 —
printAreatakesShapeby value, so passing aCirclecopies only theShapebase subobject ('object slicing'), losing the derived part and its vtable - Line 2 — the base
area()should not have a default implementation at all - Line 7 —
overrideis only valid when the base method is pure virtual - Line 13 — passing
cto a function expectingShaperequires an explicit cast
Show answer
Line 9 — printArea takes Shape by value, so passing a Circle copies only the Shape base subobject ('object slicing'), losing the derived part and its vtable
Polymorphism through virtual functions only works through a pointer or reference to the base class, because dispatch relies on the object's vptr pointing at its actual (dynamic) type's vtable. printArea(Shape s) takes its parameter by value, so the call printArea(c) invokes Shape's copy constructor with a Circle argument — and a Shape copy constructor only knows how to copy the Shape subobject. The radius member and the Circle vtable pointer are left behind entirely; s inside printArea is a genuine, fully-formed Shape, not a truncated Circle, so s.area() correctly (if surprisingly) calls Shape::area(). This is 'object slicing.' The fix is to take the parameter by reference or pointer (void printArea(const Shape& s)), which binds to the existing Circle object instead of copying it, preserving its vtable and dynamic type.
Java is strictly pass-by-value: when you pass an object to a method, the method receives a copy of the reference, so reassigning the parameter inside the method does not change which object the caller's variable points to (though mutating the object's fields is visible to the caller).#
Options
- True
- False
Show answer
True — Java is always pass-by-value. For objects, the value copied is the reference, so the parameter and the caller's variable point at the same object: mutating that object's fields is visible to the caller. But reassigning the parameter only rebinds the local copy of the reference, leaving the caller's variable unchanged. That is why a method can never swap two of the caller's variables — the giveaway that Java has no pass-by-reference.
Java is always pass-by-value, with no exceptions. For object types, the value that is copied is the reference itself, not the object — so the parameter and the caller's variable initially point at the same object. Mutating that shared object's state (e.g. list.add(x) or obj.setName(...)) is visible to the caller, which is why people mistakenly call it 'pass-by-reference'. But reassigning the parameter (obj = new Thing()) only rebinds the local copy of the reference; the caller's variable still points at the original object. The tell that it is pass-by-value: a method cannot swap two of the caller's variables or make the caller's variable point somewhere new.