Skip to content

Step 1 of 7 · Reading · ~4 min

Learn

Pointers and Memory

Dynamic Memory: new, delete and Object Lifetime

You will be able to say exactly when an object is created and destroyed, pair each new with the right delete, and grow a heap array by hand. In Fundamentals a std::vector grew and freed its own buffer; here you do that work yourself.

Two lifetimes

A local variable is automatic: it is destroyed at the closing brace of its scope. An object made with new is dynamic: it lives until you call delete. A destructor is a member function named ~T() that runs when an object's lifetime ends. Watch both kinds with a class whose constructor and destructor print:

struct Tracer {
    std::string name;
    Tracer(const std::string& n) : name(n) { std::cout << "constructed " << n << "\n"; }
    ~Tracer() { std::cout << "destroyed " << name << "\n"; }
};

int main() {
    Tracer a("a");
    { Tracer b("b"); }                 // b dies at this brace
    Tracer* c = new Tracer("c");
    std::cout << "before delete\n";
    delete c;
    std::cout << "end\n";
}
constructed a
constructed b
destroyed b
constructed c
before delete
destroyed c
end
destroyed a

Remove delete c; and destroyed c never prints: the pointer c dies with main, the object does not. That is a leak.

What new and delete do

new T(args) allocates sizeof(T) bytes (sizeof(T) is the size of one T in bytes: 4 for an int), then runs the constructor in them. delete p reverses both: run the destructor, then free the bytes. Nothing else frees heap memory for you.

Arrays and the four failures

new T[n] allocates n objects in one block and returns a pointer to element 0, the same pointer an array decays to. Elements of int start uninitialised, so write before you read. The block must be released with delete[], because only that form runs all n destructors:

struct Bell { Bell() { std::cout << "ring "; } ~Bell() { std::cout << "hush "; } };
Bell* row = new Bell[3];   // prints: ring ring ring
delete[] row;              // prints: hush hush hush
FailureExampleResult
Leaknew with no deletememory held until exit; no visible error
Double deletedelete p; delete p;undefined behaviour, usually an abort
Use after deletedelete p; *p = 1;undefined; may appear to work
Mismatched formnew int[4] then delete pundefined behaviour

The habit that prevents the middle two is delete[] p; p = nullptr;: deleting a null pointer does nothing, so delete[] on a fresh, empty array is safe too.

Growing a block by hand

A heap array cannot be stretched. To get more room: allocate a bigger block, copy the old contents across, delete[] the old block, then assign the new pointer to the member that held the old one. This example grows a block of characters to fit a whole word, so it loops to pick the capacity:

struct Text { char* chars; int len; int cap; };   // pointer, in use, room

void append(Text& t, const std::string& word) {
    int need = t.len + (int)word.size();
    int room = t.cap;
    while (room < need) room = (room == 0) ? 8 : room * 2;
    if (room != t.cap) {
        char* bigger = new char[room];
        for (int i = 0; i < t.len; ++i) bigger[i] = t.chars[i];
        delete[] t.chars;              // nullptr on the first call: harmless
        t.chars = bigger;
        t.cap = room;
    }
    for (int i = 0; i < (int)word.size(); ++i) t.chars[t.len + i] = word[i];
    t.len = need;
}

Text t = {nullptr, 0, 0};
append(t, "ab");          // need 2: cap 0 -> 8
append(t, "cdefghijk");   // need 11: cap 8 -> 16
for (int i = 0; i < t.len; ++i) std::cout << t.chars[i];
std::cout << " len=" << t.len << " cap=" << t.cap << "\n";   // abcdefghijk len=11 cap=16
delete[] t.chars;

Skip the delete[] inside append and the output is identical, yet every growth leaks a block.

Grow by doubling, not by one: over 8 appends, growing by one copies 28 elements in total, doubling copies 7. A capacity of 0 has nothing to double, so the rule needs a starting value: cur == 0 ? 1 : cur * 2 starts at 1, where the example above starts at 8. Giving room back is the same dance with an exact size: allocate exactly the elements in use, copy, delete the old block; with nothing in use there is nothing to allocate.

Common mistakes

  • Replacing the block without delete[]ing the old one. Every growth leaks; a live-block count climbs by one each time.
  • Releasing without resetting. After delete[] p; the pointer, size and capacity still describe a block that is gone. A second release is a double delete.
  • new int[0] for an empty array. It returns a real block you must delete. Use nullptr for "nothing stored".

Your exercise

You get an IntArray struct and a command driver, plus a hook that counts live new[] blocks and prints it as live=. Implement push, shrink and destroy so the printed sizes, capacities and live counts match. The likeliest slip is a destroy that frees the block but leaves the old pointer in place.

Up nextSmart Pointers: unique_ptr, shared_ptr and weak_ptrPointers and Memory

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…