UP: C17 Explained for Beginners

We’ll treat this chapter as if we’re rebuilding your understanding from the ground up, but with enough depth to master it.

This chapter is basically:

“How C++ actually works with memory and pointers, and how to not blow your foot off.”

We’ll go section by section, explaining each idea in beginner-friendly language, then show why you’d do it that way (or avoid something), with concrete code examples.


1. What this chapter is about (big picture)

The chapter covers:

  1. Dynamic memory / heap / free store

  2. RAII – a pattern that makes memory clean-up automatic

  3. Rule of Three / Rule of Five – how to correctly copy/move classes that own pointers

  4. Smart pointersunique_ptr, shared_ptr, weak_ptr

  5. Extra pointer skills – comparing pointers, pointer arithmetic, void*, and complex pointer/reference/const types

If you understand all of those, you’ve basically “unlocked” the hardest mental part of C++ fundamentals.


2. Dynamic memory: stack vs heap (free store)

2.1 Two main places your data lives

When your C++ program runs, variables live in different places. The important ones:

  • Stack (automatic storage): local variables inside functions. Created and destroyed automatically.

  • Heap / free store: dynamic memory you ask for manually (new / delete or via smart pointers).

Beginner mental model:

  • Stack = like a stack of plates – every time you call a function, you add plates (locals). When the function returns, those plates are automatically taken away.

  • Heap = a big warehouse of boxes – you must ask for boxes and return them yourself.

2.2 Why do we need heap?

Example: you want an array whose size comes from the user:

int main() {
    int n;
    std::cout << "How many numbers? ";
    std::cin >> n;

    int arr[1000];      // fixed size, can't use n here (not standard C++)
    // int arr[n];      // variable length arrays are not standard C++
}

You don’t know n at compile time, so a normal stack array won’t work.

Instead:

int main() {
    int n;
    std::cout << "How many numbers? ";
    std::cin >> n;

    int* arr = new int[n];  // allocate n ints on the heap (free store)

    // ... use arr[0] ... arr[n-1] ...

    delete[] arr;           // free the memory when done
}

This is dynamic memory: allocated and deallocated manually at runtime.

2.3 Two classic problems: leaks and corruption

If you do new and forget delete:

int* arr = new int[100];
// ... forgot delete[] arr;
  • You lose the only pointer to that memory.

  • The OS cannot reuse it while your program runs.

  • That’s a memory leak. Enough leaks → your program eats more and more RAM.

If you do delete incorrectly:

int* arr = new int[100];
delete arr;       // should be delete[] arr
// or:
int* p = new int(5);
delete p;
delete p;         // double delete

You can get heap corruption: the allocator’s internal bookkeeping gets messed up. That leads to random crashes later.

So:
Raw new/delete = powerful but easy to misuse. That’s why the chapter spends a lot of time on RAII and smart pointers.


3. RAII – making memory management automatic

RAII = Resource Acquisition Is Initialization.

Plain English:
“Wrap the resource (like heap memory) in an object whose constructor grabs it and whose destructor releases it.”

Key facts:

  • For RAII to work, the RAII object must be created on the stack.

  • When that RAII object goes out of scope, its destructor automatically runs and cleans up the resource.

  • RAII can be used for memory, files, sockets, etc.

3.1 Simple RAII example (manual dynamic array wrapper)

Let’s say we wrap an int* in a class:

class IntArray {
public:
    explicit IntArray(int size)
        : size_(size), data_(new int[size]) {}   // constructor allocates

    ~IntArray() {                                // destructor frees
        delete[] data_;
    }

    int& operator[](int i) { return data_[i]; }  // array-style access
    int size() const { return size_; }

private:
    int  size_;
    int* data_;
};

Usage:

int main() {
    IntArray arr(5);      // constructor: allocates

    for (int i = 0; i < arr.size(); ++i) {
        arr[i] = i * 10;
    }

    // no delete[] needed – destructor called automatically here
}

Why this is better:

  • You cannot forget delete[] because it’s attached to the object’s lifetime.

  • This is exactly how the HeapArray example in the chapter starts.

Important: If you allocate the RAII object itself on the heap (with new), its destructor only runs when you delete that object. For fully automatic clean-up, put the RAII object on the stack, not the heap.


4. Rule of Three: copying classes that own pointers

Once your class owns heap memory, another danger appears: copying.

4.1 The problem: default copy is shallow

Suppose:

