C++ Interview Questions

Paul J. Lucas - Aug 18 - - Dev Community

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  1. The object’s destructor, if any, is called.
  2. 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; }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Notes:

  • The p member is an owning pointer, that is S is responsible for the dynamically allocated, pointed-to T.
  • You may use only those functions explicitly declared here.
  • The details of class T don’t matter. Assume that T(T const&) copies a T object 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
}
Enter fullscreen mode Exit fullscreen mode

Line 2 guards against self-assignment, that is:

x = x;      // silly, but it still has to be correct
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:
  1. The left-hand-side’s T will be memory-leaked immediately.
  2. The left-hand- and right-hand-side’s 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?

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
                             };
Enter fullscreen mode Exit fullscreen mode

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; }
};
Enter fullscreen mode Exit fullscreen mode

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:

  1. Indexes the vtbl pointed to by the object’s _vptr with the slot to get h’s address; then:
  2. Casts the address to the correct pointer-to-function type; then:
  3. Calls the function passing the address of the object as the first parameter to become its this pointer.

That is:

void call_h( B *b ) {
  b->h();  // reinterpret_cast<void(*)()>( b->_vptr[2] )( b );
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode


Conclusion

Those are some decent C++ interview questions. Feel free to use them when interviewing candidates.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .