Object-Oriented Programming with C++

Chapter 4
Object Lifetime and Resource Management

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Object Lifetime and Resource Management

Introduction

In a mechatronic system, software objects often represent sensors, actuators, communication channels, files, buffers, threads, and mathematical models. These objects consume resources whose correct management is essential. A memory leak may eventually stop a controller; a dangling pointer may produce an intermittent actuator command; an incorrect copy operation may duplicate ownership of a hardware handle; and an exception during initialization may leave a device in an unsafe state.

C++ provides precise control over object lifetime and resource ownership. This control is powerful, but it places design responsibilities on the programmer. This chapter develops the concepts of storage duration, scope, stack and dynamic allocation, resource acquisition is initialization (RAII), copy and move semantics, assignment, smart pointers, and the rules of zero, three, and five.

Learning Objectives

By the end of this chapter, a student should be able to:

  • distinguish scope, lifetime, and storage duration;
  • explain automatic, static, thread, and dynamic storage duration;
  • analyze stack and heap allocation and their limitations;
  • apply RAII to memory and non-memory resources;
  • distinguish copying from moving and implement safe special member functions;
  • select suitable smart-pointer types;
  • apply the rule of zero, rule of three, and rule of five;
  • identify leaks, dangling references, double deletion, and ownership errors;
  • design resource-safe components for mechatronic applications.

1. Scope, Lifetime, and Storage Duration

These terms are related but not interchangeable.

Scope is the region of source code in which a name can be used. A local variable declared inside a function has block scope. A class member is accessible according to class access rules, and a namespace-level name may have namespace scope.

Lifetime is the interval during program execution during which an object exists. It begins after initialization is complete and ends when destruction is complete.

Storage duration describes how long the storage occupied by an object is reserved. C++ commonly distinguishes four categories:

Storage durationTypical exampleBeginsEnds
AutomaticLocal variableOn entering its block, subject to initializationOn leaving its block
StaticGlobal or static objectBefore program use, with initialization rulesProgram termination
Threadthread_local objectFor each threadThread termination
DynamicObject created by newSuccessful allocation and constructiondelete, a smart pointer, or another owner

An object’s scope can end before its lifetime. For example, an object may be created dynamically through a local pointer; the pointer’s scope ends when the function returns, but the dynamically allocated object remains alive if another owner exists. Conversely, storage may be reserved without a usable object being alive in it; object lifetime rules determine when an object has actually begun and ended.

Block Scope and Destruction Order

Automatic objects are destroyed in the reverse order of completed construction. This supports safe dependency management. If object B depends on object A, declaring A before B generally causes B to be destroyed first.

void controlCycle() {
    Logger logger;
    MotorDriver motor(logger);
    // motor is destroyed first; logger is destroyed second
}

The principle applies to data members as well: members are destroyed in reverse order of their declaration, not the order in a constructor’s initializer list. Therefore, members should normally be declared in dependency order.

2. Automatic and Dynamic Allocation

An automatic object is usually implemented in a function-call stack, although the C++ standard specifies behavior rather than requiring a particular physical stack. It has predictable destruction and low management overhead.

void sample() {
    SensorReading reading{};       // automatic lifetime
    std::array<int, 16> samples{}; // automatic storage
}

Dynamic allocation obtains storage during execution. The expression new T(arguments) allocates storage and constructs a T; delete destroys the object and releases its storage.

int* p = new int(42);
delete p;
p = nullptr;

Raw new and delete are sometimes necessary at low-level interfaces, but they are error-prone. Every successful allocation must have a matching release, and the release must occur exactly once. Exceptions and multiple return paths make manual matching difficult.

Dynamic allocation is appropriate when:

  • an object must outlive the block that created it;
  • its size is determined at run time;
  • polymorphic objects are created through a base-class interface;
  • a resource has a naturally shared or separately controlled lifetime.

It is not automatically better than automatic storage. Dynamic allocation can add latency, fragmentation, synchronization overhead, and failure modes. In real-time control, deterministic allocation policies are often preferred: fixed-size buffers, object pools, or preallocation during system startup.

