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.
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.
#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;
}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:
#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;
}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
#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 heresensor 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.
#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;
}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
What does
newdo thatmallocdoes not?newallocates and constructs;deletedestructs and frees. - 2
Memory allocated with
new int[10]must be freed with...Array allocations need the array form of delete. - 3
Which three functions does the rule of three refer to?
A class managing a resource usually needs all three to copy and clean up correctly. - 4
When is the object owned by
shared_ptrdeleted?The reference count reaching zero triggers deletion.