Concepts / Ownership and Object Lifetime

Ownership and Object Lifetime

A local variable's lifetime is tied to a block. A dynamically allocated object's lifetime is tied to a delete call. Code that forgets the delete call keeps the object alive forever, even though nothing can reach it.

  • Programming

A Bug That Never Shows Up on Screen

Some of the most damaging bugs in C++ produce no error message, no crash, and no wrong answer. The program appears to work correctly for every test you run, yet its memory use keeps creeping upward each time it executes a particular path. Something was obtained and never given back. That pattern is a resource leak, and it is worth understanding before you write any code that asks for memory at runtime.

What do you think happens?

A function creates an object on the heap, returns a pointer to it, and the caller reads a value from that object and then lets the pointer variable go out of scope without deleting anything. What happens to the object?

  • It is deleted automatically when the pointer variable goes out of scope

  • It stays allocated but nothing in the program can reach it anymore

  • It is deleted when the function that created it returns

Reveal answer
Answer:

It stays allocated but nothing in the program can reach it anymore.

Losing the pointer without deleting the object causes a leak. The function that created the object has already finished, and the pointer variable was the only route to the object. Going out of scope destroys the pointer variable, not the heap object it pointed to.

As you read this article, keep one question in mind: at each point in the program, who is responsible for releasing the memory, and has that responsibility actually been carried out? The rest of the article traces that question through concrete situations.

Automatic Cleanup Versus Explicit Lifetime

Memory is the resource this course focuses on, because C++ hands it to you explicitly. When you ask for an object to be created dynamically, the object lives in dynamically allocated memory until you release it yourself. Nothing reclaims it automatically at the end of a block the way a local variable is cleaned up. That explicit lifetime is the source of both the power and the danger.

Compare two objects. A local variable declared inside a function begins its life when the declaration is reached and ends when the enclosing block ends. A dynamically created object begins its life when you ask for it with new, and it ends only when you call delete on it. Its life can therefore continue well past the block, and even the function, in which it was created. The object does not care whether any variable still names it; it persists until something explicitly releases it.

Key point

A local variable's lifetime is tied to a block. A dynamically allocated object's lifetime is tied to a delete call. Code that forgets the delete call keeps the object alive forever, even though nothing can reach it.

What a Resource Leak Is

Definition

A resource leak happens when a resource, such as dynamically allocated memory, is acquired but never released, leaving it reserved yet unreachable.

Notice the two halves of that definition. The resource is reserved, so the program's memory use stays high. It is also unreachable, meaning no pointer in the program still holds its address, so no later statement can release it. A leak is therefore not simply a large amount of memory in use. It is memory that the program has lost the ability to give back.

Memory is the resource most often involved in C++ leaks for a structural reason. C++ hands you the explicit tools to request dynamic memory and to release it, and it expects your code to pair those two actions. Other resources exist, but the course's central lesson is about the free store, where every acquisition needs a matching release that the language will not insert for you.

Following a Pointer Returned from a Function

Functions often build objects and hand them back to the caller as pointers. The function creates the object in dynamic memory, returns the address, and then its own work is finished. Whoever received that address now holds the only way to reach the object. The caller has inherited the duty to release it. Ownership, in this sense, is the responsibility to call delete on an object exactly once when it is no longer needed.

Consider a function named read_shape() that reads a description from input, constructs a Shape object on the heap, and returns a pointer to it. The function itself does nothing wrong. The danger lies in the caller. The following code is a generated illustration of that situation.

cpp
Notes on the code
  • read_shape allocates the object with new and returns its address; its work ends at the return
  • The caller receives the only pointer to the object and inherits responsibility for it
  • Leaving caller without a delete means the object is still allocated but no pointer reaches it
caller()owns pointer after returnread_shape()allocates, then returnsShape objecton the free store1. call read_shape()2. new Shape3. return pointer, callee's work ends4. uses object, no delete, pointer goes out of scope
Who holds responsibility for the Shape object at each step, from the callee allocating it to the caller receiving it, and where the delete that would discharge it never happens.

Read the sequence as a relay. Responsibility for the object is created in step two, passes to the caller in step three, and is never explicitly handed onward or discharged. The last message is the one that matters: the caller uses the object and then drops the only pointer to it. From that point the object is unreachable while still occupying memory, which is exactly the situation the source warns about, where a user might fail to delete the pointer returned by read_shape().

Worked example

Tracing the Ownership Chain in read_shape

Using the caller() code above, identify at each line who is responsible for releasing the Shape object and whether that responsibility is ever carried out.

  1. 1
    Inside read_shape, new Shape()

    The callee acquires the resource. Responsibility for releasing it does not yet belong to anyone outside the function, but the callee is about to hand it over.

  2. 2
    return s

    The callee finishes and returns the address. The caller now holds the only way to reach the object, so the duty to release it passes to the caller.

  3. 3
    Inside caller, s->draw()

    The caller uses the object. Using it does not discharge ownership; ownership is discharged only by a delete.

  4. 4
    End of caller with no delete

    The pointer variable s is destroyed, but the Shape object is not. The responsibility was never carried out, so the object is leaked.

Result

The callee was correct to allocate and return the object. The leak is created entirely in the caller, which received ownership and dropped it without calling delete.