Common Errors

A memory leak occurs when allocated storage becomes unreachable without being released. A dangling pointer refers to an object whose lifetime has ended. A double deletion releases the same allocation twice. A use-after-free accesses an object after destruction. These are undefined behavior and can corrupt unrelated parts of a system.

int* bad() {
    int value = 10;
    return &value; // dangling pointer after return
}

The safe alternative is to return by value, return a reference only when the referred object outlives the caller, or return an owning smart pointer when dynamic lifetime is required.

3. RAII: Resource Acquisition Is Initialization

RAII binds resource ownership to an object’s lifetime. Acquisition occurs during construction; release occurs during destruction. Since destructors run automatically when control leaves a scope—including during exception unwinding—RAII makes cleanup reliable.

RAII applies to:

  • dynamic memory;
  • file descriptors and files;
  • serial ports and network sockets;
  • mutex locks;
  • hardware handles;
  • transactions and temporary device states.

The standard library illustrates the pattern:

{
    std::lock_guard<std::mutex> lock(mutex);
    // protected operation
} // lock is released automatically

A useful RAII wrapper has a clear invariant: either it owns a valid resource or it owns no resource. Its destructor releases the resource and should normally not throw exceptions.

class FileHandle {
public:
    explicit FileHandle(const char* name) : file_(std::fopen(name, "r")) {}

    ~FileHandle() {
        if (file_) std::fclose(file_);
    }

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    bool valid() const noexcept { return file_ != nullptr; }

private:
    std::FILE* file_{};
};

This class has unique ownership: copying is disabled because two objects must not close the same file handle. A production wrapper should also define or report an acquisition failure policy, such as throwing an exception or exposing an error state.

4. Copy Semantics

Copy construction creates a new object from an existing object. Copy assignment replaces the state of an existing object with a copy of another object.

Motor a;
Motor b = a; // copy construction
Motor c;
c = a;       // copy assignment

For value-like classes, copying should produce independent, equivalent values. For a class containing a raw owning pointer, the compiler-generated copy operations perform shallow copying: both objects point to the same resource. That often causes double deletion or unintended shared mutation.

A deep copy allocates a separate resource and copies its contents. A shallow copy copies an address or handle without creating an independent resource. Neither is universally correct; the ownership model must decide.

Copy assignment must also handle self-assignment safely and must preserve class invariants if copying fails. The copy-and-swap idiom is one possible strategy:

Buffer& Buffer::operator=(Buffer other) {
    swap(*this, other);
    return *this;
}

The parameter is copied first. The swap then gives the target the copied state, while the temporary releases the old state when it is destroyed. This can provide strong exception safety if copying and swapping are correctly implemented.

5. Move Semantics

Move construction transfers resources from a source object rather than duplicating them. Move assignment does the same for an already existing target. They normally accept an rvalue reference, written T&&.

std::vector<int> makeSamples() {
    std::vector<int> result(1000);
    return result; // move or elision; no expensive logical copy is required
}

After a move, the source object remains valid but its value is generally unspecified. It may be assigned to, destroyed, or reset, but application code should not assume it retains its former contents.

A movable resource-owning type commonly transfers a pointer and sets the source pointer to nullptr:

class Block {
public:
    explicit Block(std::size_t n) : data_(new int[n]), size_(n) {}
    ~Block() { delete[] data_; }

    Block(const Block&) = delete;
    Block& operator=(const Block&) = delete;

