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.