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
| Failure | Example | Result |
|---|---|---|
| Leak | new with no delete | memory held until exit; no visible error |
| Double delete | delete p; delete p; | undefined behaviour, usually an abort |
| Use after delete | delete p; *p = 1; | undefined; may appear to work |
| Mismatched form | new int[4] then delete p | undefined 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. Usenullptrfor "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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…