class MyContainer {
private:
    int* elements = nullptr;
    int  size = 10;

public:
    MyContainer() {
        elements = new int[size];
    }

    ~MyContainer() {
        delete[] elements;
    }
};

If you don’t define a copy constructor / copy assignment operator, the compiler generates default ones that just copy each member bit-for-bit.

So:

MyContainer a;
MyContainer b = a;      // calls default copy constructor (shallow)

Internally:

  • a.elements points to some heap array.

  • b.elements becomes the same pointer.

Now both a and b think they own that same memory:

  • First destructor runs → delete[] elements → OK.

  • Second destructor runs → delete[] elements again → double delete → memory corruption.

This is called a shallow copy: only pointers are copied, not the underlying data.

4.2 Deep copy: what we actually want

We need to define:

  • Destructor (to free memory)

  • Copy constructor

  • Copy assignment operator

This is the Rule of Three: if you need any one of those, you should define all three.

A correct implementation looks like this (from the chapter’s MyContainer class, simplified):

class MyContainer {
private:
    int* elements = nullptr;
    int  size = 10;

public:
    // constructor
    MyContainer() {
        elements = new int[size];
    }

    // destructor
    ~MyContainer() {
        delete[] elements;
    }

    // copy constructor (deep copy)
    MyContainer(const MyContainer& tocopy) {
        size     = tocopy.size;
        elements = new int[size];           // allocate new array
        for (int i = 0; i < size; ++i) {
            elements[i] = tocopy.elements[i];   // copy data
        }
    }

    // copy assignment (deep copy with temp pointer for safety)
    MyContainer& operator=(const MyContainer& tocopy) {
        auto temp = new int[size];                // allocate new
        for (int i = 0; i < size; ++i) {
            temp[i] = tocopy.elements[i];         // copy data
        }
        delete[] elements;                        // free old memory
        elements = temp;                          // point to new
        return *this;                             // allow a = b = c;
    }
};

Key insights:

  • Copy constructor: create a new array, copy all elements → deep copy.

  • Copy assignment: allocate new array, copy into temp, delete old, then swap. Using a temporary adds exception safety: if copying throws, original object stays unchanged.

Beginner summary:

  • Shallow copy: “both objects share the same backpack”.

  • Deep copy: “each object gets its own backpack with a copy of the items”.


5. Rule of Five and move semantics

Copying big arrays is expensive. C++11 introduced move semantics so you can transfer ownership of data, instead of copying.

This extends the Rule of Three into the Rule of Five:
If you define a destructor, you usually also need:

  • Copy constructor

  • Copy assignment operator

  • Move constructor

  • Move assignment operator

5.1 What is an rvalue reference?

The move operations use rvalue references, declared with &&:

MyContainer(MyContainer&& tomove);                    // move ctor
MyContainer& operator=(MyContainer&& tomove);         // move assignment

“rvalue” basically means a temporary object, something you’re not going to use anymore (like MyContainer{} or a result of a function returning a MyContainer).

5.2 Move constructor: stealing the pointer

From the chapter’s MyContainer move constructor:

MyContainer(MyContainer&& tomove) {
    elements        = tomove.elements;   // steal pointer
    tomove.elements = nullptr;          // remove from source
}

Why set tomove.elements = nullptr?
So that when tomove is destroyed, it won’t delete[] memory it no longer owns.

This is much more efficient than deep copy, because you don’t allocate new memory or copy elements. You just pass the pointer.

5.3 Move assignment: similar idea

From the chapter:

MyContainer& operator=(MyContainer&& tomove) {
    delete[] elements;              // free current memory
    elements        = tomove.elements; // steal pointer
    tomove.elements = nullptr;      // empty source
    return *this;
}

Again, no element-by-element copy, just pointer reassignment.

5.4 Rule of Five with RAII

When you use RAII for a heap resource, you already have a destructor.
According to the Rule of Five, if you want move semantics, you should define all of:

  • Destructor

  • Copy constructor

  • Copy assignment operator

  • Move constructor

  • Move assignment operator

The HeapArray class in the chapter is a full Rule-of-Five example: it defines all of these to manage an array on the heap safely.


6. Smart pointers: safer than raw new/delete

If all you need is “allocate some memory, use it, release it when done”, writing your own RAII + Rule of Five can be a lot of work. That’s why C++11 introduced smart pointers in <memory>.

6.1 Types of smart pointers

