C++

Language-Specific Deep Dives

C++ Deep Dive

C++ is a language renowned for its performance and control. It gives the programmer direct access to memory and low-level hardware features, making it the language of choice for systems programming, game development, high-frequency trading, and other performance-critical domains.

This power comes with responsibility. A deep understanding of C++'s memory management and object lifecycle is essential for writing correct and safe code. This section covers some of the most critical C++ concepts that are frequently tested in interviews.

1. RAII & Smart Pointers

This is arguably the most important C++ idiom.

  • The Problem: In C++, you are responsible for manual memory management. If you allocate memory with new, you must remember to deallocate it with delete. Forgetting to do so causes memory leaks. More than just memory, you might also need to manage file handles, network sockets, or mutex locks. Remembering to release these resources, especially in the face of exceptions, is very difficult and error-prone.

  • The Solution: Resource Acquisition Is Initialization (RAII) is a design pattern that ties the lifecycle of a resource to the lifetime of an object.

    1. You acquire the resource in the object's constructor.
    2. You release the resource in the object's destructor.

    Since C++ guarantees that the destructor of a stack-allocated object is always called when it goes out of scope (whether by normal exit or by an exception), the resource is guaranteed to be cleaned up.

Smart Pointers are the embodiment of RAII for memory management. They are wrapper classes that hold a raw pointer and automatically deallocate the memory when the smart pointer itself is destroyed.

The Three Modern Smart Pointers

  • std::unique_ptr<T>: Represents unique ownership.

    • Only one unique_ptr can point to a given object at a time.
    • It cannot be copied. You must explicitly move ownership from one unique_ptr to another using std::move.
    • This is the most lightweight smart pointer and should be your default choice.
  • std::shared_ptr<T>: Represents shared ownership.

    • Multiple shared_ptr instances can point to the same object.
    • It keeps a reference count of how many shared_ptrs are pointing to the object.
    • The memory is deallocated only when the last shared_ptr is destroyed.
    • Use this when you need to share an object between multiple owners and it's not clear which one will be the last to finish using it. Be wary of circular references, which can cause memory leaks.
  • std::weak_ptr<T>: A non-owning "observer" of a shared_ptr.

    • It points to an object managed by a shared_ptr but does not increase the reference count.
    • It is used to break circular references between shared_ptrs.
    • To access the object, you must try to "lock" it, which creates a temporary shared_ptr. If the underlying object has already been destroyed, the lock will fail.
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass Created\n"; }
    ~MyClass() { std::cout << "MyClass Destroyed\n"; }
};

void use_unique_ptr() {
    // Memory is allocated here.
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();

    // std::unique_ptr<MyClass> ptr2 = ptr1; // COMPILE ERROR: Cannot copy.

    // Ownership is transferred to ptr2. ptr1 is now null.
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);

} // ptr2 goes out of scope here, and the MyClass object is automatically destroyed.

void use_shared_ptr() {
    std::shared_ptr<MyClass> ptr1;
    {
        // Memory is allocated, ref count is 1.
        std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>();
        // Both pointers share ownership, ref count is 2.
        ptr1 = ptr2;
    } // ptr2 goes out of scope, ref count becomes 1.

} // ptr1 goes out of scope, ref count becomes 0, object is destroyed.

Interview Takeaway: RAII is the fundamental C++ pattern for resource management. Smart pointers are the implementation of RAII for memory. Default to unique_ptr for performance and clear ownership, and use shared_ptr only when shared ownership is truly necessary.

2. The Rule of Three / Five / Zero

This is a rule of thumb for C++ class design that relates to RAII and copy semantics.

  • The Rule of Three: If a class needs a user-defined destructor, a copy constructor, or a copy assignment operator, then it almost certainly needs all three.

    • Why?: A user-defined destructor implies the class is managing a resource (like a raw pointer). The default, member-wise copy created by the compiler would be shallow, leading to two objects pointing to and trying to manage the same resource (e.g., double-freeing memory). You must define proper deep-copying behavior.
  • The Rule of Five: With the introduction of move semantics in C++11, the rule was extended. If a class defines any of the "big three," or a move constructor, or a move assignment operator, it should probably define or delete all five.

    • Move semantics allow for the efficient transfer of resources from one object to another without expensive copying. If your class manages a resource, you should define how to "move" that resource.
  • The Rule of Zero: This is the modern ideal. Design your classes so that they don't manage any raw resources directly. Instead, use existing resource-managing classes (like std::string, std::vector, and smart pointers). If your class is just a composition of these well-behaved types, you don't need to write any of the "big five" yourself. The compiler-generated defaults will work correctly.

Interview Takeaway: The Rule of Zero is the goal. Structure your classes to use standard library containers and smart pointers to manage resources. If you must manage a resource manually, you must follow the Rule of Five and carefully define the copy and move semantics for your class.

3. Virtual Functions and Polymorphism

C++ supports runtime polymorphism through virtual functions.

  • What they are: When you declare a function in a base class as virtual, you are telling the compiler that this function may be overridden by a derived class. When you call this function through a base class pointer or reference, the compiler will generate code to look up the correct version of the function to call at runtime based on the actual type of the object. This is called dynamic dispatch.
  • How it works: The compiler typically implements this using a vtable (virtual table). A vtable is a lookup table of function pointers. Each class with virtual functions has its own vtable. Each object of such a class has a hidden pointer (the vptr) to its class's vtable. When a virtual function is called, the program follows the vptr to the vtable to find the address of the correct function to call.

Key Keywords:

  • virtual: In the base class, declares a function as being overridable.
  • override: In the derived class, indicates that you are intentionally overriding a base class virtual function. This is not mandatory but is strongly recommended as it allows the compiler to catch errors (e.g., if you misspell the function name).
  • final: In the derived class, indicates that this function cannot be overridden any further down the inheritance chain.
  • Virtual Destructor: If a class is intended to be used as a base class, its destructor must be declared virtual. This ensures that when you delete a derived class object through a base class pointer, the correct destructor (the derived one, followed by the base one) is called. Forgetting this leads to resource leaks.
#include <iostream>
#include <memory>

class Animal {
public:
    // A virtual destructor is essential for a base class.
    virtual ~Animal() = default;
    virtual void speak() const {
        std::cout << "Animal speaks\n";
    }
};

class Dog : public Animal {
public:
    // 'override' ensures we are correctly overriding a base virtual function.
    void speak() const override {
        std::cout << "Dog barks\n";
    }
};

int main() {
    // Polymorphism in action.
    // 'animal_ptr' is a base pointer, but it points to a derived object.
    std::unique_ptr<Animal> animal_ptr = std::make_unique<Dog>();

    // Because speak() is virtual, the Dog::speak() version is called at runtime.
    animal_ptr->speak(); // Prints "Dog barks"

    return 0;
} // The virtual destructor ensures Dog's destructor is called, then Animal's.

Interview Takeaway: Virtual functions are C++'s mechanism for runtime polymorphism. Explain how they work using the concept of a vtable and dynamic dispatch. Emphasize the critical importance of a virtual destructor in any base class to prevent resource leaks.