cpp
Notes on the code
  • The delete is placed in the caller, the party that received ownership
  • The object is released exactly once, on the path that uses it

Containers of Pointers

A second opportunity for a leak appears when objects are stored in a container through pointers. A container of Shape pointers holds addresses, not the shapes themselves. Removing the pointers from the container, or destroying the container, does not by itself release the objects they refer to. Those objects remain allocated until somebody explicitly deletes them.

cpp
Notes on the code
  • Each push_back stores an address; the vector never creates or owns the Shape objects themselves
  • The vector's destruction releases the list of addresses, not the objects those addresses name
shapes vectordestroyed at end of scopepointer 0stored address, destroyedwith vectorShape Astill allocatedpointer 1stored address, destroyedwith vectorShape Bstill allocatedholdsholdspoints topoints to
When the vector holding the pointers goes out of scope, the heap objects remain, and no remaining path reaches them to delete them.

The diagram shows the failure. The stack-side container and its pointers are gone once the scope ends, but the two Shape objects on the heap are untouched. Each one is reachable only through a pointer that no longer exists, so no statement in the program can delete it. The container cleaned itself up perfectly and the leak still happened, because the container was never responsible for the objects in the first place.

The source describes the owner of such a container as the party who might forget to delete the pointed-to objects. Ownership is the key idea. Someone must be designated as the owner, and that owner must release every element. If the container is passed around, the question of who owns its elements has to be answered as clearly as the question of who owns the container.

cpp
Notes on the code
  • The loop deletes each pointed-to object, which is the step the destroyed vector cannot perform
  • The container is cleared afterwards so it no longer holds addresses to objects that have been deleted

Finding Leaks Along Each Branch

A leak often hides on one path through a function rather than on the path you tested. To find it, follow every possible route from the acquisition to the function's exit and ask whether the allocated resource reaches a release point on each one. An early return is the most common place where a route escapes without releasing it.

cpp
Notes on the code
  • The allocation happens before the branch, so every route through the function must release it
  • The delete sits only on the path that reaches the end of the function
new Shaperesource acquiredn < 0 ?branch pointreturnno delete on this pathleaked Shapeunreachabledraw then deleteresource releasedfunction exitall paths end hereyesnoexitsreleased
Along each possible execution path, does the allocated Shape reach a delete, or does one branch exit without releasing it?

The flowchart makes the defect visible. The yes branch leaves the function through return, and the Shape acquired at the top is still held by no one. The no branch reaches a delete before the exit. A test that only passes a nonnegative n will always take the release path and will never reveal the leak. This is why the source notes that such a bug can look correct on every test you run.

cpp
Notes on the code
  • Each exit route now discharges the ownership of s before the function ends

Mistakes Beginners Make with Ownership

  • Common mistake

    Assuming that a pointer going out of scope deletes the object it points to

    What it looks like

    Shape* s = read_shape(); s->draw(); with no delete, followed by the end of the function

    Why it fails

    Only the pointer variable is destroyed. The object lives in dynamic memory until delete is called on it.

    Do this instead

    Pair every new-created object with a delete on each path that uses it, or pass ownership explicitly to a party that will delete it.

  • Common mistake

    Believing that destroying a container of pointers releases the objects it points to

    What it looks like
    A std::vector<Shape*> going out of scope after push_back(new Shape())
    Why it fails

    The container stores addresses rather than the objects themselves, so destroying it releases only the addresses.

    Do this instead

    Delete each pointed-to object explicitly before the container goes away, and make clear which part of the program owns the elements.

  • Common mistake

    Checking only the common path when looking for leaks

    What it looks like

    Testing process(5) and concluding that process has no leak

    Why it fails

    The early return taken when n is negative never reaches the delete, and the test never exercised it.

    Do this instead

    Trace every branch and exit, including early returns, from the acquisition to the function's end.

  • Common mistake

    Forgetting to document the transfer of ownership at a function interface, so callers do not know they must delete the returned pointer

Spot the Leaks

MEDIUM

Read the function below. Identify every path on which the Shape allocated with new is not released, and say which line you would change to fix each one. Then decide whether the function's caller also needs to change.

Shape* make_and_check(int code) {
    Shape* s = new Shape();
    if (code == 0) {
        return nullptr;
    }
    if (code == 1) {
        return s;
    }
    delete s;
    return nullptr;
}
Hints
  • Trace each of the three return statements and ask whether the Shape has been deleted on that route
  • A returned pointer transfers ownership to the caller, so a return that hands s back is not itself a leak; the caller must then delete it
  • The nullptr return on the first path discards the only pointer to the object

Key Takeaways

  • A resource leak happens when a resource such as dynamically allocated memory is acquired but never released, leaving it reserved yet unreachable
  • A function that returns a pointer to a free-store object transfers responsibility for releasing it to the caller, and losing that pointer without a delete causes a leak
  • A container of pointers stores addresses rather than objects, so destroying the container does not release the objects; the owner must delete each one explicitly
  • Leaks often hide on a single execution path, such as an early return, so trace every path from acquisition to exit rather than only the path you tested
  • Clear ownership, documented at function interfaces and enforced at every exit path, is the main defense against resource leaks

Related concepts

More concepts starting with O