10 Proven C++ Interview Questions

C++ Interview Questions

Introduction

We all know how hard it is to find good programmers to hire.

This is doubly true in the case of C++ programmers, for one simple reason:

The age, scope and complexity of the C++ language make it one of the hardest mainstream programming languages to learn. Thus, it takes a significant amount of C++ knowledge and experience to even be able to assess the expertise of candidate on interview. This is why it’s helpful to have a canned set of interview questions to turn to.

If you are hiring developers you might ask the questions below on real-life interview. Also we suggest to use our C++ test as pre-interview step. This way you can save a lot of time by avoiding incompetent programmers.

The article has unusual format. C++ programming interview questions have been wrapped into two interview stories.

C++ Interview Story #1

My first interview candidate today is Rami. Rami started out as a Java programmer working for a major bank, but taught himself C++ in order to get a job in the game industry.

We shake hands and sit down at the interview table. To get the conversation started, I ask Rami how he got into programming. It turns out he was inspired by the video games of the late ‘80s — so he got a computer science degree, but then lost sight of his passion for a while.

After some more chit-chat about how great a game Elite was, we get down to business. Because of Rami’s background in Java, I want to see whether he “thinks like a C++ programmer” now, or whether he still carries around the mental baggage of the Java world, much of which doesn’t apply in C++. So I start the interview proper with a question about the C++ preprocessor, which has no equivalent in most other languages.

Here’s what I ask:

[Question #1]

“At what stage of compilation is the preprocessor invoked?” I ask.

“Before actual compilation,” Rami answers promptly. “It basically takes the code, transforms it, then passes it on to the compiler. It’s mostly a dumb lexical transformation, which is also what makes it risky to use.”

Sounds like a good summary, but I want to dig a bit deeper, so I ask: “What kind of directives are there?”

#define, of course,” Rami answers. “And #undef to remove a definition. There’s #ifdef and #endif for conditional compilation. Oh, and #include, of course.”

He missed out a few, but I don’t really care. One never needs to list these out in practice. On to something more interesting; I show him the following line of code and ask him which two things are wrong with it:

#define A 2048*B

“There should be parentheses,” he answers immediately. “Otherwise, things could go wrong. Say, for example, when you write A+4… no, that expands to 2048*B+4, so that’s fine. But if you used some operator that has higher precedence than *, you’re in trouble. So you need parentheses, like (2048*B).”

“True,” I say. “Anything else?”

Rami thinks for a while, then says:

“Depending on the definition of B, you might also want to parenthesize its use. Suppose B is defined as 4+4, then you’d get 2048*4+4. So the correct definition of the macro would be (2048*(B)).”

“Yep,” I nod. I note that he didn’t mention names; for instance, if I use a local variable named A, or more likely, a template type argument A, things will break. But I’m willing to overlook that fact, because the example looks contrived and might have put him on the wrong foot.

[Question #2]

“OK, onwards to the next question,” I say.

Here’s what I ask him next:

“Could you explain to me what an automatic object (also known as stack object) is, and what its lifetime is?”

Rami looks slightly unsure. “An object on the stack, you mean? It is created at the point of its definition, and lives until the end of its scope. So basically, from its definition until the first closing curly brace.”

“How about an object on the heap?”

Rami seems to have been expecting the question. “These are created with the new keyword. C++ doesn’t have a garbage collector, so the destructor isn’t called automatically. You have to call delete to clean the object up.”

It’s clear that he knows the basics here, but I want to see whether he knows the practice as well as the theory.

That’s why I ask him the following:

“Why is heap storage discouraged? How can you avoid it?”

“You have to be careful to clean up,” Rami answers, “otherwise you leak memory. You should use stack allocation whenever possible. If it’s not possible, always make sure to wrap your code in try/catch so that you clean up even if an exception is thrown.”

Too bad. I was hoping he would mention the use of smart pointers, but it seems his Java background and the fact that he’s an autodidact in C++ have caught up with him. I dig a bit deeper to see what he knows: “To make it concrete, why is this a bad idea?”

auto p = new int[50];

Rami seems a bit unsure what I’m driving at. “You have to delete it afterwards, and you’ll leak memory if you don’t. It’s better to declare it on the stack. But if you want to return it from a function, for instance, you cannot do that; then you’ll have to wrap the array into an object, like this…” He starts writing some example code, until halfway through, he comes to a realization: “Oh, that’s basically just std::vector. So you should use that instead.”

“Good call,” I admit. Still no mention of smart pointers, sadly.

Would you like to share the article?

[Question #3]

I decide to move on to a different topic. Here’s my next question:

“On that note, while you’re designing classes, could you write the Rule of Five functions for the following class? That is, copy constructor, move constructor, copy assignment operator and move assignment operator?

class DirectorySearchResult {
public:
    DirectorySearchResult(
        std::vector<std::string> const& files,
        size_t attributes,
        SearchQuery(const* query)
        : files(files),
          attributes(attributes),
          query(new SearchQuery(*query))
    {}

    ~DirectorySearchResult() { delete query; }

    //TODO

private:
    str::vector<std::string> files;
    size_t attributes; // of the directory
    SearchQuery* query;
};

Rami takes a look at the code, scratches his chin. “The files and attributes are copied automatically. That’s easy. For the query member, do you want me to clone it, or keep the pointer to the same object?”

“Take a good look at the existing constructor,” I say, not wanting to give away the entire game. “That should be able to answer your question.”

Rami’s face lights up. “Ah, I see, we’re creating a new object here. So the query is owned by the DirectorySearchResult. I wonder why it needs to be a pointer then?”

“Great observation!” I exclaim, surprised.

I seriously hadn’t even seen that. But if we go that route, the whole question becomes trivial. “So let’s pretend that null values are valid for this pointer as well. There may not always be a query.”

Rami nods. “OK. Let’s start with the copy stuff…”

DirectorySearchResult(const DirectorySearchResult& other) {
    this->files = other.files;
    this->attributes = other.attributes;
    this->query = new SearchQuery(*other.query);
}

I note that he doesn’t use the initializer list, and writes a few needless references to this. Both are typical for Java programmers, but mostly harmless. When I point out that we just said that query may be null, he quickly adapts:

if (other.query) {
    this->query = new SearchQuery(*other.query);
}

Sadly, in C++, this may leave a query uninitialized rather than null.

Rami proceeds with the copy assignment operator:

operator=(const DirectorySearchResult& other) {
    this->files = other.files;
    this->attributes = other.attributes;
    if (this->query) {
        delete this->query;
        this->query = 0;
    }
    if (other.query) {
        this->query = new SearchQuery(*other.query);
    }
}

Usage of 0 instead of nullptr or even NULL is a bit old-fashioned; maybe he learned from resources written before the C++11 days? But at least nulls are handled correctly here. He missed out the return value; but his compiler would tell him that, so I’m not too concerned about it. More concerning is that self-assignment is not being checked for, so it will break.

Edging him on, I say: “OK. How about move construction and move assignment?” These concepts are specific to C++, so especially if he only learned “old style” C++, I might expect him to flounder. And indeed he does:

DirectorySearchResult(DirectorySearchResult&& other) {
    this->files = other.files;
    this->attributes = other.attributes;
    this->query = other.query;
    other.query = 0;
}

While this works, the files vector is still being copied, rather than moved, largely negating the performance benefit of a move constructor in the first place. Similarly in the move assignment operator:

operator=(DirectorySearchResult&& other) {
    this->files = other.files;
    this->attributes = other.attributes;
    if (this->query) {
        delete this->query;
        this->query = 0;
    }
    this->query = other.query;
    other.query = 0;
}

A more succinct and idiomatic approach would have been to use swap. Again he missed the return type and return statement, but that was likely due to copy and paste of his previous code.

[Question #4]

“Alright,” I say, “that should do it. Let’s move on.” I still want to see whether he knows anything about smart pointers.

That’s why I ask:

“What happens when a std::unique_ptr is passed by value to a function? For example, in this code snippet?”

#include <memory>

auto f(std::unique_ptr<int> ptr) {
    *ptr = 42;
    return ptr;
}

int main() {
    auto ptr = std::make_unique<int>();
    ptr = f(ptr);
}

Rami looks at the code. “So it takes a unique_ptr, and returns it as well. I don’t think that would even compile. A unique pointer can’t be copied.”

“Correct,” I say. “How could you make it compile?”

It turns out Rami knows a little about smart pointers after all: “You could use a regular pointer, but of course then you’d need to clean it up afterwards. Better is to use a std::shared_ptr instead. It can be copied and returned.”

“That’s one way of doing it,” I admit, “but what if you really had to use a unique_ptr? How could you make that work?”

Rami scratches his chin. “You could pass it by pointer or by reference. By reference would be nicer, because a pointer to a pointer would be confusing.”

“Sure. Any other ways?” I probe.

“Hmm… wrap it in an object? That doesn’t make much sense though,” he mutters. Using std::move does not seem to occur to him – another sign that he might not be well-versed in the modern editions of C++.

[Question #5]

I decide not to press the matter. Instead, I ask something else:

“On the subject of iterators, could you tell me what types there are, and how they relate to each other?”

Here, Rami seems to be back on familiar ground. “There are forward iterators and backward iterators. You also have random access iterators, which you can increment with arbitrary integers, rather than just +1 or -1. Most iterators are input iterators, which let you read, but there are also output iterators like std::back_inserter.”

Something he said about backward iterators has caught my attention, because they don’t really exist.

I decide to dig a bit deeper:

“You mentioned backward iterators. Do any backward iterators exist that cannot go forward?”

“Yes, for example std::vector::rbegin(),” he says, confidently at first. “Oh, wait, that’s actually random access, so it can go forward by incrementing with -1. But, for instance, std::list::rbegin() cannot, because it’s not random access; then again, it can just be decremented to go forwards. Maybe std::map::rbegin()? But I’m not sure about that. Maybe they don’t exist.”

I’ll leave it at that and move on: “Will this code work? Why or why not?”

std::list<int> l{1, 2, 3};
std::sort(l.begin(), l.end());

Glancing over the code, Rami gives the answer I was hoping for: “That doesn’t work. Sorting requires random access, which a linked list doesn’t provide.”

“Quite right,” I say, ending the interview on a high note. “We’re almost out of time here, so I’d like to give you the opportunity to ask me questions in return. Anything you want to know about the job, the company, or anything else really?”

This leads to more nostalgic talk about childhood video games, until a polite knock on the door reminds us that the room is booked for another interview. I say goodbye to Rami and leave with mixed feelings.

On the one hand, Rami is passionate and has come a long way. On the other hand, his answers to my interview questions were a mixed bag, and I’m not convinced that he is as experienced in C++ as we need him to be.

Would you like to share the article?

C++ Interview Story #2

The second interview candidate of the day is Samantha. Samantha has a background in computer science, and has worked as an embedded device programmer in the automotive industry for several years. She’s now interviewing for a job as a C++ programmer for real-time, high-performance data mining.

We make ourselves comfortable and chat a bit about Samantha’s experiences in embedded software. “It’s a very different world,” she says. “Everything has to be small and efficient. On the one hand, it’s a nice puzzle. On the other, it gets really frustrating sometimes.”

[Question #6]

After the introductory chat, I throw my first interview question to see whether Samantha would qualify for the C++ role she’s interviewing for.

Here’s what I ask her:

“For starters, could you tell me what ‘undefined behavior’ means, and how it’s different from ‘unspecified behavior’?”

“Sure,” she says, somehow seeming relieved. “UB means that the standard guarantees nothing about how the program should behave. It might work, it might crash, it might make demons fly out of your nose. In short, this is something you always want to avoid.”

I encourage her to go on.

“Unspecified behavior… I think you mean implementation-defined? As the word says, the standard requires the behavior to be well-defined, but leaves the definition up to the compiler implementation. You want to avoid IB in portable code, but sometimes there’s no way around it. For example, on one CPU architecture I worked with, we needed a special attribute to use the correct calling conventions for functions.”

I want to know about practice as well as theory, so I ask:

“Could you give some examples of undefined behavior?”

Samantha answers without hesitation. “Most common would be dereferencing a null or wild pointer, or otherwise accessing uninitialized memory, like going beyond the bounds of an array. Deleting the same memory twice, or more generally deleting a wild pointer. Reading uninitialized values is another, like if you declare int x; but don’t assign it a value. Many people think you just get garbage, and this is often true in practice, but it’s actually UB so anything could happen.”

I nod for her to continue, so she does:

“Let’s see, what else… arithmetic errors, like division by zero. You may cause a CPU trap, or for all you know it just sets an error bit, gives you a weird value, and you never hear about it again. It’s undefined, so you cannot rely on it.”

A comprehensive enumeration! It’s clear that she has some experience. But let’s see how thorough she really is.

[Question #7]

“Here’s a code snippet,” I say, showing her the code. “Find as many bugs in it as you can!”

#include <vector.h>

void main(int argc, char** argv)
{
    int n;
    if (argc > 1)
        n = argv[0];
    int* stuff = new int[n];
    vector<int> v(100000);
    delete stuff;
    return 0;
}

“Sooo… vector.h? That’s a C-like header, but vector is a C++ construct. It should just be vector, without the .h. Incidentally, where it’s being used, it lacks the std:: namespace reference. I’m not sure about void main… doesn’t that need to be int main in C++? I always use int, but I’m not sure if the standard requires it. But if you’re returning a value, like here, it definitely needs to be int. The rest of the signature is fine…”

Samantha traces the code with her finger, visibly wincing when she gets to the assignment.

“Ouch, if no arguments are given, n is not initialized. And if we do initialize it, better not set it to a pointer! Maybe they wanted to parse it as an integer. That would not go down well either, because argv[0] is the name of the program, not the first argument.”

She continues with the next line:

“I wonder why stuff is a raw array, not just a vector, because the author clearly knows about vector? That would also prevent the memory leak in case the vector allocation throws an exception. And the delete call should be delete[]. Although in practice it will probably work for an array of simple integers, it’s still UB.”

“Wow!” I say, genuinely impressed. “I think you got them all.”

“Well,” she says, “it’s just a toy program. It gets a lot harder in code that actually does something useful.”

[Question #8]

“True that,” I concur. “Let’s see if we have any of that lying around. How about this… could you write a template to tell me if its argument is a pointer?”

“Like a type trait?” she asks, not waiting for an answer. “I think there’s something like std::is_pointer already. It would look something like this:”

template<typename T> struct is_pointer {
    enum { value = false; };
};
template<typename T> struct is_pointer<T*> {
    enum { value = true; };
}

“Template overload resolution will pick the most specific one, so if the type is a pointer, the last one will be selected. Otherwise it falls back to the first.”

“Why the anonymous enums, and not simply a static const bool?” I wonder out loud. My own solution used this, and compiled fine.

“For one, the constants would still occupy memory space, so it’s not a 100% compile time construct anymore,” Samantha explains. “More importantly, you’d need to redefine the existence of value outside the template in order for it to exist, because the assignment of a value in this case does not make it into an actual definition. It would work in some cases, but would fail if you take the address, for example. Strange but true.”

Would you like to share the article?

[Question #9]

Such remarks clearly point towards experience as well as deep knowledge, and are a very positive signal in interviews.

“Nice,” I say, “I didn’t know that! While we’re on the subject of templates, suppose I have a function called insertion_sort which sorts an std::array. Because insertion sort is relatively slow, it should only be used on short arrays. I would like a compile-time check that the array is no longer than 128 elements. Could you write that?”

“So I guess the signature is something like this?” Samantha asks, writing down some code:

template<typename T, size_t N>
void insertion_sort(std::array<T, N> &array);

I confirm.

“Okay,” she nods. “We cannot specialize a template based on a range of values without listing each out individually, but we can use SFINAE magic from std::enable_if. Off the top of my head, and I might be slightly off on syntax here, but I think it goes like this…”

template<typename T, size_t N>
std::enable_if_t<N <= 128, void>
insertion_sort(std::array<T, N> &array);

Highlighting the enable_if_t construct, she explains: “Before C++14, I think we’d have to write std::enable_if<...>::type. This is just a slight shortcut.”

[Question #10]

“Yep, quite right,” I say. It’s a happy coincidence that I had to use this construct yesterday, otherwise I wouldn’t have known. “Let’s write some more interesting code. Could you design the interface for a max heap class? We’ll continue with the implementation later.”

“Wow, it’s been a while since I’ve done that!” Samantha laughs, “I hope I can remember how that works! But the API should be easy enough. Let’s see what we need. Copy and move operations, like the standard containers. Add a value; find and remove the maximum value; maybe size?”

She can see I’m smiling, so she continues:

“Iterators might be nice as well, but I’ll leave them out for now. For implementation, a heap is most efficiently stored in an array. If I base the implementation on a std::vector, I get all the move and copy stuff for free.”

template<typename T>
class MaxHeap {
    public:
        void add(T const &value);
        T const &max() const;
        T remove_max();
        size_t size() const;
    private:
        std::vector<T> elements;
};

Many candidates, when presented with such a question, will implicitly assume integers. It’s nice to see that Samantha starts off with a template, which will make the code more reusable, and writing it more interesting. She also takes care of const correctness. There’s no mention of noexcept, but then again, it might be reasonable for all of these functions except size() to throw an exception.

“Okay,” Samantha says. “So now let’s implement insertion. The array holds nodes in a top-to-bottom order, with the nodes in each row stored from left to right. To find the parent of a node, we shift out the least significant bit. This is a max heap, so we need to make sure that all of a node’s children are smaller than or equal to the node itself. If I recall correctly, you normally start by adding the element to the end of the array, then bubble it up as far as required. Here goes…”

void add(T const &value) {
    size_t index = elements.size();
    elements.push_back(value);
    while (elements[index >> 1] < elements[index]) {
        std::swap(elements[index >> 1], elements[index]);
        index >>= 1;
    }
}

After writing this, she mentally walks through the code to verify it, explaining: “I could add an extra check for when we reach the top, but if the < operator is properly implemented, we'll eventually compare the element to itself if it reaches the top, and the loop will terminate. Note that I only use the < operator here, none of the other comparison operators, which is common in C++ standard library algorithms as well."

She continues with even more detail:

“The max() and size() functions are trivial:”

T const &max() const { return elements.front(); }
size_t size() const { return elements.size(); }

Again, she explains what she’s doing:

“In line with the standard library containers, I decided not to throw an exception if the heap is empty, and just chalk that up to undefined behavior. And now for removal: the textbook technique is to remove the topmost (first) element, put the last element into its place, then bubble it down to restore the heap property.”

T remove_max() {
    T max = elements.front();
    elements.front() = elements.back();
    elements.pop_back();
    size_t index = 0;
    while (true) {
        size_t left = index >> 1;
        size_t right = left + 1;
        if (elements[left] < elements[index] && elements[right] < elements[index]) {
            // Heap property is OK: element is larger than its children.
            break;
        } else if (elements[left] < elements[right]) {
            // Right child is the largest.
            std::swap(elements[index], elements[right]);
            index = right;
        } else {
            // Left child is the largest.
            std::swap(elements[index], elements[left]);
            index = left;
        }
    }
    return max;
}

It takes her less than 10 minutes to write all that, with minimal corrections along the way.

At the end, here's what she says:

"That's the basic idea, but I left out an important bit so I could get the basic structure down. We need to check for left and right to be within the bounds of the vector, otherwise we will invoke undefined behaviour. So it's not complete yet."

"That's okay," I say. "I see where you are going with this, and we're out of time for this interview. Do you have any questions for me in return? About the company, the job opening, the interview process, anything at all?"

"Yeah," she says, smiling slightly. "I was wondering if you'd ever implemented a heap yourself in practice…"

I grin. "To be honest, I haven't. All the more impressive that you could just write it down like that."

I mean it. Textbook algorithms knowledge is nice, but the ability to turn it (or any other problem specification) into functioning, well-designed code is what we are really looking for. Before I even leave the interview room, I have already made up my mind that Samantha will be a definite hire.

Conclusion

There are as many interview styles as there are interviewers, and as many interview methodologies as there are companies. The above are just samples of how C++ interviews could play out, depending on the company, the open position, the interviewer, the candidate, and the coding questions asked.

We hope our readers got some useful info which could be used in their own interviews.

Authors

Many thanks to our developers which have been working on these interview questions as well on main C++ programming test:

Special thanks I want to say to Thomas ten Cate, who wrapped all the C++ questions into interview stories, making them much easier and more interesting to read! Thank you!

  2 Comments

  1. Pingback: How to Make Career in Programming? - Moz Backup

  2. Pingback: Things To Conscious About C++ Interview Questions - DevsTrend

Leave a Reply

Your email address will not be published. Required fields are marked *