From the chapter:

  • std::unique_ptr<T>

    • Only one pointer owns the object.

    • When it goes out of scope → memory is freed.

    • Non-copyable, but movable.

  • std::shared_ptr<T>

    • Multiple pointers share ownership.

    • Keeps a reference count; when count hits 0 → memory freed.

  • std::weak_ptr<T>

    • Observes a shared_ptr without increasing the reference count.

    • Used to avoid circular references (A owns B, B owns A, so count never reaches 0).

Most of the time you should prefer unique_ptr: it has less overhead and expresses clear ownership.

6.2 unique_ptr basic usage

Include the header:

#include <memory>

int main() {
    std::unique_ptr<int> p(new int);  // one int on the heap
    *p = 5;

    std::cout << *p << "\n";          // use like a normal pointer

    // no delete needed; p’s destructor frees the memory
}

For arrays:

std::unique_ptr<int[]> arr(new int[10]);
for (int i = 0; i < 10; ++i) {
    arr[i] = i;
}

You can’t do pointer arithmetic (arr + 1 etc.) with unique_ptr (you’d need get() for that), but you usually don’t need to.

6.3 make_unique (C++14+)

Even better: use std::make_unique instead of new.

auto p   = std::make_unique<int>();       // single int
auto arr = std::make_unique<int[]>(10);   // array of 10 ints

Advantages:

  • Cleaner syntax (no new).

  • Safer in the presence of exceptions.

Example from the chapter: a function returning a smart pointer to an array.

std::unique_ptr<int[]> create_array(unsigned int size) {
    auto arr = std::make_unique<int[]>(size);
    return arr;  // moved out, no leak
}

Then:

int main() {
    unsigned int size = 0;
    std::cout << "Please enter the size of the array: ";
    std::cin >> size;

    auto arr = create_array(size);  // owns the array

    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << ' ';
    }
}  // arr goes out of scope, memory freed automatically

6.4 When to use which

  • Use stack objects and standard containers (std::vector, std::string) first.

  • If you must use raw dynamic memory:

    • Prefer std::unique_ptr (single owner).

    • Use std::shared_ptr only when you genuinely need shared ownership.

    • Use std::weak_ptr to break cycles when using shared_ptr.


7. The HeapArray + SensorAnalysis example (putting it together)

This program shows RAII + Rule of Five + pointers working together.

7.1 What problem are they solving?

  • A “sensor” takes one reading per second.

  • User enters number of days.

  • Program needs to store one reading per second for all those days.
    → Huge array, size only known at runtime, might be too big for stack → must use heap.

They could have used std::vector<int>, but the chapter purposely doesn’t, to practice manual memory handling.

7.2 HeapArray class

From the header:

class HeapArray {
private:
    int  array_size;
    int* arr = nullptr;

public:
    // constructors
    HeapArray(const int);
    HeapArray(const HeapArray& tocopy);       // copy ctor
    HeapArray(HeapArray&& tomove);            // move ctor

    // destructor
    ~HeapArray();

    // assignment operators
    HeapArray& operator=(const HeapArray& tocopy);   // copy assignment
    HeapArray& operator=(HeapArray&& tomove);        // move assignment

    // subscript
    int& operator[](int i);

    // member functions
    int  size() const;
    int* begin();
    int* end();
};

This class:

  • Implements the Rule of Five for a dynamic array.

  • Provides operator[] so you can use data[i].

  • Provides begin() / end() so you can use STL algorithms.

The implementation shows:

  • Deep copy in copy constructor & copy assignment.

  • Move semantics: HeapArray(HeapArray&&) steals the pointer, empties the source.

  • Destructor that safely deletes arr, even when arr == nullptr.

7.3 How main() uses it

From the chapter:

int main() {
    std::cout << "The SensorAnalysis program\n\n";

    int num_days = 0;
    std::cout << "Enter the number of days you'd like to analyze: ";
    std::cin >> num_days;

    const int seconds_per_day = 86400;
    int total_seconds = num_days * seconds_per_day;

    HeapArray data(total_seconds);               // dynamic array on heap

    load_sensor_data(data);                      // fill it

    double total = std::accumulate(data.begin(), data.end(), 0);
    auto   min   = std::min_element(data.begin(), data.end());
    auto   max   = std::max_element(data.begin(), data.end());

    std::cout << "Number of sensor readings over "
              << num_days << " days: " << data.size() << '\n';
    std::cout << "Average reading: " << (total / data.size()) << '\n';
    std::cout << "Lowest reading: "  << *min << '\n';
    std::cout << "Highest reading: " << *max << "\n\n";
}

