Dynamic Memory Management and the Rule of Three, CSCE 120 Exam 3 – Study Notes
offline

Difficulty: Intermediate | Prerequisites: Part 1 (C-Style Arrays and Dynamic Arrays), basic class syntax in C++


Big Picture

Once you know how to allocate and free dynamic arrays, the next question is: what happens when objects own that memory? If a class has a pointer member pointing to heap memory, the default copy behaviour in C++ will copy the pointer but not the data it points to. This leads to double deletions, dangling pointers, and subtle bugs. The Rule of Three exists to solve exactly this problem. This topic is the bridge between raw memory manipulation and writing safe, well-behaved C++ classes. It appears heavily in CSCE 120 exam free-response questions.


TL;DR

Dynamic memory is allocated with new/new[] and freed with delete/delete[]. If your class manages dynamic memory, you must implement a destructor, a copy constructor, and a copy assignment operator (the Rule of Three) to ensure deep copies and correct cleanup. Shallow copies share the same heap memory and cause double-free bugs.


Key Terms

Dynamic memory (heap memory)

Memory explicitly requested from the operating system at runtime via new or new[]. It persists until the programmer calls delete or delete[]. Think of it as renting storage that nobody else will tidy up for you.

Memory leak

Allocated heap memory that is never freed because no pointer to it remains. The programme gradually consumes more and more memory. In simple terms, you lost the key to your rented storage, so it sits there occupied forever.

Dangling pointer

A pointer that still holds the address of memory that has been freed. Dereferencing it is undefined behaviour. Think of it as a key to a storage unit that has already been given to someone else.

Double deletion

Calling delete or delete[] on the same memory address twice. This is undefined behaviour and can corrupt the heap. In simple terms, you tried to return a storage unit you already returned.

RAII (Resource Acquisition Is Initialisation)

A design principle where a resource (memory, file handle, lock) is acquired in the constructor and released in the destructor. When the object goes out of scope, cleanup happens automatically. Think of it as tying the lifetime of the resource to the lifetime of the object.

Shallow copy

Copying only the pointer value, not the data it points to. Both the original and the copy now point to the same heap memory. In simple terms, two people holding keys to the same storage unit.

Deep copy

Allocating new memory and copying the actual contents from the source. The original and the copy each own their own independent heap memory. In simple terms, you rent a second unit and photocopy everything into it.

Rule of Three

If your class defines any one of a destructor, a copy constructor, or a copy assignment operator, it should define all three. In simple terms, if you need custom cleanup, you also need custom copying.

Destructor (~ClassName())

A special member function called automatically when an object is destroyed (goes out of scope, or delete is called on a pointer to it). Its job is to release resources. Destructors must never throw exceptions.

Copy constructor (ClassName(const ClassName& other))

Called when a new object is created as a copy of an existing one (e.g. MyClass b = a; or passing by value). It should perform a deep copy.

Copy assignment operator (ClassName& operator=(const ClassName& other))

Called when an already-existing object is assigned the value of another (e.g. a = b; where both already exist). Must handle self-assignment, free the old resource, then deep-copy the new one.

Self-assignment guard

The check if (this == &other) return *this; at the top of the copy assignment operator. Without it, the function would delete its own data before trying to copy from it.

Member initialisation list

The syntax ClassName(int n) : size(n), buffer(new int[n]) {} that initialises members before the constructor body runs. More efficient than assigning inside the body.


Core Content

Allocation and Deallocation

  • Single object: int* p = new int; allocates one integer. Free with delete p;.

  • Array: int* arr = new int[10]; allocates ten contiguous integers. Free with delete[] arr;.

  • Object: MyClass* obj = new MyClass(); allocates one object. Free with delete obj;.

The arrow operator -> is shorthand for (*p).member. So obj->size is the same as (*obj).size.

The Three Things to Avoid

Memory leaks occur when you allocate and then lose the pointer without freeing:

void leak() {
    int* p = new int[100];
    // function returns without delete[] p
}

