C++ Memory Ownership

Subpage of C++ Deep Dive

An up close and personal look at C++.

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:

  1. Type Safety

    C: More permissive; implicit conversions between pointer types are often allowed (e.g., const char* to char* 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.

  2. 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 x

    Unlike 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.
  3. Dynamic Memory Management

    C: Uses malloc, calloc, realloc, and free.

    C++: Introduces new and delete, 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 new keyword, conceptually, the following happen:

    1. Obtain enough storage for Widget
    2. Construct a Widget in that storage

    Similarly, when we use the delete keyword, conceptually, the following happen:

    1. Run Widget's destructor
    2. Release its storage
  4. Smart Pointers (C++ only)

    C++11+ adds std::unique_ptr, std::shared_ptr, and std::weak_ptr to 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;

    p is now a std::unique_ptr<Widget>. When it is destroyed, the widget is deleted.

  5. 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 name

is 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:

  • p is destroyed
  • the allocated int still 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 exception

This 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;   // error

If 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
                  observes

p 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:

p1

is 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:

  1. Two objects, say A and B, are created on the heap via std::shared_ptr.
  2. Object A contains a std::shared_ptr pointing to B.
  3. Object B contains a std::shared_ptr pointing to A.
  4. When external pointers to A and B go out of scope, the reference count for A drops to 1 (held by B), and the count for B drops to 1 (held by A).
  5. 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
   Widget

rather than:

Containing scope
      |
      v
 unique_ptr
      |
      v
   Widget

A 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;   // wrong

This 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 storage

You 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::deque

and 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:

numbers

but 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[] data

This 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 operator

it 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 assignment

This 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 storage

Destroying 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 a

This 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
a

which 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:

T

Use 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 resource

This same pattern can manage:

mutex locks
database transactions
network sockets
GPU handles
window handles
temporary files
threads

That 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 behavior

because 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:

w

automatically 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 ----> allocation

This 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 C

Destroying 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 B

but each child may have a raw pointer back to the parent:

Parent
  |
  | owns
  v
Child
  |
  | observes
  +------> Parent

The 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 Document

Other 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 C

If 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
       T

If 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::string

manage ownership.

A good way to think about this is:

new and delete are 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_ptr

to express dynamic ownership models.

Containers

vector
string
map

which 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 thing

Cleanup 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/delete

A more accurate progression is:

manual resource management
        ↓
constructors/destructors
        ↓
RAII
        ↓
value types + containers + smart pointers
        ↓
ownership encoded in abstractions and types

That 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 released

No 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
               RAII

C++ 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.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.