So far, we've been using variables without really thinking where they live / who owns them.
In C++, understanding memory is extremely important as it helps you write
- faster programs
- safer code
- avoid nasty bugs like memory leaks
Pointers
Similar to C, if we want to get the memory location of a variable, we can use the & operator. The basic won’t be covered here because it follows from C. Instead, we will take a close up look at the key differences:
- Type Safety
C: More permissive; implicit conversions between pointer types are often allowed (e.g.,
const char*tochar*with warnings).C++: Stricter; such conversions are errors unless explicitly cast.
Example: In C++, you must use
const_cast<char*>(p)if you really intend to drop constness.const char* p = "Hello"; char* q = p; // Warning in C, error in C++In C++ we would need:
const char* p = ...; char* q = const_cast<char*>(p);The advantage is that the code explicitly communicates what category of dangerous conversion is being requested. That explicitness supports C++'s broader type-safety philosophy.
- References vs. Pointers
C: Only pointers exist.
C++: Adds references (
int&), which act as safer aliases for variables. You declare a reference by placing an ampersand (&) after the data type. [1, 2]Example: This avoids explicit dereferencing and null pointer issues.
int x = 5; int& ref = x; // Reference ref = 10; // Changes xUnlike pointers, references are strictly managed by the compiler and must follow these constraints:
- Must be initialized: You cannot declare an empty reference; it must bind to a variable immediately.
- Cannot be reassigned (reseated): Once a reference points to a variable, it points to it forever. Trying to reassign it later will just overwrite the value of the original variable.
- Cannot be null: A reference must always point to valid data. There is no "null reference" equivalent to a
nullptr. - Implicit dereferencing: You do not use
*or->operators to use them. They behave exactly like the original variable.
- Dynamic Memory Management
C: Uses
malloc,calloc,realloc, andfree.C++: Introduces
newanddelete, which call constructors/destructors for objects.Example:
int* arr = (int*)malloc(5 * sizeof(int)); // C free(arr); int* arr = new int[5]; // C++ delete[] arr;These new keywords exist because C++ objects may require construction and destruction, not simply storage allocation (remember C++ support OOP).
When we use the
newkeyword, conceptually, the following happen:- Obtain enough storage for Widget
- Construct a Widget in that storage
Similarly, when we use the
deletekeyword, conceptually, the following happen:- Run Widget's destructor
- Release its storage
- Smart Pointers (C++ only)
C++11+ adds
std::unique_ptr,std::shared_ptr, andstd::weak_ptrto manage memory automatically and prevent leaks.These don’t exist in C, where manual memory management dominates. Instead, in C++ it is possible to have an object that holds the pointer so that it can be given the functionality of destroying the object when the pointer object is removed. This is to avoid the issue of a dangling pointer. Here is an example:
auto p = std::make_unique<Widget>(); // Instead of Widget* p = new Widget;pis now astd::unique_ptr<Widget>. When it is destroyed, the widget is deleted. - Pointer to Member Functions
- C: Functions are not tied to objects, so only function pointers exist.
- C++: Supports pointers to member functions of classes, which require special syntax:
class MyClass { public: void hello() { std::cout << "Hello\n"; } }; void (MyClass::*fp)() = &MyClass::hello; MyClass obj; (obj.*fp)(); // Calls obj.hello()
Coming from C, think of C++ pointers as the same tool but embedded in a richer ecosystem that emphasizes safety and object-oriented design.
Ownership
People often teach C++ memory using two locations:
- the stack
- the heap
That terminology is useful, but it is slightly more precise to talk about storage duration, because the C++ language specification does not require every implementation to literally use a hardware stack and heap in exactly the way those terms suggest.
The C++ standard defines four distinct types of storage duration: automatic, static, thread-local, and dynamic.
Automatic storage duration
void f()
{
int x = 5;
std::string name = "Alice";
}x and name are local objects. When f() ends:
std::string nameis automatically destroyed. There is no explicit delete name; and no free(name); command that runs. The object's destructor runs automatically. This deterministic destruction is fundamental to C++.
Static storage duration
Objects such as globals and static variables generally live until program termination:
int globalCounter = 0;
void f()
{
static int calls = 0;
++calls;
}Their lifetimes are much longer than ordinary local variables.
Thread storage duration
C++ also supports objects whose lifetime is associated with a particular thread:
thread_local int counter = 0;Each thread gets its own instance.
Dynamic storage duration
An object can also be created dynamically:
int* p = new int(42);From the above example, the local variable p may disappear when its scope ends, but the dynamically allocated int does not automatically disappear just because p does.
void bad()
{
int* p = new int(42);
}When bad() returns:
pis destroyed- the allocated
intstill exists - its address has been lost
That is a memory leak (once p disappears, you've lost the only address you had for that allocated int). This is where ownership becomes especially important.
Here is how a memory leak could be avoided:
void good()
{
int* p = new int(42);
// use *p
delete p;
}However, this needs the programmer to think about all the variables they use and manually manage them. This becomes difficult when the program scales. This is a problem. C++'s answer is RAII.
RAII
RAII stands for:
Resource Acquisition Is Initialization
Despite the somewhat awkward name, the principle is simple:
Tie ownership of a resource to the lifetime of an ordinary C++ object.
When the owning object is created, it acquires the resource. When the owning object's lifetime ends, its destructor releases the resource. For example:
void f()
{
std::string name = "Alice";
}You never manually destroy the internal memory owned by name. std::string manages that memory. RAII is therefore not really a “memory trick”. It is a general ownership model for resources.
This means C++ deliberately makes local-object destruction predictable. When the scope of a resource is closing, the destructor of the resource runs:
void f()
{
Resource r;
riskyOperation();
}If riskyOperation() throws an error, we still have r being destroyed because the destructor has been placed on the stack. Visually:
construct r
|
v
riskyOperation()
|
+---- success ----> destroy r
|
+---- exception --> destroy r --> propagate exceptionThis means that cleanup follows object lifetime rather than every individual control-flow path.
Smart Pointers as RAII
Smart pointers ensure that there is automatic deletion of objects when their lexical scope is exceeded.
One of the consequences of this is that we observe exclusive ownership of smart pointers. std::unique_ptr<T> expresses an extremely useful guarantee:
Exactly one unique_ptr owns this object at a time.Example:
auto p = std::make_unique<int>(42);When p dies, the integer dies. This is intentionally illegal:
auto p1 = std::make_unique<int>(42);
auto p2 = p1; // errorIf copying were allowed, both pointers would appear to own the same object. Then when both were destroyed, they could both attempt delete object which would result in a double deletion. So C++ makes exclusive ownership part of the type's semantics.
However, smart pointers do present challenges, notably when used with get. Smart pointers can expose their internal raw pointer:
auto owner = std::make_unique<Widget>();
Widget* p = owner.get();Now:
owner ----owns----> Widget
^
|
p
observesp does not become an owner. If:
owner.reset();the Widget is destroyed. Then p dangles. This illustrates the core separation perfectly:
Possessing an address and owning the object at that address are different things.
Ownership Semantics
Instead of copying a unique_ptr, ownership can be transferred:
auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1);The integer wasn't copied. Its ownership moved. This is one of the most important reasons move semantics became such a major feature of modern C++. Move semantics allow the language to express:
“Transfer this resource instead of duplicating it.”
An important subtlety is that:
std::move(p1)doesn't itself physically move the resource. std::move essentially tells the type system, this object may now be treated as something whose resources can be transferred. Then unique_ptr's move constructor performs the ownership transfer.
Ownership semantics become especially useful in APIs. Suppose:
void setEngine(std::unique_ptr<Engine> engine);Calling it:
auto engine = std::make_unique<Engine>();
setEngine(std::move(engine));communicates something much stronger than a raw pointer could: setEngine is receiving ownership of this Engine. The type itself documents the ownership contract. This is a major impact of modern C++ memory design:
Ownership can often be communicated through function signatures.
Shared Ownership
Sometimes an object genuinely needs multiple owners. C++ provides std::shared_ptr<T> . For example:
auto p1 = std::make_shared<Widget>();
auto p2 = p1;Now both pointers participate in ownership:
p1 ----\
\
---> Widget
/
p2 ----/The object remains alive while at least one owning shared_ptr remains.
When:
p1is destroyed, Widget survives because p2 still owns it. When the final owning shared_ptr disappears, the Widget is destroyed.
A shared_ptr generally participates in a separate control block containing ownership bookkeeping. Conceptually:
shared_ptr A ----\
\
shared_ptr B ------> control block ----> Widget
/
shared_ptr C ----/The control block keeps track of how many shared owners remain. The exact implementation is more sophisticated, but this captures the ownership model.
It might seem tempting to ask:
If shared_ptr automatically handles everything, why not use it everywhere?Because shared ownership has costs. Compared with unique_ptr, it generally requires additional:
- bookkeeping
- storage for a control block
- reference-count management
- synchronization considerations
- reasoning about who is keeping something alive
More importantly, shared ownership makes object lifetime less obvious. With unique_ptr<Widget> you know exactly who owns the object. With many shared_ptr<Widget> instances spread throughout a system, determining why something is still alive can become harder.
Modern C++ therefore generally favors exclusive ownership unless shared ownership is actually required.
The shared_ptr Cycle Problem
Reference counting has an important weakness. Consider the following flow:
- Two objects, say
AandB, are created on the heap viastd::shared_ptr. - Object
Acontains astd::shared_ptrpointing toB. - Object
Bcontains astd::shared_ptrpointing toA. - When external pointers to
AandBgo out of scope, the reference count forAdrops to 1 (held byB), and the count forBdrops to 1 (held byA). - Neither count hits zero, so neither object is deleted.
This keeps their reference counts above zero even when they are no longer accessible from the rest of the program, causing a permanent memory leak because destructors never run.
This is a reference cycle.
C++ solves this with:
std::weak_ptr<T>A weak_ptr refers to an object managed by shared_ptr, but it does not contribute shared ownership. Instead of:
A ----shared----> B
^ |
| |
+------shared-----+you can design:
A ----shared----> B
^ |
| |
+-------weak------+Now B knows about A, but doesn't keep A alive. When all real owners of A disappear, A can be destroyed.
Because the object may already have disappeared, you don't simply dereference a weak_ptr. Instead:
std::weak_ptr<Widget> weak = shared;
if (auto p = weak.lock()) {
p->doSomething();
}lock() attempts to obtain temporary shared ownership. If the object is still alive, p contains a valid shared_ptr. Otherwise it is empty.
Ownership vocabulary
Modern C++ programs become much easier to understand if you use four conceptual categories.
1. Value ownership
Widget w;The surrounding object/scope directly owns w.
2. Exclusive dynamic ownership
std::unique_ptr<Widget>Exactly one owner.
3. Shared dynamic ownership
std::shared_ptr<Widget>Multiple owners determine lifetime together.
4. Non-owning access
Commonly:
Widget*
Widget&
const Widget*
const Widget&
std::weak_ptr<Widget>These provide access without necessarily controlling the object's lifetime.
Value Ownership
One of the most important lessons in modern C++ is that dynamic allocation should not be the starting point.
Instead of:
Widget* widget = new Widget;prefer:
Widget widget;when possible.
Instead of:
std::unique_ptr<std::string> name =
std::make_unique<std::string>("Alice");normally just write:
std::string name = "Alice";Value ownership is easier to understand:
Containing scope
|
v
Widgetrather than:
Containing scope
|
v
unique_ptr
|
v
WidgetA similar issues comes out when using data structure. Dynamic arrays historically required:
int* values = new int[100];
delete[] values;while an individual object requires:
int* value = new int;
delete value;Mixing the forms is incorrect:
int* values = new int[100];
delete values; // wrongThis mismatch is another example of why direct memory management is error-prone.
Dynamic allocation is useful when you actually need properties such as:
- runtime-determined lifetime
- polymorphic objects
- stable allocation independent of containing scope
- certain recursive structures
- very large or dynamically structured data
- shared ownership
But it shouldn't automatically be used just because C++ provides new.
Containers as owners
You may already have been using ownership without realizing it.
Consider:
std::vector<int> values{1, 2, 3, 4, 5};values manages memory internally.
Conceptually:
vector object
|
| owns
v
dynamic array
[1][2][3][4][5]When the vector is destroyed:
~vector()
|
v
destroy elements
|
v
release storageYou do not write:
delete[] values.data();The vector owns that memory. The same philosophy applies to:
std::string
std::vector
std::map
std::unordered_map
std::dequeand most other standard containers. This is why modern C++ code can perform substantial dynamic allocation while containing almost no visible new or delete.
Ownership also explains why code like this can be dangerous:
std::vector<int> numbers{10, 20, 30};
int* p = &numbers[0];The vector owns the integer. p merely points to it. If the vector reallocates:
numbers.push_back(40);
numbers.push_back(50);
numbers.push_back(60);its internal storage may move. The old pointer may then be invalid. Ownership remained with:
numbersbut the borrowed pointer's validity depended on the owner's storage remaining unchanged. This is another fundamental insight:
Lifetime safety isn't only about deletion. It also involves operations that invalidate references, pointers, and iterators.
Destructors as Ownership Machinery
Consider:
class Buffer {
private:
int* data;
public:
Buffer(std::size_t size)
: data(new int[size])
{
}
~Buffer()
{
delete[] data;
}
};A Buffer object owns its data. Its destructor enforces that ownership:
Buffer destroyed
|
v
~Buffer()
|
v
delete[] dataThis is essentially implementing your own miniature RAII type. But now another problem appears. What happens if we copy it? Suppose:
Buffer a(100);
Buffer b = a;Without a custom copy constructor, data might simply be copied. Then:
a.data ----\
>---- same allocation
b.data ----/When a is destroyed:
delete[] data;Then when b is destroyed:
delete[] data;The same allocation is deleted twice. Undefined behavior follows. Resource ownership therefore affects how copying must behave.
An owning type must decide what copying means. One possibility is deep copying:
Before:
a ----> [1][2][3]
After copying:
a ----> [1][2][3]
b ----> [1][2][3]Two independent resources exist. Alternatively, copying may not make logical sense at all. For something representing an exclusive OS handle or unique_ptr, the operation can simply be forbidden. This is why resource ownership is tightly connected to C++'s constructors and assignment operators.
The Rule of Three
Historically, if a class manually managed a resource and needed one of these:
destructor
copy constructor
copy assignment operatorit usually needed all three. For example:
class Resource {
public:
Resource(const Resource&);
Resource& operator=(const Resource&);
~Resource();
};This became known as the Rule of Three. The reason is ownership. Once a class manually controls destruction, default copying frequently produces incorrect ownership semantics.
C++11 added move semantics. Resource-owning classes then needed to consider:
destructor
copy constructor
copy assignment
move constructor
move assignmentThis became the Rule of Five. For example:
class Resource {
public:
Resource(const Resource&);
Resource& operator=(const Resource&);
Resource(Resource&&);
Resource& operator=(Resource&&);
~Resource();
};Modern C++ usually aims for something better:
Don't manually manage low-level ownership inside ordinary application classes.
Instead:
class Person {
std::string name;
std::vector<int> scores;
};Both members already manage their own resources. Therefore Person often needs no:
- destructor
- copy constructor
- copy assignment
- move constructor
- move assignment
The compiler-generated operations simply compose the correct behavior of the members. This is called the Rule of Zero. It is one of the clearest demonstrations of how ownership influences modern C++ design.
Composition of Ownership
Suppose:
class Car {
Engine engine;
std::vector<Wheel> wheels;
std::string registration;
};A Car owns its members. Conceptually:
Car
├── Engine
├── vector
│ └── Wheel storage
└── string
└── character storageDestroying the Car causes its members to be destroyed automatically. Their destructors recursively clean up their resources. This creates a tree of ownership. RAII works extremely well when ownership forms clear hierarchies like this.
If objects are created:
A a;
B b;
C c;then, broadly speaking, local destruction occurs in reverse construction order:
construct a
construct b
construct c
destroy c
destroy b
destroy aThis matters because resources frequently depend on other resources. C++ deliberately makes destruction ordering predictable enough for RAII relationships to work.
Exception Safety
Consider:
void process()
{
std::unique_ptr<Widget> a =
std::make_unique<Widget>();
std::unique_ptr<Database> b =
std::make_unique<Database>();
riskyOperation();
}If, riskyOperation() throws, stack unwinding destroys:
b
awhich destroys their owned resources. No hand-written cleanup branch is required. Without RAII, you would need something conceptually like:
Widget* a = new Widget;
Database* b = nullptr;
try {
b = new Database;
riskyOperation();
}
catch (...) {
delete b;
delete a;
throw;
}
delete b;
delete a;RAII greatly reduces this bookkeeping. The connection between exceptions and deterministic destructors is therefore a deliberate part of C++'s design philosophy.
Virtual Destructors
Suppose a derived object is owned through a base-class pointer.
If deletion is intended through that base type, the base class needs an appropriate virtual destructor:
class Animal {
public:
virtual ~Animal() = default;
};Then:
std::unique_ptr<Animal> animal =
std::make_unique<Dog>();can correctly destroy the complete Dog. This shows that polymorphism and ownership aren't independent topics. The way an object is owned influences how its type hierarchy must be designed.
Ownership and Function Parameters
Function signatures can communicate a surprisingly rich ownership model.
Borrow and modify
void update(Widget& widget);Meaning the caller keeps ownership; function gets temporary mutable access.
Borrow read-only
void inspect(const Widget& widget);Meaning the caller keeps ownership; function gets temporary read-only access.
Optional borrowed object
void inspect(const Widget* widget);Meaning the function doesn't own the object, and nullptr may represent no object.
Transfer ownership
void store(std::unique_ptr<Widget> widget);Meaning the function receives exclusive ownership.
Share ownership
void registerWidget(std::shared_ptr<Widget> widget);Meaning the function may become one of multiple lifetime owners. The types serve as documentation.
Similar reasoning applies to return values. This:
Widget createWidget();returns a value. This:
std::unique_ptr<Widget> createWidget();communicates:
“I'm giving the caller exclusive ownership of a dynamically managed object.”
This:
Widget* findWidget();often communicates:
“Here is a pointer to an object owned somewhere else.”
Although raw-pointer ownership is technically possible, modern APIs generally avoid using raw pointers to communicate ownership because the intent is ambiguous.
A Modern Guideline
A useful rule of thumb is:
TUse when ordinary value ownership works.
T&
const T&Use for required borrowing.
T*
const T*Use primarily for optional or pointer-like non-owning access.
unique_ptr<T>Use for dynamic exclusive ownership.
shared_ptr<T>Use when lifetime genuinely needs multiple owners.
weak_ptr<T>Use to observe a shared_ptr-managed object without extending its lifetime. These aren't absolute laws, but they form a very strong starting point.
Ownership in general
Imagine:
class File {
FILE* handle;
public:
explicit File(const char* name)
: handle(std::fopen(name, "r"))
{
}
~File()
{
if (handle)
std::fclose(handle);
}
};Here the class doesn't primarily own memory.
It owns a file handle.
The lifetime relationship is identical:
File object
|
owns
v
operating-system file resourceThis same pattern can manage:
mutex locks
database transactions
network sockets
GPU handles
window handles
temporary files
threadsThat is why ownership in C++ should really be understood as resource ownership, with memory being the most familiar example.
48. A Small Correction to Your C/C++ Type-Safety Notes
The following is not the strongest C-versus-C++ example:
const char* p = "Hello";
char* q = p;because discarding const isn't something conforming C simply treats as a harmless implicit conversion; a diagnostic is required.
A clearer difference is void*.
C commonly permits:
void* memory = malloc(100);
int* p = memory;without a cast.
C++ does not implicitly convert an arbitrary void* to int*:
void* memory = /* ... */;
int* p = memory; // error in C++An explicit conversion is required.
C++ generally has a richer and stricter type system around pointer conversions because pointers interact with:
- overload resolution
- inheritance
const- templates
- object lifetime
- constructors/destructors
Also note that in C++:
"Hello"has an array-of-const char type, so:
char* p = "Hello";is ill-formed.
49. C-Style Casts vs C++ Casts
C permits casts such as:
int* p = (int*)something;C++ supports that syntax for compatibility, but provides more specific casts:
static_cast<T>(...)
const_cast<T>(...)
reinterpret_cast<T>(...)
dynamic_cast<T>(...)For example:
const char* p = ...;
char* q = const_cast<char*>(p);The advantage is not that casting somehow becomes safe.
The advantage is that the code explicitly communicates what category of dangerous conversion is being requested.
A const_cast immediately tells the reader:
“This operation is deliberately changing cv-qualification.”
That explicitness supports C++'s broader type-safety philosophy.
50. Be Careful With const_cast
Even though this compiles:
const char* p = "Hello";
char* q = const_cast<char*>(p);you must not assume modifying through q is safe:
q[0] = 'J'; // undefined behaviorbecause the underlying string literal is not modifiable.
A cast may convince the type system to allow an operation syntactically. It does not change the actual properties or lifetime of the underlying object.
No Garbage Collector Requirement
C++ deliberately does not require a tracing garbage collector as its normal lifetime model.
Instead, its traditional model emphasizes:
- values
- deterministic destruction
- RAII
- explicit ownership
- zero-cost abstractions where practical
- programmer control over object lifetime
This has significant consequences.
Advantages
C++ can give developers tight control over:
- allocation
- destruction time
- object placement
- memory layout
- resource acquisition/release
- performance characteristics
That is particularly valuable in:
- game engines
- operating systems
- embedded software
- browsers
- database engines
- low-latency systems
- graphics software
Cost
The programmer and library designer must understand lifetime relationships.
C++ therefore exposes complexity that garbage-collected languages can often hide.
52. C++ Tries to Move Ownership Problems Into Types
Old-style C++ might write:
Widget* createWidget();and rely on documentation saying:
Caller must remember to call delete.Modern C++ can write:
std::unique_ptr<Widget> createWidget();Now the type itself says:
Caller receives ownership.
Furthermore, failing to clean it up normally isn't possible accidentally:
{
auto w = createWidget();
}At the end of the scope:
wautomatically destroys its resource.
The language/library design has shifted correctness away from programmer discipline and toward type-enforced semantics.
53. Copy Semantics and Move Semantics Reflect Ownership
Ownership also explains why C++ distinguishes copying from moving.
Consider:
std::vector<int> a = {1, 2, 3};
std::vector<int> b = a;Copying means both vectors become independent owners of their contents.
Conceptually:
a ----> [1][2][3]
b ----> [1][2][3]But:
std::vector<int> b = std::move(a);can transfer resources instead:
before:
a ----> allocation
after:
a ----> valid but unspecified moved-from state
b ----> allocationThis avoids unnecessarily copying potentially large amounts of data.
Ownership transfer is therefore also a major performance feature.
54. Why This Matters for C++ Performance
Memory ownership isn't only about avoiding crashes.
It also influences performance.
Clear ownership allows C++ implementations and programmers to:
- avoid unnecessary heap allocations
- transfer resources instead of copying them
- place objects directly inside other objects
- destroy resources immediately when no longer needed
- optimize around predictable lifetimes
- use contiguous storage such as
std::vector
For example:
std::vector<BigObject> objects;may store objects directly in contiguous memory.
Compare that with:
std::vector<std::shared_ptr<BigObject>> objects;which introduces a much more complicated allocation and ownership structure.
Neither design is universally correct; ownership requirements determine which one makes sense.
55. Ownership Affects Data Structures
Consider a linked list:
struct Node {
int value;
std::unique_ptr<Node> next;
};This directly describes its ownership structure:
Node A
|
owns
v
Node B
|
owns
v
Node CDestroying A destroys its unique_ptr, which destroys B, which destroys its unique_ptr, which destroys C.
Ownership is encoded directly into the data structure.
56. Non-Owning Relationships Can Still Exist
Suppose we have a tree:
struct Node {
Node* parent = nullptr;
std::vector<std::unique_ptr<Node>> children;
};Ownership is:
Parent
|
+---- owns Child A
|
+---- owns Child Bbut each child may have a raw pointer back to the parent:
Parent
|
| owns
v
Child
|
| observes
+------> ParentThe parent pointer should not own the parent because doing so would create circular ownership.
This is an example where raw pointers are entirely reasonable.
Raw pointers are not “bad”.
Unclear ownership is bad.
57. Ownership Should Usually Form a Clear Structure
One of the best ways to reason about a program is to ask:
Who owns this object?Ideally, the answer is easy.
For example:
Application
|
+--- owns Window
|
+--- owns Menu
|
+--- owns Toolbar
|
+--- owns DocumentOther objects may temporarily reference these objects, but there is a clear lifetime hierarchy.
Problems often begin when ownership looks like:
A owns B
B maybe owns C
C sometimes owns A
D stores B
E might delete CIf nobody can explain the ownership graph, the code is likely to contain lifetime bugs.
58. A Practical Hierarchy of Choices
When deciding how an object should be represented, a useful decision process is:
Can it simply be a value?
|
yes
|
v
TIf not:
Does exactly one thing own it?
|
yes
|
v
unique_ptr<T>If not:
Does its lifetime genuinely require multiple owners?
|
yes
|
v
shared_ptr<T>For access without ownership:
T&
const T&
T*
const T*
weak_ptr<T>depending on the exact situation.
Notice what isn't near the top of the decision tree:
new T;Direct manual allocation should usually be hidden inside a resource-managing abstraction.
59. What Happened to new and delete in Modern C++?
They still matter.
Understanding them is essential because smart pointers and containers eventually rely on allocation mechanisms underneath.
But ordinary application code often should contain very few direct calls to:
new
delete
new[]
delete[]Instead:
std::make_unique<T>()
std::make_shared<T>()
std::vector<T>
std::stringmanage ownership.
A good way to think about this is:
newanddeleteare low-level mechanisms. RAII types are the higher-level ownership abstraction.
This is similar to how knowing assembly helps you understand a machine without meaning every application should be written in assembly.
60. Ownership and API Design
A good API should make answers to lifetime questions obvious.
Consider:
void addChild(Node* node);What does this mean?
Does the function:
- copy the node?
- store the pointer?
- delete it later?
- borrow it temporarily?
- expect the caller to keep it alive?
The type doesn't say.
Compare:
void addChild(std::unique_ptr<Node> node);Much clearer:
Ownership is transferred into addChild.Or:
void inspectNode(const Node& node);Clear again:
Borrow the node for this operation.
Ownership-aware types therefore improve not only safety but communication between programmers.
61. The Core Impact on the Language
Memory ownership has influenced an enormous portion of C++.
It explains why the language has or heavily relies upon:
Destructors
~T();so lifetime termination can trigger cleanup.
Constructors
T();so objects can establish invariants and acquire resources.
Copy constructors
T(const T&);so resource-owning objects can define what copying means.
Move constructors
T(T&&);so ownership can be transferred efficiently.
References
T&to represent non-owning aliases.
Rvalue references
T&&which enable move semantics and forwarding.
Smart pointers
unique_ptr
shared_ptr
weak_ptrto express dynamic ownership models.
Containers
vector
string
mapwhich encapsulate dynamic memory ownership.
RAII
which turns lifetime into the primary cleanup mechanism.
Exception unwinding
which automatically invokes destructors while propagating exceptions.
These features aren't isolated additions.
They form a coherent resource-management model.
62. Comparing C and Modern C++
The conceptual difference can be summarized like this.
Traditional C code often exposes the ownership operations directly:
Thing* thing = malloc(sizeof(Thing));
/* use thing */
free(thing);The programmer must ensure that every path obeys the ownership contract.
Modern C++ tends toward:
auto thing = std::make_unique<Thing>();
// use thingCleanup follows the owner's lifetime automatically.
Even better, if dynamic allocation isn't needed:
Thing thing;No pointer is needed at all.
So the key development from C to C++ isn't simply:
malloc/free
↓
new/deleteA more accurate progression is:
manual resource management
↓
constructors/destructors
↓
RAII
↓
value types + containers + smart pointers
↓
ownership encoded in abstractions and typesThat is the much more significant change.
63. Revisiting Your Original Pointer Notes
I would slightly reorganize your original section.
Your current points about:
- type safety
- references
- dynamic allocation
- smart pointers
are relevant.
Pointers to member functions, however, aren't really part of memory ownership. They belong in a broader discussion of pointer syntax or C++ object/member semantics.
For a memory-ownership chapter, I would instead build the pointer section around:
1. A pointer stores an address.
2. Pointer lifetime and pointee lifetime are independent.
3. Raw pointers do not inherently express ownership.
4. Raw pointers can be null.
5. Raw pointers can dangle.
6. References usually express required non-owning access.
7. Dynamic allocation introduces explicit lifetime responsibility.
8. RAII transfers that responsibility to objects.
9. unique_ptr expresses exclusive ownership.
10. shared_ptr expresses shared ownership.
11. weak_ptr expresses non-owning observation of shared objects.That creates a much stronger conceptual progression.
64. The Four Questions to Ask About Any C++ Object
A useful mental checklist is:
1. Who owns it?
scope?
another object?
unique_ptr?
several shared_ptrs?2. How long does it live?
until the scope ends?
until its owner dies?
until the final shared owner disappears?
for the whole program?3. Who may access it?
references?
raw pointers?
iterators?
weak_ptr?4. Can those observers outlive it?
If yes, you may have a dangling-pointer problem.
Those four questions explain a large percentage of C++ lifetime bugs.
65. The Central Philosophy
The deepest idea is not:
“Remember to delete what you allocate.”
That is old-fashioned manual-memory thinking.
The stronger modern C++ principle is:
Every resource should have a clearly defined owner, and ownership should preferably be represented by an object's type and lifetime.
Then destruction becomes automatic.
For example:
void run()
{
Database db;
std::vector<Record> records;
auto engine = std::make_unique<Engine>();
// ...
}When the scope exits:
engine destroyed
↓
Engine destroyed
records destroyed
↓
elements destroyed
↓
storage released
db destroyed
↓
database resource releasedNo explicit cleanup procedure is necessary.
The Big Picture
If you want one diagram tying the whole subject together, use this:
OBJECT
|
+------------+------------+
| |
value ownership dynamic ownership
| |
T / container +-----+------+
| |
exclusive shared
| |
unique_ptr<T> shared_ptr<T>
|
weak_ptr<T>
non-owning observer
Non-owning access to any suitable object:
T&
const T&
T*
const T*And underlying all of it:
OBJECT LIFETIME
|
v
destructor executes
|
v
owned resources released
|
v
RAIIC++ memory management therefore isn't primarily about memorizing new, delete, pointers, or smart-pointer syntax. It is about building predictable lifetime relationships.
Once you start looking at C++ in terms of owners, resources, borrowers, transfers, and lifetime, features that can initially seem unrelated—destructors, move semantics, smart pointers, references, containers, exception handling, and the Rule of Zero—start fitting into a single coherent design.