    Block(Block&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Block& operator=(Block&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    int* data_{};
    std::size_t size_{};
};

The noexcept specification is important for many standard containers. A vector may prefer moving elements during reallocation only when moving is known not to throw; otherwise it may copy them to preserve exception guarantees.

6. The Rule of Zero, Three, and Five

The rule of zero is the preferred design: if a class’s members manage resources through RAII types such as std::vector, std::string, and std::unique_ptr, the class should usually declare none of the destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator.

class Controller {
private:
    std::vector<double> gains_;
    std::unique_ptr<Logger> logger_;
};

The member types already provide correct lifetime behavior.

The rule of three states that if a class needs to declare any one of the following, it probably needs all three:

  1. destructor;
  2. copy constructor;
  3. copy assignment operator.

This is a warning sign that the class directly manages a resource requiring special copying.

The rule of five extends the rule of three by adding:

  1. move constructor;
  2. move assignment operator.

A class that manually owns a resource and supports efficient transfers should consider all five. However, declaring a destructor can suppress implicit move operations, so move support may need to be explicitly defaulted or defined.

These are design guidelines, not a requirement to implement five functions mechanically. If copying is conceptually invalid, explicitly delete copy operations and implement only the valid ownership operations.

7. Smart Pointers and Ownership Models

Smart pointers are objects that express ownership and perform automatic cleanup.

TypeOwnership modelTypical use
std::unique_ptr<T>ExclusiveOne owner of a dynamically allocated object
std::shared_ptr<T>Shared, reference-countedMultiple independent owners
std::weak_ptr<T>Non-owning observationBreaking cycles or observing shared objects

std::unique_ptr

Use unique_ptr by default when dynamic allocation is necessary and there is one owner.

auto driver = std::make_unique<MotorDriver>();
std::unique_ptr<MotorDriver> backup = std::move(driver);

After the move, driver is empty and backup owns the driver. A unique_ptr can be passed by reference for observation, moved to transfer ownership, or replaced by a raw pointer/reference for a non-owning operation.

std::shared_ptr

shared_ptr uses a control block that typically stores a reference count and deletion information. The managed object is destroyed when the last owning shared_ptr is destroyed or reset.

auto model = std::make_shared<CalibrationModel>();
process(model); // another owner may be created

Reference counting does not make an object automatically thread-safe, and shared ownership can obscure responsibility and prolong lifetime. Prefer unique_ptr unless shared ownership is genuinely required.

std::weak_ptr

A weak_ptr does not keep the object alive. To use the object safely, call lock() and test the resulting shared_ptr:

if (auto current = weakModel.lock()) {
    current->update();
}

It is especially useful for observer relationships and for breaking cycles such as parent–child graphs where a child observes its parent.

Custom Deleters

Some resources are not released with delete. A smart pointer can store a custom deleter:

using FilePtr = std::unique_ptr<std::FILE, int(*)(std::FILE*)>;
FilePtr file(std::fopen("data.bin", "rb"), std::fclose);

The deleter must be compatible with the resource and must not release it more than once.

8. Assignment and Exception Safety

Assignment differs from initialization: the target already owns a state that must be released or replaced. A sound assignment operation maintains class invariants even if an operation fails.

The main exception-safety guarantees are:

  • No-throw guarantee: the operation cannot fail by throwing;
  • Strong guarantee: failure leaves the program state unchanged;
  • Basic guarantee: invariants hold and no resources leak, but state may change;
  • No guarantee: generally unacceptable for resource-management code.

Move operations are often marked noexcept when they only transfer ownership and cannot fail. Destructors should not throw; an exception escaping a destructor during stack unwinding can terminate the program.

9. Applied Case: A Robotic Gripper Controller

Background

Consider an illustrative robotic gripper controller. It communicates with a motor driver through a file descriptor and stores calibration data. The controller is created during system initialization and may be replaced when a new tool head is attached.

Problem

An initial implementation stores a raw descriptor and a raw pointer to calibration data. Its default copy operations duplicate the descriptor and pointer. When a temporary controller is copied and destroyed, the live controller may use a closed descriptor or freed calibration block.

Analysis

The descriptor is an exclusive resource. Calibration data is value-like and should be copied independently, or managed by a standard container. The controller should have one clear owner of the descriptor, deterministic release, and safe replacement semantics.

Approach and Solution

A robust design uses an RAII descriptor wrapper and std::vector<double> for calibration values:

class GripperController {
public:
    GripperController(DeviceHandle handle,
                      std::vector<double> calibration)
        : handle_(handle), calibration_(std::move(calibration)) {}

    GripperController(const GripperController&) = delete;
    GripperController& operator=(const GripperController&) = delete;
    GripperController(GripperController&&) noexcept = default;
    GripperController& operator=(GripperController&&) noexcept = default;

private:
    DeviceHandle handle_;              // RAII exclusive owner
    std::vector<double> calibration_;   // value semantics
};

The class cannot be accidentally copied, can be transferred efficiently, and delegates cleanup to member types. If multiple software components must access the same controller, shared ownership should be introduced only after defining synchronization and shutdown policies.

Outcome

The design removes manual cleanup from normal control flow, prevents double release, clarifies ownership, and makes replacement of a controller explicit through move assignment.

Learning Points

  1. Hardware handles are resources even though they are not memory.
  2. RAII makes cleanup occur on normal return and exception paths.
  3. Ownership policy should be visible in the type system.
  4. Rule-of-zero design is usually safer than hand-written special members.
  5. Real-time systems should also assess allocation timing and fragmentation.

10. Best Practices and Limitations

Use automatic objects and standard containers whenever possible. Prefer make_unique and make_shared over explicit new. Do not return pointers or references to local objects. Avoid owning raw pointers; if a raw pointer is used, document that it is non-owning. Do not create multiple owning smart pointers from the same raw pointer. Use weak_ptr for non-owning observation of shared objects. Keep ownership graphs simple, and test destruction, replacement, exception paths, and moved-from states.

Resource safety does not solve every systems problem. A correctly managed object may still control a device incorrectly. Shared ownership may introduce nondeterministic destruction, and dynamic allocation may be unacceptable in hard real-time loops. Safety-critical applications require additional standards, code review, static analysis, testing, and explicit fault-handling policies. In mechatronics, software must also place hardware in a safe state when a resource or communication channel fails.

Chapter Summary

Object scope identifies where a name is usable, while lifetime identifies when an object exists. Storage duration may be automatic, static, thread, or dynamic. Automatic storage provides convenient deterministic destruction; dynamic storage supports run-time lifetime and polymorphism but increases management risks.

RAII connects resource ownership to object lifetime and is the central C++ technique for reliable cleanup. Copying should be used for independent value semantics or deliberately defined shared semantics. Moving transfers resources efficiently while leaving the source valid but unspecified. The rule of zero favors resource-managing member types; the rules of three and five guide classes that directly manage resources.

unique_ptr expresses exclusive ownership, shared_ptr shared ownership, and weak_ptr non-owning observation. Safe assignment, non-throwing destructors, explicit ownership, and attention to real-time constraints are essential in professional mechatronic software.

Key Terms

Scope: Region of source code where a name can be used.

Lifetime: Period during which an object exists.

Storage duration: Classification of how long an object’s storage is reserved.

Automatic storage: Storage associated with block execution and automatic destruction.

Dynamic storage: Storage obtained during execution, commonly through allocation.

RAII: Design technique that acquires a resource in construction and releases it in destruction.

Resource: An entity requiring controlled acquisition and release.

Ownership: Responsibility for managing a resource’s lifetime.

Dangling pointer: Pointer referring to an object whose lifetime has ended.

Memory leak: Allocated storage that is no longer reachable and has not been released.

Deep copy: Copy creating an independent resource or resource state.

Shallow copy: Copying an address or handle without independent ownership.

Move construction: Construction by transferring resources from another object.

Move assignment: Replacement of a target’s state by transferred resources.

Rule of zero: Prefer declaring no special member functions by using RAII members.

Rule of three: Guidance to define destructor, copy constructor, and copy assignment together.

Rule of five: Extension of the rule of three to include move constructor and move assignment.

unique_ptr: Smart pointer expressing exclusive ownership.

shared_ptr: Smart pointer supporting reference-counted shared ownership.

weak_ptr: Non-owning observer associated with a shared_ptr control block.

Review Questions

5 Short Answer Questions

  1. Distinguish scope from lifetime.
  2. Name the four principal C++ storage-duration categories.
  3. What problem does RAII solve?
  4. What is the state of a moved-from object expected to be?
  5. Why is unique_ptr generally preferred over shared_ptr when ownership is exclusive?

5 Long Answer Questions

  1. Explain automatic and dynamic storage, including their advantages and risks in a mechatronic controller.
  2. Describe shallow copying, deep copying, and the double-deletion problem.
  3. Explain the rule of zero, rule of three, and rule of five, giving a suitable use case for each.
  4. Compare unique_ptr, shared_ptr, and weak_ptr in terms of ownership and application.
  5. Explain how exception safety influences assignment operators and destructors.

3 Analytical / Application Questions

  1. A class contains a raw pointer to a motor-driver handle and uses the compiler-generated copy constructor. Analyze the possible failures and redesign the ownership model.
  2. A real-time control loop allocates and deletes a sensor buffer on every cycle. Evaluate the risks and propose two safer alternatives.
  3. Design the ownership relationships for a robot model containing a controller, several sensors, and observer objects. Justify where exclusive ownership, shared ownership, and non-owning observation should be used.

Multiple Choice Questions with Answers

Question 1

Which statement best distinguishes scope from lifetime?

A. Scope describes where a name is usable; lifetime describes when the object exists.

B. Scope describes memory size; lifetime describes execution speed.

C. Scope and lifetime are always identical.

D. Scope applies only to dynamic objects.

Correct Answer: A. Scope describes where a name is usable; lifetime describes when the object exists.

Question 2

Which technique binds resource release to object destruction?

A. Template specialization

B. RAII

C. Function overloading

D. Namespace qualification

Correct Answer: B. RAII

Question 3

What is the usual ownership meaning of std::unique_ptr<T>?

A. Non-owning observation

B. Reference-counted shared ownership

C. Exclusive ownership

D. Temporary stack ownership

Correct Answer: C. Exclusive ownership

Question 4

What should a moved-from object generally be assumed to be?

A. Invalid and impossible to destroy

B. Valid but with an unspecified value

C. Automatically copied back to the source

D. Guaranteed to contain its original value

Correct Answer: B. Valid but with an unspecified value

Question 5

Which set belongs to the rule of three?

A. Constructor, default constructor, and conversion operator

B. Destructor, copy constructor, and copy assignment operator

C. Move constructor, move assignment, and destructor only

D. Virtual function, friend function, and destructor

Correct Answer: B. Destructor, copy constructor, and copy assignment operator

Question 6

Why can compiler-generated copying be dangerous for a class owning a raw pointer?

A. It always prevents compilation.

B. It performs a shallow copy, potentially leaving two owners of one resource.

C. It converts the pointer into a reference.

D. It automatically creates a shared_ptr.

Correct Answer: B. It performs a shallow copy, potentially leaving two owners of one resource.

Question 7

Which pointer type is designed to observe a shared_ptr-managed object without extending its lifetime?

A. std::unique_ptr

B. std::weak_ptr

C. Raw owning pointer

D. std::array

Correct Answer: B. std::weak_ptr

Question 8

What is the preferred design when a class can rely entirely on RAII member types for resource management?

A. Declare all five special member functions manually.

B. Use the rule of zero.

C. Disable the destructor.

D. Allocate every member with raw new.

Correct Answer: B. Use the rule of zero.

Question 9

Which operation transfers ownership from an existing unique_ptr to another?

A. Copy construction

B. std::move

C. Address-of operator

D. Reference binding to a const integer

Correct Answer: B. std::move

Question 10

Why are destructors normally required not to throw?

A. Throwing destructors use no memory.

B. An exception during stack unwinding can cause program termination.

C. Destructors cannot access data members.

D. The compiler replaces them with copy constructors.

Correct Answer: B. An exception during stack unwinding can cause program termination.

Object-Oriented Programming with C++
Chapter 4 of 14