Learning C++ part 3

Table of Contents

Interface in c++

In C++, an interface is not a built-in language construct, but rather a design pattern implemented using a class that contains only pure virtual functions. A pure virtual function is declared using = 0, which makes the class non-instantiable and forces derived classes to provide implementations.

By convention, an interface class typically: avoids storing state (no data members), exposes only behavior, and declares a virtual destructor to ensure proper cleanup through base-class pointers. Although C++ does not strictly forbid constructors or implemented methods in an interface-style class, idiomatic usage keeps it minimal and purely behavioral.

1class IDisplayable { // 'I' prefix by convention
2public:
3    virtual void display() = 0; // Pure virtual function
4    virtual ~IDisplayable() = default; // Always make base destructors virtual
5};

The key idea is that an interface defines a contract. Any class inheriting from IDisplayable must implement display(), guaranteeing consistent behavior across unrelated types.

A more general abstract class, on the other hand, is simply any class that contains at least one pure virtual function. Unlike a strict interface-style class, an abstract class may: provide both pure virtual and fully implemented methods, contain data members (shared state), and define constructors to initialize that shared state.

Abstract classes are particularly useful when modeling a hierarchy of closely related types that share common behavior. For example, a base Animal class might implement a concrete eat() function while leaving makeSound() as a pure virtual function. This allows derived classes to reuse shared logic while still customizing specific behaviors.

virtual keyword

The virtual keyword enables dynamic dispatch, also known asruntime polymorphism. When a function is declared as virtual in a base class, the function that gets executed is determined at runtime based on the actual type of the object, not the type of the pointer or reference.

The most critical place where virtualmatters is the destructor. If you delete a derived object through a base class pointer, the base class destructor must be virtual. Otherwise, only the base portion of the object will be destroyed, and the derived class's destructor will not run. This leads to resource leaks and undefined behavior.

1class Base {
2public:
3    virtual ~Base() = default; // Must be virtual for polymorphic deletion
4};
5
6class Derived : public Base {
7public:
8    ~Derived() {
9        // Cleanup specific to Derived
10    }
11};

Without a virtual destructor, deleting a Derivedobject through a Base*would only call ~Base(), skipping ~Derived().

As a best practice, if a class contains any virtual functions or is intended to be used polymorphically (i.e., accessed through a base pointer or reference), its destructor should always be declaredvirtual. This ensures correct object destruction and safe polymorphic behavior.

stack vs heap

In C++, memory is broadly divided into two regions: the stack and the heap. The stack is automatically managed and typically stores local variables with a well-defined and limited lifetime. Memory on the heap, in contrast, is dynamically allocated and manually managed (either directly using new/delete or indirectly through standard library containers and smart pointers).

C-style arrays and std::arrayare usually allocated on the stack when declared as local variables with a compile-time constant size. Their storage duration follows the scope in which they are defined, meaning they are automatically destroyed when the function exits.

std::vector, however, behaves differently. Even if the std::vector object itself is created on the stack, its elements are allocated on the heap. The vector internally manages a dynamically allocated buffer that can grow or shrink at runtime.

In general, the stack is ideal for small, short-lived, and predictable data, while theheap is suited for data whose size is not known at compile time or must outlive the current scope.

static_cast vs dynamic_cast

In C++, both static_cast anddynamic_cast are used for type conversions, but they serve very different purposes in terms of safety and runtime behavior.

static_cast performs conversions that can be resolved at compile time. It is commonly used for standard type conversions such as int todouble, explicit constructor calls, and upcasting within an inheritance hierarchy. It does not perform runtime type checking, which makes it efficient but potentially unsafe when used for downcasting.

When downcasting (casting from a base pointer to a derived pointer),static_cast assumes the programmer guarantees correctness. If the object is not actually of the derived type, the behavior is undefined behavior.

dynamic_cast, on the other hand, performs runtime type checking. It is specifically designed for safe downcasting in polymorphic hierarchies. If the cast fails, it returns nullptr when casting pointers, or throws std::bad_cast when casting references.

A key requirement is that dynamic_cast only works on polymorphic types, meaning the base class must contain at least one virtual function. This enables runtime type information (RTTI), which dynamic_cast relies on.

In summary, use static_castwhen the conversion is guaranteed to be correct and no runtime check is needed. Use dynamic_cast when performing downcasts in polymorphic hierarchies where type safety must be verified at runtime.

c++ string

A std::string is a high-level container that manages a dynamically sized sequence of characters. The string object itself is typically a small, fixed-size object (often stored on the stack when declared locally). Internally, it maintains metadata such as a pointer to the character buffer, the current size, and the capacity.

For longer strings, the actual character buffer is allocated on the heap. When the string grows beyond its current capacity, it typically allocates a new, larger buffer (often using a growth strategy such as doubling the capacity), copies the existing characters, and then deallocates the old buffer. This allows std::string to resize dynamically.

1Stack:
2[ std::string object ]
3    - pointer
4    - size
5    - capacity
6
7Heap:
8[ 'h','e','l','l','o','' ]

Most modern implementations apply Small String Optimization (SSO). With SSO, short strings are stored directly inside the std::string object itself, avoiding heap allocation entirely. On many 64-bit systems, strings up to roughly 15 characters can fit within this internal buffer, though the exact size is implementation-dependent.

Conceptually, without SSO: the std::string object (pointer, size, capacity) resides on the stack, while the character data lives on the heap.

With SSO enabled: the characters themselves are stored directly inside the string object, eliminating heap allocation for small strings and improving performance.

Memory management is handled automatically throughRAII (Resource Acquisition Is Initialization). When a std::string object goes out of scope, its destructor releases any dynamically allocated memory, ensuring safe and automatic cleanup.