Key ideas:

  • HeapArray data(total_seconds);

    • Allocates a big dynamic array on the heap inside HeapArray.

    • When main exits, data is destroyed → Destructor frees the array automatically.

  • accumulate, min_element, max_element come from <numeric> and <algorithm> and work because begin() and end() return raw pointers, which are valid iterators for ranges.


8. “More skills with pointers”

The last part of the chapter introduces some extra pointer features.

8.1 Comparing pointers

Example:

double price = 89.99, score = 89.99;
double* p1 = &price;
double* p2 = &price;
double* s  = &score;

std::cout << (p1 == p2);    // 1 – same address
std::cout << (p1 == s);     // 0 – different address
std::cout << (*p1 == *s);   // 1 – same value
  • == on pointers compares addresses, not values.

  • To compare values, you have to dereference (*p1, *s).

8.2 Pointer arithmetic

Example:

int arr[] = {2, 4, 6, 8, 10};
int* p = arr;          // points to arr[0]

std::cout << *p << '\n';  // 2

++p;                     // now points to arr[1]
std::cout << *p << '\n';  // 4

p = p + 2;               // now points to arr[3]
std::cout << *p << '\n';  // 8

p = p - 3;               // now points to arr[0] again
std::cout << *p << '\n';  // 2

--p;                     // outside array bounds – undefined behavior
std::cout << *p << '\n';  // garbage

Rules:

  • Adding 1 to an int* moves it by sizeof(int) bytes (usually 4).

  • Pointer arithmetic only makes sense within the same array.

  • Going out of bounds is allowed at the pointer level but dereferencing it is undefined behavior.

8.3 void* pointers

void* = “pointer to unknown type”.

Example:

double pi = 3.14;
int    i  = 0;

void* ptr = &pi;   // points to a double
ptr = &i;          // now points to an int

// std::cout << *ptr;  // error – compiler doesn't know the type

You cannot dereference a void* or use pointer arithmetic on it.

To use it, cast it to a typed pointer:

int* iptr = static_cast<int*>(ptr);
std::cout << *iptr << '\n';    // OK – prints 0
``` :contentReference[oaicite:52]{index=52}  

The chapter warns that `void*` is error-prone and mainly seen in older C/C++ code. :contentReference[oaicite:53]{index=53}  

### 8.4 Complex pointer / reference / const types

You can mix pointers, references, and `const` in various ways. The chapter mentions you can create “complex compound types.” :contentReference[oaicite:54]{index=54}  

Some examples you should recognize:

```cpp
int i = 42;

int*       p  = &i;   // pointer to int
int*&      rp = p;    // reference to pointer to int
const int* pc = &i;   // pointer to const int (you can’t change i via pc)
int* const cp = &i;   // const pointer to int (pointer can't change)
const int* const cpc = &i; // const pointer to const int

A good trick: read from right to left:

  • const int* → pointer to const int

  • int* const → const pointer to int


9. How to actually master this chapter

Over the next few days, you can build mastery by:

  1. Rewriting the chapter’s examples yourself:

    • Implement a tiny RAII class for a dynamic double array (constructor, destructor, operator[], size()).

    • Add copy constructor & copy assignment (Rule of Three).

    • Then add move constructor & move assignment (Rule of Five).

  2. Translate raw pointers → smart pointers:

    • Take a sample program that uses new/delete directly and rewrite it to use std::unique_ptr.

  3. Practice pointer reasoning:

    • For small snippets, step through: “What address does this pointer hold now?” and “Who owns this memory?”

I’ll skip the chapter’s explicit exercises like you asked, but we can create custom practice if you want.


10. Quick understanding check (so I know what to focus on next)

Answer these in your own words (short answers are fine):

  1. Dynamic memory & RAII

    • What is the difference between memory on the stack and memory on the heap?

    • In RAII, what do the constructor and destructor each do for heap memory?

  2. Rule of Three / Rule of Five

    • Why is the default copy constructor dangerous in a class that has int* data pointing to heap memory?

    • In a move constructor, why do we usually set the source pointer to nullptr after stealing it?

  3. Smart pointers

    • When would you use std::unique_ptr<T> instead of a raw T*?

    • What is the main difference in behavior between unique_ptr and shared_ptr?

  4. Pointer basics

    • What does p + 1 mean when p is an int* pointing into an array?

    • Why can’t you write *ptr when ptr is a void*?

Your answers will reveal what parts to zoom in on more in the next chapter or if we should spend another round drilling this one.