December Code

C++ classes and constructors interview questions, with answers

C++ gives a class far more control over its own life than most languages: which constructors exist, how it is copied and moved, and what happens when it is destroyed. Interview questions probe that control, from the special member functions the compiler writes for you to the braces that call a different constructor from the parentheses. The general questions about constructors, initialiser lists and destructors are answered on the OOP constructor page; this page takes the C++-specific ones, each with code that was compiled and run.

The questions start with the rules of three, five and zero, move through the keywords that control construction, and end with the copies C++ is allowed, or required, to skip. Then take the free C++ diagnostic — ten questions across every C++ topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What are the rules of three, five and zero in C++?

    In short: A class that needs a custom destructor, copy constructor or copy assignment usually needs all three; with move semantics that becomes five; and the best class needs none, letting members manage themselves.

    If a class owns a raw resource, the compiler-generated copy operations copy the handle, not the resource, so two objects end up owning one buffer and both free it. The rule of three says that writing any of the destructor, copy constructor or copy assignment operator means you almost certainly need all three. C++11 added the move constructor and move assignment, making it the rule of five: declaring a copy operation or a destructor also stops the compiler generating the moves. The rule of zero is the modern goal: hold resources in members that already manage themselves, such as std::string, std::vector and std::unique_ptr, and write none of the five, so the defaults are correct, as in the class below.

    struct Doc {
        std::string title;
        std::vector<int> pages;
    };
    
    int main() {
        Doc a{"notes", {1, 2}};
        Doc b = a;
        b.pages.push_back(3);
        std::cout << a.pages.size()
            << b.pages.size();
    }
    // 23
  2. 2.What do = default and = delete do in C++?

    In short: = default asks the compiler to generate a special member function with its usual behaviour; = delete removes a function, so any call to it is a compile error.

    = default is used when you want the compiler's version but would not otherwise get it, for example a default constructor alongside another constructor, or when you want to be explicit that the generated copy is correct. = delete makes a function unusable while keeping it visible to overload resolution, which is the modern way to make a class non-copyable, as below, replacing the old trick of declaring the copy constructor private and never defining it. It works on any function, not only special members: declaring void take(double) = delete; next to take(int) rejects a double argument that would otherwise be silently converted to int. A deleted function still takes part in overload resolution; if it wins, the call fails, which is the point.

    struct Handle {
        Handle() = default;
        Handle(const Handle&)
            = delete;
    };
    
    int main() {
        Handle a;
        Handle b = a;
        // does not compile
        std::cout << "ok\n";
    }
    // ok
  3. 3.What does the explicit keyword do on a C++ constructor?

    In short: It stops a constructor that takes one argument from being used as an implicit conversion, so the object can only be created by naming the type.

    Any constructor callable with one argument is also a converting constructor: without explicit, a function taking a Price would silently accept an int, because the compiler would build a Price from it. That hides bugs, such as passing a count where an amount was meant. Marking the constructor explicit keeps direct construction, Price p(5) or Price{5}, and explicit conversions such as static_cast, but rejects copy-initialisation from an int, as in Price p = 5;, and implicit conversions in calls. Most style guides mark every single-argument constructor explicit unless the conversion is genuinely natural, as with std::string from a string literal. Since C++11 explicit also applies to conversion operators, which is how explicit operator bool lets an object be tested in an if without converting to int.

    struct Price {
        explicit Price(int c)
            : cents(c) {}
        int cents;
    };
    
    int main() {
        Price a(250);
        Price b = 250;
        // does not compile
        std::cout << a.cents;
    }
    // 250
  4. 4.What are delegating constructors in C++?

    In short: Since C++11 a constructor can call another constructor of the same class in its initialiser list, so shared initialisation is written once.

    Before C++11, classes with several constructors either repeated the same initialisation in each or moved it into a private init() function, which could only assign members, not initialise them. A delegating constructor names another constructor of its own class as its only initialiser, as in Rect() : Rect(1, 1) {}; the target constructor runs completely first, then the delegating constructor's body. The object is considered constructed once the target finishes, so if the delegating body then throws, the destructor does run. A delegating constructor cannot also initialise members itself, and a cycle of constructors delegating to each other makes the program ill-formed, though compilers need not diagnose it. Default member initialisers, int w = 1;, are the other tool for removing repetition.

    struct Rect {
        int w, h;
        Rect(int a, int b)
            : w(a), h(b) {}
        Rect(int s) : Rect(s, s) {}
        Rect() : Rect(1) {}
        int area() const {
            return w * h;
        }
    };
    
    int main() {
        Rect r;
        Rect q(3);
        std::cout << r.area()
            << ' ' << q.area();
    }
    // 1 9
  5. 5.When does C++ skip the copy constructor entirely?

    In short: Copy elision: since C++17, initialising an object from a temporary of the same type constructs it in place, with no copy or move, and compilers usually elide the copy of a returned local too.

    Returning an object by value used to mean a copy into the caller, which is why old advice said to return through output parameters. C++17 made elision mandatory when an object is initialised from a prvalue of the same type, such as T x = T(); or returning T() from a function: there is no temporary at all, so the code below prints one constructor call and no copy, and the class does not even need a copy or move constructor. Returning a named local, return result;, is named return value optimisation, which is permitted but not required, though every major compiler does it; when it does not happen, the object is moved rather than copied. This is why returning large objects by value is idiomatic C++.

    struct Big {
        Big() {
            std::cout << "ctor ";
        }
        Big(const Big&) {
            std::cout << "copied ";
        }
    };
    
    Big make() { return Big(); }
    
    int main() {
        Big b = make();
    }
    // ctor
  6. 6.Why does std::vector<int> v{3} differ from std::vector<int> v(3)?

    In short: Braces prefer an std::initializer_list constructor when one exists, so v{3} is a vector holding the single value 3, while v(3) calls the size constructor and holds three zeros.

    Brace initialisation, introduced in C++11 as uniform initialisation, has one special rule: if the class has a constructor taking std::initializer_list and the braced values can convert to its element type, that constructor wins, even over a better match. std::vector has one, so v{3} is the list {3}, and v{3, 5} is the list {3, 5}, while v(3) and v(3, 5) call the size and size-with-value constructors, three zeros and three fives. Braces also forbid narrowing conversions, so int x{2.5}; does not compile, which is a reason to prefer them elsewhere. The rule matters most for containers and for any class you write with an initializer_list constructor.

    std::vector<int> a{3};
    std::vector<int> b(3);
    std::cout << a.size() << a[0];
    std::cout << ' ' << b.size()
        << b[0];
    // 13 30
  7. 7.What is the difference between a shallow copy and a deep copy in C++?

    In short: A shallow copy duplicates a pointer, so both objects share one resource; a deep copy duplicates the resource itself, which is what a class owning raw memory must do in its copy operations.

    The compiler-generated copy constructor copies each member, and for a raw pointer member that copies the address, not the memory it points to. Two objects then share one buffer: a change through one is visible through the other, and when both destructors run, the buffer is deleted twice. A deep copy allocates a new buffer and copies the contents, so each object owns its own resource, which is what a class holding a raw owning pointer must write in its copy constructor and copy assignment. The simpler fix, per the rule of zero, is to hold the data in std::vector or std::string, whose copy operations are already deep, as below, or in std::unique_ptr, which makes the class move-only instead.

    struct Buf {
        std::vector<char> data;
    };
    
    int main() {
        Buf a{{'x', 'y'}};
        Buf b = a;
        b.data[0] = 'z';
        std::cout << a.data[0]
            << b.data[0];
    }
    // xz

How the diagnostic asks it

One question from the C++ bank, exactly as a sitting would show it. The bank has 5 on classes & constructors and 30 across C++.

Classes & Constructors · easyCPP-005

Which call in this C++ code does not compile?

struct Counter {
    int n = 0;
    int get() const { return n; }
    void inc() { n++; }
};

int main() {
    const Counter c{};
    c.get();
    c.inc();
}
  1. 1c.get();
  2. 2c.inc();correct
  3. 3Both: no member function can be called on a const object
  4. 4Neither: both calls compile

On a const object, only member functions marked const can be called. get() is declared const, a promise checked by the compiler that it does not modify the object, so c.get() is fine. inc() is not const, so calling it on c fails: g++ says passing 'const Counter' as 'this' argument discards qualifiers. Both is wrong, because const member functions exist precisely so that const objects can use them. Neither ignores that inc() could change c. A const member function that tried to change n would itself fail to compile, unless n were declared mutable.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 C++ questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated