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
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.
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.
Top comments (0)