Dangling pointers occur when you free memory but keep using the pointer:

delete[] arr;
arr[0] = 5;  // undefined behaviour

After delete, always set the pointer to nullptr:

delete[] arr;
arr = nullptr;

Double deletions occur when two pointers point to the same memory and both call delete:

int* a = new int(5);
int* b = a;        // shallow copy of pointer
delete a;
delete b;           // double deletion, undefined behaviour

RAII

The principle is simple: acquire in the constructor, release in the destructor. A class wrapping a dynamic array acquires it in the constructor and calls delete[] in the destructor. When the object goes out of scope, the destructor runs and the memory is freed automatically.

Shallow Copy vs Deep Copy

Given a class with a pointer member:

class MyArray {
public:
    int* data;
    int size;
};

A shallow copy (MyArray b = a; with the default copy) copies a.data into b.data. Both point to the same heap block. If a is destroyed and calls delete[] data, then b.data is now dangling.

A deep copy allocates fresh memory for b.data and copies the contents element by element. Now a and b are independent.

The Rule of Three: When and Why

If your class manages a resource (typically heap memory), the compiler-generated defaults will do shallow copies, which break things. You need:

  • Destructor to free the resource.

  • Copy constructor to deep-copy when a new object is created from an existing one.

  • Copy assignment operator to deep-copy when an existing object is assigned from another.

Implementing the Rule of Three

Here is the complete pattern using a MyString class as an example:

Constructor (from C-string):

MyString(const char* text) {
    if (text) {
        len = std::strlen(text);
        buffer = new char[len + 1];
        std::copy(text, text + len + 1, buffer);
    } else {
        buffer = nullptr;
        len = 0;
    }
}

Destructor:

~MyString() {
    delete[] buffer;
}

Copy constructor:

MyString(const MyString& other) : len(other.len) {
    if (other.buffer) {
        buffer = new char[len + 1];
        std::copy(other.buffer, other.buffer + len + 1, buffer);
    } else {
        buffer = nullptr;
    }
}

Copy assignment operator:

MyString& operator=(const MyString& other) {
    if (this == &other) return *this;  // self-assignment guard

    delete[] buffer;                   // free old resource

    len = other.len;
    if (other.buffer) {
        buffer = new char[len + 1];
        std::copy(other.buffer, other.buffer + len + 1, buffer);
    } else {
        buffer = nullptr;
    }

    return *this;
}

Copy Assignment Operator: The Six Steps

  1. Self-assignment check: if (this == &other) return *this;

  1. Free existing resource: delete[] buffer;

  1. Copy simple members: size = other.size;

  1. Allocate new memory: buffer = new int[size];

  1. Copy the resource (deep copy): loop and copy elements.

  1. Return *this so chained assignment (a = b = c) works.

Constructor Best Practices

Use the member initialisation list to set members before the body runs:

ClassName(int n) : size(n), buffer(new int[n]) {
    for (int i = 0; i < n; i++) buffer[i] = 0;
}

This is more efficient than assigning size and buffer inside the body, and it is the only way to initialise const or reference members.

Worked Example: Polygon Class

A Polygon holds a pointer to a dynamic array of Point structs and an integer count. The Rule of Three implementation:

  • Destructor: delete[] vertices;

  • Copy constructor: copy numVertices, allocate a new Point array if non-zero, copy each element.

  • Copy assignment: self-assignment guard, delete[] vertices, copy numVertices, allocate and copy if non-zero, return *this.

  • addVertex: allocate a new array one larger, copy existing vertices, add the new point, delete the old array, redirect, increment count.

Worked Example: foo Class With Single Heap int

class foo {
private:
    int* x;
public:
    foo(int value) { x = new int(value); }
    ~foo() { delete x; }
    foo(const foo& object) { x = new int(*object.x); }
    foo& operator=(const foo& object) {
        if (this != &object) {
            delete x;
            x = new int(*object.x);
        }
        return *this;
    }
    int getValue() { return *x; }
};

