Why This Matters
On a typical 64-bit C++ ABI, int*, int& as a parameter implementation detail, and std::unique_ptr<int> with the default deleter all occupy one machine word. std::shared_ptr<int> usually occupies two words in the handle plus a separately allocated control block containing reference counts. Those sizes change cache traffic and ownership semantics even when the source code has only one extra token.
C gives you addresses and manual free. C++ adds references, destructors, move semantics, and library smart pointers. The point is not to avoid addresses. The point is to state which object owns storage, which object merely observes it, and when cleanup runs.
Core Definitions
Value semantics
An object has value semantics when assignment, copying, and destruction are operations on the object itself rather than on a separately owned resource. For int x = y;, x receives a copy of the integer bits. For std::vector<int> a = b;, a receives its own sequence of int objects.
Raw pointer
A raw pointer T* is an object whose value is an address, or the null pointer value. It may point to one T, the first element of an array of T, storage that no longer holds a live T, or nothing. The type alone does not say whether the pointer owns the object.
Reference
A reference T& names an existing object. In well-defined C++, a reference is not null and cannot be rebound after initialization. Assigning through a reference assigns to the referred object; it does not reseat the reference.
Ownership
An owner is responsible for ending the lifetime of a resource exactly once. A borrower observes or mutates a resource without ending its lifetime. C++ encodes ownership with objects whose destructors release resources, such as std::unique_ptr<T>, std::shared_ptr<T>, std::vector<T>, and std::fstream.
Values, Pointers, and References
A value stores the object. A pointer stores an address. A reference is an alias in the C++ type system; compilers often pass it as an address, but the language rules are different from pointer rules.
#include <cstdio>
void by_value(int x) {
x = 7; // modifies local copy
}
void by_pointer(int* p) {
if (p) *p = 8; // caller may pass nullptr
}
void by_reference(int& r) {
r = 9; // caller must provide an int object
}
int main() {
int a = 1;
by_value(a); // a == 1
by_pointer(&a); // a == 8
by_reference(a); // a == 9
by_pointer(nullptr); // allowed
}
On an LP64 platform, assume a lives at address 0x00007fff00001000.
address bytes meaning
0x00007fff00001000 09 00 00 00 int a, value 9
0x00007fff00000ff8 00 10 00 00 ff 7f 00 00 int* p == &a, little-endian
The pointer object has its own address and its own bytes. A reference variable does not have reseating operations. This distinction shows up in assignment:
int a = 1;
int b = 2;
int* p = &a;
p = &b; // p now points to b
*p = 5; // b == 5
int& r = a;
r = b; // a == b, r still refers to a
A reference can be made invalid by undefined behavior, such as dereferencing a null pointer and binding the result. That does not make null a valid reference state.
int* p = nullptr;
// int& r = *p; // undefined behavior, not a null reference value
RAII and Lifetime Boundaries
Resource Acquisition Is Initialization means a resource is acquired by a constructor and released by a destructor. The resource may be heap memory, a file descriptor, a mutex, or a GPU handle. The lifetime of the C++ object becomes the cleanup boundary.
#include <cstdio>
#include <stdexcept>
class File {
std::FILE* f_;
public:
explicit File(const char* path) : f_(std::fopen(path, "rb")) {
if (!f_) throw std::runtime_error("fopen failed");
}
~File() {
if (f_) std::fclose(f_);
}
File(const File&) = delete;
File& operator=(const File&) = delete;
std::FILE* get() const { return f_; }
};
The destructor runs when File leaves scope. No separate close call is required at each return site. This is the same shape used by std::unique_ptr<T> for heap objects.
Raw new returns a pointer. It does not create an owning C++ object around the allocation.
int* p = new int(42); // allocation plus int construction
delete p; // int destruction plus deallocation
For a single int on a common allocator, the visible pointer is 8 bytes, but the allocator may keep a header before the returned address. One possible heap layout is:
allocator address bytes meaning
0x100000f0 21 00 00 00 00 00 00 00 allocator size/class metadata
0x100000f8 2a 00 00 00 int object, value 42
Your program receives 0x100000f8. Calling delete with that same pointer lets the runtime find metadata and release the block. Calling delete twice corrupts the allocator state or aborts.
unique_ptr as Single Ownership
std::unique_ptr<T> represents one owner of a dynamically allocated T. It is move-only. Copying is disabled because two independent owners would both try to delete the same object.
#include <memory>
std::unique_ptr<int> make_counter() {
auto p = std::make_unique<int>(0);
*p += 1;
return p; // move from return object
}
int main() {
std::unique_ptr<int> a = make_counter();
std::unique_ptr<int> b = std::move(a);
// a.get() == nullptr after the move is the usual library behavior
// b owns the int and deletes it at the end of main
}
For the default deleter, most implementations store only the raw address:
#include <memory>
#include <cstddef>
#include <cstdio>
int main() {
std::printf("%zu\n", sizeof(int*));
std::printf("%zu\n", sizeof(std::unique_ptr<int>));
}
A typical 64-bit output is:
8
8
That is the zero-overhead claim in its narrow sense: the handle has the same size as T*, and destruction compiles to a null check plus delete path. A custom deleter with state changes the handle size.
Use std::unique_ptr<T> for ownership transfer:
void consume(std::unique_ptr<int> p); // takes ownership
void inspect(const int& x); // borrows, cannot be null
void maybe_inspect(const int* p); // borrows, may be null
auto p = std::make_unique<int>(17);
inspect(*p);
maybe_inspect(p.get());
consume(std::move(p));
After std::move(p), do not dereference p. It no longer owns an object.
shared_ptr, weak_ptr, and Cycles
std::shared_ptr<T> represents shared ownership through a control block. The handle usually stores two pointers: one to the object returned by get(), and one to the control block. The control block stores at least a strong count, a weak count, and a deleter.
shared_ptr handle, 16 bytes on typical LP64
offset bytes meaning
0 8 T* object pointer
8 8 control_block* pointer
control block, implementation-specific
offset bytes meaning
0 8 strong count, updated atomically
8 8 weak count, updated atomically
16 ... deleter, allocator data, or inline T for make_shared
Copying a shared_ptr increments the strong count. Destroying one decrements it. When the strong count reaches zero, the T object is destroyed. The control block remains until the weak count also reaches zero.
#include <memory>
#include <cstdio>
int main() {
auto a = std::make_shared<int>(5); // strong == 1
{
std::shared_ptr<int> b = a; // strong == 2
std::printf("%ld\n", a.use_count());
} // strong == 1
std::printf("%ld\n", a.use_count());
} // strong == 0, int destroyed
A typical output is:
2
1
The count operations are atomic for ownership bookkeeping, but the pointee is not protected. Two threads that both do ++*sp still need synchronization for the int.
A cycle keeps strong counts above zero forever:
#include <memory>
#include <string>
struct Node {
std::string name;
std::shared_ptr<Node> next;
~Node() { /* observable in a debugger or log */ }
};
void leak_cycle() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // b strong count becomes 2
b->next = a; // a strong count becomes 2
} // local a and b drop, each node still has strong count 1
Break non-owning back edges with std::weak_ptr<T>:
struct TreeNode {
std::weak_ptr<TreeNode> parent; // observes
std::shared_ptr<TreeNode> left, right; // owns children
};
void use_parent(const std::shared_ptr<TreeNode>& n) {
if (auto p = n->parent.lock()) {
// p is a shared_ptr while this block runs
}
}
weak_ptr::lock() checks whether the strong count is nonzero and returns a new shared_ptr if the object is still alive.
Const Correctness for Borrowing
Const placement matters because pointers are objects and also refer to objects.
void f(const int& r); // borrowed int, f cannot modify the int through r
void g(const int* p); // p may reseat locally, *p is const
void h(int const* p); // same as const int*
void k(int* const p); // p cannot reseat locally, *p is mutable
void m(int const* const p); // p cannot reseat locally, *p is const
Read declarations from right to left around * and const:
int x = 1;
int y = 2;
const int* a = &x;
a = &y; // ok
// *a = 3; // error
int* const b = &x;
// b = &y; // error
*b = 3; // ok
int const* const c = &x;
// c = &y; // error
// *c = 4; // error
For parameters, prefer const T& when null is not a valid input and the function does not need ownership. Use T* or const T* when null has a meaning. Use std::unique_ptr<T> by value to take ownership. Use std::shared_ptr<T> by value only when the function must extend shared lifetime.
Refactoring Raw new and delete
Start with code that has an ownership hole:
#include <stdexcept>
struct Model {
int* weights;
int n;
explicit Model(int n_) : weights(new int[n_]), n(n_) {}
~Model() {
delete[] weights;
}
};
int score(bool fail) {
Model* m = new Model(4);
m->weights[0] = 10;
if (fail) return -1; // leak, delete is skipped
int out = m->weights[0];
delete m;
return out;
}
Refactor the owning pointer first:
#include <memory>
struct Model {
std::unique_ptr<int[]> weights;
int n;
explicit Model(int n_) : weights(std::make_unique<int[]>(n_)), n(n_) {}
};
int score(bool fail) {
auto m = std::make_unique<Model>(4);
m->weights[0] = 10;
if (fail) return -1; // Model destructor runs
return m->weights[0];
}
The byte-level ownership state before and after is different even though the payload array is still on the heap.
raw version
stack m: 8-byte pointer to Model
heap Model: 8-byte pointer weights, 4-byte int n, padding
heap int[4]: 10 00 00 00, 00 00 00 00, 00 00 00 00, 00 00 00 00
unique_ptr version
stack m: 8-byte unique_ptr handle to Model
heap Model: 8-byte unique_ptr<int[]> handle, 4-byte int n, padding
heap int[4]: same 16 payload bytes
The cleanup path moved from handwritten control flow into destructors.
Key Result
For everyday C++ ownership, use these invariants:
unique_ptr<T>has an owner count in .- A
shared_ptr<T>object dies when the strong count reaches 0. weak_ptr<T>observes without adding to the strong count.
These formulas are not performance laws. They are lifetime laws. They say which operations can end the object. unique_ptr destruction, reset, and move assignment may delete. shared_ptr destruction, reset, and assignment may delete only when that operation decrements the last strong owner. weak_ptr cannot keep the object alive.
A practical ownership table follows.
parameter type meaning
T pass or return a value
T& borrow mutable, not null
const T& borrow read-only, not null
T* borrow mutable, null allowed by convention
const T* borrow read-only, null allowed by convention
std::unique_ptr<T> take or return single ownership
const std::unique_ptr<T>& usually a smell; prefer T* or const T&
std::shared_ptr<T> share ownership and extend lifetime
const std::shared_ptr<T>& observe handle without incrementing count
std::weak_ptr<T> observe shared object without owning it
The main failure mode is encoding ownership in comments while the type says T*. A comment cannot run a destructor.
Common Confusions
A reference is just a pointer with nicer syntax
Many ABIs pass references as addresses, but the language contract differs. A pointer can be null and reseated. A reference must bind during initialization and assignment writes to the referred object. This difference changes overload design and precondition checks.
shared_ptr makes the pointee thread-safe
shared_ptr coordinates the lifetime counts. It does not make T atomic. If two threads mutate *sp, protect the pointee with a mutex or use atomic operations inside T.
const unique_ptr<T>& is the right way to borrow
A function that only needs the T should take T&, const T&, T*, or const T*. Taking const std::unique_ptr<T>& exposes the caller's ownership wrapper without taking ownership.
Exercises
Problem
Given the code below, list the values of a, b, p == &a, and p == &b after each numbered line.
int a = 1;
int b = 2;
int* p = &a;
int& r = a;
*p = 3; // 1
p = &b; // 2
r = 4; // 3
*p = 5; // 4
Problem
Refactor this function so that it has no raw new or delete, while preserving the returned values.
int f(int n) {
int* xs = new int[3];
xs[0] = n;
xs[1] = n + 1;
xs[2] = n + 2;
if (n < 0) {
delete[] xs;
return -1;
}
int out = xs[0] + xs[2];
delete[] xs;
return out;
}
Problem
A program creates two nodes with std::make_shared<Node>(). Each node has std::shared_ptr<Node> peer;. It then assigns a->peer = b; b->peer = a; and exits the function. Compute the strong counts just before function exit and just after the local variables are destroyed. Then change one peer to std::weak_ptr<Node> and recompute.
References
Canonical:
- Bjarne Stroustrup, A Tour of C++ (3rd ed., 2022), ch. 1 and ch. 5, covers objects, pointers, references, resource management, and smart pointers
- Bjarne Stroustrup, The C++ Programming Language (4th ed., 2013), ch. 7, ch. 13, and ch. 34, covers references, destructors, exceptions, and resource management
- Randal E. Bryant and David R. O'Hallaron, Computer Systems: A Programmer's Perspective (3rd ed., 2016), ch. 3 and ch. 9, covers machine-level representation and memory allocation context
- Scott Meyers, Effective Modern C++ (2014), Items 18 to 22, covers
unique_ptr,shared_ptr,weak_ptr, andmake_unique - ISO/IEC 14882:2020, Programming Languages, C++, [dcl.ref], [expr.delete], [memory.smartptr], specifies references, delete expressions, and smart pointer library behavior
Accessible:
- cppreference.com, "Reference declaration", "std::unique_ptr", "std::shared_ptr", and "std::weak_ptr"
- C++ Core Guidelines, R.1 to R.37 and F.15 to F.21, covers ownership rules and parameter passing
- Herb Sutter, "GotW #89 Smart Pointers", covers ownership choices and common smart pointer mistakes
Next Topics
- /computationpath/move-semantics-and-rvalue-references
- /computationpath/destructors-raii-and-resource-management
- /computationpath/cpp-object-lifetime-and-storage
- /computationpath/memory-allocation-and-fragmentation