Bytebook Compiler

Beyond the Basics

Dynamic Memory & Smart Pointers

new and delete, arrays on the heap, shallow vs deep copy, the rule of three, and modern smart pointers — unique_ptr and shared_ptr — that free memory for you.

5 min read4 runnable examples4-question quiz

new and delete

C++ replaces malloc/free with new and delete. new allocates memory and calls the constructor; delete calls the destructor and frees the memory.

C++
#include <iostream>
using namespace std;

class Book {
public:
    string title;
    Book(const string &t) : title(t) { cout << "construct " << title << endl; }
    ~Book() { cout << "destroy " << title << endl; }
};

int main() {
    int *n = new int(42);                 // one int, initialised to 42
    cout << *n << endl;
    delete n;

    int *arr = new int[5]{1, 2, 3, 4, 5}; // an array
    for (int i = 0; i < 5; i++) cout << arr[i] << " ";
    cout << endl;
    delete[] arr;                         // arrays need delete[]

    Book *b = new Book("C++ Primer");     // constructor runs
    cout << "reading " << b->title << endl;
    delete b;                             // destructor runs
    return 0;
}
Output
42
1 2 3 4 5 
construct C++ Primer
reading C++ Primer
destroy C++ Primer
malloc / free new / delete
function from <cstdlib> operator built into the language
returns void *, needs a cast returns the correct pointer type
size given in bytes size computed from the type
does not call constructors/destructors calls constructors and destructors
returns NULL on failure throws bad_alloc on failure

Shallow copy vs deep copy

If a class owns heap memory through a pointer, the compiler's default copy copies the pointer, not the data — a shallow copy. Two objects then share one buffer, and both destructors free it: a crash.

A deep copy allocates new memory and copies the contents.

The rule of three

If a class needs a custom destructor, it almost certainly also needs a custom copy constructor and copy assignment operator:

C++
#include <iostream>
#include <cstring>
using namespace std;

class Name {
    char *text;
public:
    Name(const char *s) : text(new char[strlen(s) + 1]) { strcpy(text, s); }
    ~Name() { delete[] text; }                                     // 1. destructor
    Name(const Name &o) : text(new char[strlen(o.text) + 1]) {     // 2. copy constructor (deep)
        strcpy(text, o.text);
    }
    Name &operator=(const Name &o) {                               // 3. copy assignment (deep)
        if (this != &o) {                                          // guard against a = a
            char *fresh = new char[strlen(o.text) + 1];
            strcpy(fresh, o.text);
            delete[] text;
            text = fresh;
        }
        return *this;
    }
    void setFirst(char c) { text[0] = c; }
    const char *get() const { return text; }
};

int main() {
    Name a("asha");
    Name b = a;          // copy constructor
    Name c("x");
    c = a;               // copy assignment
    b.setFirst('U');
    c.setFirst('N');
    cout << a.get() << " " << b.get() << " " << c.get() << endl;   // each has its own buffer
    return 0;
}
Output
asha Usha Nsha

(C++11 extends this to the rule of five by adding a move constructor and move assignment. Even better is the rule of zero: let standard types like string, vector and smart pointers own the memory, and write none of these functions.)

Smart pointers

Manual delete is easy to forget, especially when exceptions fly. Smart pointers in <memory> own heap objects and free them automatically — the RAII idea applied to memory.

unique_ptr: sole ownership

C++
#include <iostream>
#include <memory>
using namespace std;

struct Sensor {
    int id;
    Sensor(int i) : id(i) { cout << "sensor " << id << " on" << endl; }
    ~Sensor() { cout << "sensor " << id << " off" << endl; }
};

int main() {
    unique_ptr<Sensor> a = make_unique<Sensor>(1);
    cout << "reading sensor " << a->id << endl;
    // unique_ptr<Sensor> b = a;          // error: cannot copy a unique_ptr
    unique_ptr<Sensor> b = std::move(a);  // ownership moves to b
    cout << "a is " << (a ? "set" : "empty") << ", b owns sensor " << b->id << endl;
    {
        auto c = make_unique<Sensor>(2);
    }                                     // c's sensor freed here automatically
    cout << "end of main" << endl;
    return 0;
}                                         // b's sensor freed here
Output
sensor 1 on
reading sensor 1
a is empty, b owns sensor 1
sensor 2 on
sensor 2 off
end of main
sensor 1 off

shared_ptr: shared ownership

A shared_ptr keeps a reference count; the object is deleted when the last shared_ptr to it is destroyed.

C++
#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Playlist {
    string name;
    Playlist(string n) : name(n) {}
    ~Playlist() { cout << "deleting playlist " << name << endl; }
};

int main() {
    shared_ptr<Playlist> mine = make_shared<Playlist>("Coding Lo-fi");
    cout << "owners: " << mine.use_count() << endl;
    {
        shared_ptr<Playlist> friendCopy = mine;
        cout << "owners: " << mine.use_count() << endl;
    }
    cout << "owners: " << mine.use_count() << endl;
    mine.reset();                          // last owner gone: object deleted
    cout << "done" << endl;
    return 0;
}
Output
owners: 1
owners: 2
owners: 1
deleting playlist Coding Lo-fi
done
Smart pointer Ownership Copyable? Use for
unique_ptr exactly one owner no (move only) the default choice
shared_ptr many owners, reference counted yes genuinely shared objects
weak_ptr observes a shared_ptr without owning yes breaking reference cycles

Test yourself

4 questions · pick an answer to check it instantly

  1. 1

    What does new do that malloc does not?

  2. 2

    Memory allocated with new int[10] must be freed with...

  3. 3

    Which three functions does the rule of three refer to?

  4. 4

    When is the object owned by shared_ptr deleted?