Paul J. Lucas
Posted on August 18, 2024
Introduction
This is a follow-up article to C Interview Questions, so if you haven’t read that, you should. As stated there, all those C questions are also valid C++ questions, so those could be asked of a candidate interviewing for a C++ job. Of course C++ is a much larger language than C, so there are several additional questions that are specific to C++.
Here are a few questions (with answers) I’d ask a candidate during an interview for a job programming in C++ (in addition to the C questions).
As before, if you’re a beginner, I recommend trying to answer the questions for yourself before clicking on the Answer links.
Questions
Question 1: Local Variables
Given this function (where T
is some arbitrary type that doesn’t matter here):
T& f() {
T t;
// ...
return t;
}
Question: What’s wrong with this function?
This question is basically the same as the C Interview Question #4 except references replace pointers.
Answer
Because t
is a local variable, it will cease to exist upon return, hence the reference will be a dangling reference. Attempting to access the referred-to value would result in undefined behavior (and would likely result in a core dump, if you’re lucky).
Question 2: delete
What two things happen when you call delete
?
Answer
Question 3: delete[]
What is the difference between delete
and delete[]
?
Answer
Plain delete
is used to destroy a single object; delete[]
is used to destroy an array of objects.
Question 4: Assignment Operators
Given two C++ classes:
struct T {
T( T const& );
~T();
};
struct S {
T *p; // may be null
S& operator=( S const &that ) {
// ...
}
~S() { delete p; }
};
Question 4a: Implement the assignment operator for class S
such that it performs a deep copy, that is:
S x, y;
// ...
x = y; // x.p points to a copy of *y.p, if any
Notes:
- The
p
member is an owning pointer, that isS
is responsible for the dynamically allocated, pointed-toT
. - You may use only those functions explicitly declared here.
- The details of class
T
don’t matter. Assume thatT(T const&)
copies aT
object conventionally.
Line 2 guards against self-assignment, that is: Line 3 deletes the existing It is never necessary to check a pointer for null before deleting it. C++ guarantees that deleting a null pointer does nothing. Line 4 checks that Line 6 returns Answer
A first-cut implementation might be:
S& S::operator=( S const &that ) {
if ( &that != this ) { // 2
delete p; // 3
p = that.p ? new T{ *that.p } : nullptr; // 4
}
return *this; // 6
}
x = x; // silly, but it still has to be correct
T
, if any.
that.p
is non-null (as given, p
may be null) and dynamically allocates a copy of *that.p
if not; otherwise, just sets p
to null.*this
as all assignment operators do.
Question 4b (senior): Is the implementation exception-safe? Why or why not? If not, how would you make it exception-safe?
To be exception-safe, a function must behave as if nothing happened if an exception is thrown. In this case, the assignment operator must not delete the To make it exception-safe, introduce a temporary variable: Now instead of deleting Answer
The previous answer is not exception-safe because if T(T const&)
throws an exception (which it might because it’s not declared noexcept
), the T
to which p
points will already have been deleted.
T
to which p
points.
S& S::operator=( S const &that ) {
if ( &that != this ) {
T *t = that.p ? new T{ *that.p } : nullptr;
delete p; // 4
p = t;
}
return *this;
}
p
first, call T(T const&)
first. If it throws an exception, line 4 will never be reached. If the code reaches line 4, it means that it did not throw an exception so now it’s safe to delete p
.
Question 4c (senior): What two bad consequences would result if we did not implement S::operator=(S const&)
?
Answer
Without implementing our own assignment operator, the compiler would automatically synthesize a default one that would do a shallow copy, that is simply copy the value of p
and not the T
to which p
points. This has two bad consequences:
T
will be memory-leaked immediately.p
will point at the same T
. When the first S
gets destroyed, it will delete T
. When the second S
gets destroyed, it will try to delete the T
that has already been deleted. This will most likely result in a core dump.
Question 5: std::map
vs. std::unordered_map
What are the differences between std::map
and std::unordered_map
, specifically:
Question 5a: How are each typically implemented and what are their average and worst-case running times (big “O” notation) for insertion, look-up, and deletion?
Answer
std::map
is typically implemented using a balanced binary tree, e.g., a red-black tree, for which both the average and worst-case insertion, look-up, and deletion times are all O(lg n).std::unordered_map
is implemented using a hash-table for which the average insertion, look-up, and deletion times are all O(1), but whose worst-case times are all O(n).
Question 5b: What are the requirement(s) for elements placed into each?
Answer
std::map
elements must be less-than comparable.std::unordered_map
elements must be both equality comparable and hashable.
Question 5c: When you would use one vs. the other?
Answer
In general, std::unordered_map
is preferred due to O(1) average running time. However, if you need to iterate in sorted order, you need to use std::map
.
Question 6 (Senior): virtual
Functions
How are virtual functions typically implemented by C++ compilers?
Each slot contains a pointer-to-function of that class’s implementation of the function. (If a particular class doesn’t override a virtual function of its base class, then its slot contains a pointer to the base’s function.) For example, given two classes For For Every object of such a class contains a vptr (“vee pointer”) that points to the vtbl for its class. The value of this pointer determines the type of the object at run-time. For example, the compiler inserts a That is, in every constructor, it inserts code to initialize Then to call a virtual function, say That is: Notice that if Answer
Every class that has at least one virtual function has an associated vtbl (“vee table”) array with one “slot” per virtual function (including the destructor).
B
and D
, the compiler generates the vtbls:
struct B { void const *const B_vtbl[] = {
virtual ~B(); (void*)&B_dtor,
virtual void f(int); (void*)&B_f,
void g(); (void*)&B_h
virtual void h(); };
};
struct D : B { void const *const D_vtbl[] = {
~D(); (void*)&D_dtor,
void h() override; (void*)&B_f,
}; (void*)&D_h
};
B_vtbl
, a pointer to B_dtor
(the implementation of ~B()
) is put into slot 0; B_f
is put into slot 1, and B_h
is put into slot 2. (g()
doesn’t get a slot since it’s not virtual.)D_vtbl
, it’s similar except that slot 1 points to B_f
(since D
didn’t override B::f()
) and slot 2 points to D_h
(since it did override B::h()
)._vptr
member into the base class and initializes it like:
struct B {
void *const *_vptr;
B( /*...*/ ) { _vptr = B_vtbl; }
};
struct D : B {
D( /*...*/ ) { _vptr = D_vtbl; }
};
_vptr
with the right vtbl for the class.h()
, the compiler looks up h
’s slot (here, 2) from its internal mapping, then generates code that:
_vptr
with the slot to get h
’s address; then:this
pointer.
void call_h( B *b ) {
b->h(); // reinterpret_cast<void(*)()>( b->_vptr[2] )( b );
}
b
actually points to a D
, it will call D::h()
since _vptr
points to D_vtbl
.
Question 7: Spot the Error
Given:
struct S {
// Hint: the error is in the next line.
S() : p1{ new T }, p2{ new T } {
}
~S() {
delete p1;
delete p2;
}
T *p1, *p2;
};
Assume T
is some other class the details for which don’t matter.
Question 7a (senior): What’s wrong with that class?
Answer
If T::T()
throws an exception during the construction of p2
, p1
will leak because destructors are not called for partially constructed objects (in this case S
, hence ~S()
will not run and delete p1
will never be called).
Question 7b (senior): How would you fix it?
Even though the destructor isn’t called on objects whose constructor (or constructor of a data member) throws, destructors are called for fully constructed data members. In this case, Answer
Perhaps the simplest way to fix this is to use std::unique_ptr
:
struct S {
S() : p1{ new T }, p2{ new T } {
}
std::unique_ptr<T> p1, p2;
};
p1
’s destructor will be called (thus freeing p1
) if p2
’s constructor throws.
Question 8: new
Given:
T *t = new( p ) T;
Question 8a (senior): What does that syntax mean?
Answer
It’s known as placement new and is used to create an object at a specific memory address, in this case, the memory pointed to by p
.
Question 8b (senior): What restriction(s) does it have, if any?
Answer
The address must be suitably aligned for the type of object being constructed and of sufficient size.
Question 8c (senior): How do you destroy such an object?
Answer
You must explicitly call its destructor like:
t->~T();
Conclusion
Those are some decent C++ interview questions. Feel free to use them when interviewing candidates.
Posted on August 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.