Note: single new int(value) is freed with delete x (not delete[]).


Formulas / Diagrams

No mathematical formulas. The key mental model is a stack/heap diagram: objects on the stack contain pointer members that point to blocks on the heap. A shallow copy duplicates the stack portion (including the pointer value); a deep copy duplicates both the stack portion and the heap block.


Real-World Applications

The Rule of Three is the foundation of resource management in pre-C++11 code. In modern C++ (C++11 onwards), the Rule of Five adds move semantics, and smart pointers (std::unique_ptr, std::shared_ptr) automate much of this. But understanding the manual version is essential for knowing what those tools do under the hood, and CSCE 120 tests it directly.


Common Misconceptions

  • "The default copy constructor does a deep copy." It does not. The default performs a member-wise (shallow) copy. For pointer members, this copies the address, not the data.

  • "I only need a destructor if I use new." Broadly true, but the Rule of Three says if you need the destructor, you almost certainly need the other two as well.

  • "Self-assignment can't happen in real code." It can. Consider a = a; or more subtly, arr[i] = arr[j] where i == j and the elements are objects.

  • "Returning *this from the copy assignment operator is optional." It is required for chained assignment to work correctly (a = b = c).


Why It Matters / Exam Flags

  • ⚠️ Free-response questions frequently ask you to implement the Rule of Three for a given class. Know the pattern cold.

  • ⚠️ The self-assignment guard is a common detail to forget. Exams test it explicitly.

  • ⚠️ Know the difference between when the copy constructor is called (new object from existing) versus the copy assignment operator (existing object assigned from another).

  • ⚠️ delete vs delete[] must match the allocation. Single new uses delete; array new[] uses delete[].


Quick Self-Test

  1. True or False: The default copy constructor performs a deep copy of pointer members.

  1. Fill in the blank: The first line of a copy assignment operator should check for ________.

  1. True or False: RAII means resources are released in the constructor.

  1. Fill in the blank: The Rule of Three states that if you define any one of destructor, copy constructor, or ________, you should define all three.

  1. True or False: A dangling pointer is a pointer to memory that has been freed.

Answers: 1. False (shallow copy). 2. Self-assignment (this == &other). 3. False (released in the destructor). 4. Copy assignment operator. 5. True.


Practice Q&A

Q: Given a class with int* data and int size, write the destructor.

A: ~ClassName() { delete[] data; }

Q: Why is the self-assignment check necessary in the copy assignment operator?

A: Without it, the function would delete[] its own data first, then try to copy from the (now freed) source, since this and &other point to the same object.

Q: What is printed?

MyArray a;
a.size = 3;
a.data = new int[3]{1,2,3};
MyArray b = a;   // default (shallow) copy
b.data[0] = 99;
cout << a.data[0];

A: 99. The shallow copy means a.data and b.data point to the same heap block.

Q: In the Polygon class, what does addVertex need to do with memory?

A: Allocate a new Point array one element larger, copy all existing vertices, add the new vertex at the end, delete[] the old array, redirect the pointer, and increment numVertices.

Q: A class has int* items and int size. The copy assignment operator forgets to delete[] items before allocating new memory. What problem does this cause?

A: A memory leak. The old heap block is never freed; it becomes unreachable.


Connections to Other Topics

This connects to C-style and dynamic arrays (Part 1), where you first encountered new[] and delete[]. It also connects to std::vector (Part 3), which implements RAII internally so you do not need to write the Rule of Three yourself when using it. In later courses, this foundation leads into move semantics (Rule of Five) and smart pointers.


Related Terms / Search Tags

dynamic memory, heap, stack, new, delete, new[], delete[], memory leak, dangling pointer, double deletion, use-after-free, RAII, Resource Acquisition Is Initialisation, shallow copy, deep copy, Rule of Three, destructor, copy constructor, copy assignment operator, self-assignment, member initialisation list, *this, Polygon class, MyString class, CSCE 120, Texas A&M, Exam 3