Memory Management and Object Lifecycle in C++

Memory Allocation: new vs. malloc

The distinction betwean new and malloc lies at the core of C++’s object model:

  • Language integration: new is an operator built into C++, while malloc is a C-standard library function declared in <cstdlib>.
  • Unit of allocation: new allocates memory sized for a specific type (e.g., new Widget), where as malloc requests raw bytes (e.g., malloc(sizeof(Widget))).
  • Initialization: new invokes constructors, enabling type-safe initialization; malloc returns uninitialized memory.
  • Portability & semantics: new is universally supported across conforming C++ implementations. In contrast, malloc may be unavailable or unsafe in freestanding environments (e.g., embedded kernels or bootloaders).
  • OOP compatibility: Only new properly constructs objects — using malloc bypasses construction logic, risking undefined behavior if used with non-POD types.

Deallocation: delete vs. free

Correspondingly, deallocation must match allocation style:

  • delete ensures destructors run before releasing memory; free performs no cleanup.
  • delete is standard across all C++ toolchains; free lacks guarantees in constrained execution contexts.
  • Mismatched pairs (malloc + delete, or new + free) result in undefined behavior — including memory corruption or silent leaks.
#include <iostream>
#include <cstdlib>

struct ResourceHolder {
    int* data;
    ResourceHolder() : data(new int(42)) {
        std::cout << "ResourceHolder constructed\n";
    }
    ~ResourceHolder() {
        delete data;
        std::cout << "ResourceHolder destroyed\n";
    }
};

int main() {
    // Correct: constructor/destructor invoked
    ResourceHolder* safe = new ResourceHolder();
    delete safe;

    // Dangerous: no constructor called → data uninitialized
    ResourceHolder* unsafe = static_cast<ResourceHolder*>(malloc(sizeof(ResourceHolder)));
    // ... using 'unsafe' here risks undefined behavior

    // Also dangerous: destructor skipped → memory leak
    free(unsafe);

    return 0;
}

Virtual Functions and Object Construction/Destruction

Virtual dispatch relies on runtime type information stored in vtables — but that infrastructure isn’t fully available during construction or destruction:

  • Constructors cannot be virtual: The vtable pointer is not yet set up when a constructor begins executing. Hence, virtual calls inside constructors resolve statically to the current class’s implementation.
  • Destructors should often be virtual: When deleting through a base-class pointer, a virtual destructor ensures the correct derived-class destructor runs first, preventing resource leaks and slicing issues.
  • No dynamic dispatch in ctors/dtors: Even if overridden, virtual function calls inside these functions behave as if they were non-virtual — only the version defined in the currently constructing/destroying class executes.
#include <iostream>

class Animal {
public:
    Animal() {
        std::cout << "Animal::Animal()\n";
        speak(); // Static bind → Animal::speak()
    }
    virtual ~Animal() {
        std::cout << "Animal::~Animal()\n";
        speak(); // Static bind → Animal::speak()
    }
    virtual void speak() { std::cout << "Animal speaks\n"; }
};

class Dog : public Animal {
public:
    Dog() {
        std::cout << "Dog::Dog()\n";
        speak(); // Dynamic bind possible → Dog::speak()
    }
    ~Dog() override {
        std::cout << "Dog::~Dog()\n";
        speak(); // Dynamic bind possible → Dog::speak()
    }
    void speak() override { std::cout << "Dog barks\n"; }
};

int main() {
    Animal* pet = new Dog();
    delete pet; // Requires virtual ~Animal() to avoid UB
}

Type-Safe Downcasting with dynamic_cast

dynamic_cast enables safe, runtime-checked conversions between polymorphic types:

  • Requires at least one virtual function in the involved classes (typically a virtual destructor suffices).
  • For pointers: returns nullptr on failure; safe too test before dereferencing.
  • For references: throws std::bad_cast on failure — requires exception handling.
  • Only valid along inheritance hierarchies (upcasts are implicit; downcasts require verification).
#include <iostream>
#include <typeinfo>

class Shape { public: virtual ~Shape() = default; };
class Circle : public Shape {};
class Rectangle : public Shape {};

int main() {
    Shape* s = new Circle();

    // Safe downcast
    Circle* c = dynamic_cast<Circle*>(s);
    if (c) {
        std::cout << "Cast succeeded: Circle detected\n";
    } else {
        std::cout << "Cast failed\n";
    }

    // This would fail
    Rectangle* r = dynamic_cast<Rectangle*>(s);
    if (!r) {
        std::cout << "Not a Rectangle\n";
    }

    delete s;
}

Tags: C++ memory-management virtual-functions rtti dynamic-cast

Posted on Thu, 21 May 2026 18:56:47 +0000 by PatriotXCountry