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
- The object’s destructor, if any, is called.
- The memory used by the object is deallocated.
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
pmember is an owning pointer, that isSis responsible for the dynamically allocated, pointed-toT. - You may use only those functions explicitly declared here.
- The details of class
Tdon’t matter. Assume thatT(T const&)copies aTobject conventionally.
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
}
Line 2 guards against self-assignment, that is:
x = x; // silly, but it still has to be correct
Line 3 deletes the existing T, if any.
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 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.
Line 6 returns *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?
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.
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 T to which p points.
To make it exception-safe, introduce a temporary variable:
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;
}
Now instead of deleting 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:
- The left-hand-side’s
Twill be memory-leaked immediately. - The left-hand- and right-hand-side’s
pwill point at the sameT. When the firstSgets destroyed, it will deleteT. When the secondSgets destroyed, it will try to delete theTthat 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::mapis 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_mapis 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::mapelements must be less-than comparable. -
std::unordered_mapelements 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?
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).
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 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
};
For 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.)
For 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()).
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 _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; }
};
That is, in every constructor, it inserts code to initialize _vptr with the right vtbl for the class.
Then to call a virtual function, say h(), the compiler looks up h’s slot (here, 2) from its internal mapping, then generates code that:
- Indexes the vtbl pointed to by the object’s
_vptrwith the slot to geth’s address; then: - Casts the address to the correct pointer-to-function type; then:
- Calls the function passing the address of the object as the first parameter to become its
thispointer.
That is:
void call_h( B *b ) {
b->h(); // reinterpret_cast<void(*)()>( b->_vptr[2] )( b );
}
Notice that if 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?
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;
};
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, 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.
Top comments (0)