CS253: Software Development with C++

Spring 2023

Dynamic Memory

Show Lecture.DynamicMemory as a slide show.

CS253 Dynamic Memory

The Dynamic Duo

The old C way

In C, we used functions to allocate & free memory:

They still work for non-objects, but don’t use them. They’re not type-safe, and they don’t call ctors & dtors (constructors and destructors).

The new C++ way

Scalar Example

int *p = new int;
*p = 42;
cout << p << ' ' << *p << '\n';
delete p;
0x104b2b0 42

This is silly. If we want only a single int, we would simply declare an int as a local variable on the stack.

Array Example

int *a = new int[10];
for (int i=0; i<10; i++)
    a[i] = i*11;

for (int i=0; i<10; i++)
    cout << a[i] << ',';

delete[] a;         // Note the []
0,11,22,33,44,55,66,77,88,99,

This would make more sense if the size were determined at run-time.

Linked List Example

struct Node {
    float value;
    Node *next;
} *head = nullptr;

for (double d = 1.1; d < 4; d += 1.1) {
    Node *n = new Node;
    n->value = d; n->next = head;
    head = n;
}

for (auto p = head; p; p = p->next)
    cout << p->value << '\n';

while (head) {
    Node *doomed = head;
    head = head->next;
    delete doomed;
}
3.3
2.2
1.1

new delete, new[] delete[]

new delete, new[] delete[]

auto *p = new double[3];
delete p;    // 🦡
auto *q = new double;
delete[] q;  // 🦡
cout << "Got away with it!\n";
Got away with it!
auto *r = new string[3];
delete r;    // 🦡
c.cc:2: warning: ‘void operator delete(void*, std::size_t)’ called on 
   pointer ‘<unknown>’ with nonzero offset 8
c.cc:1: note: returned from ‘void* operator new [](std::size_t)’
munmap_chunk(): invalid pointer
SIGABRT: Aborted
auto *s = new string;
delete[] s;  // 🦡
free(): invalid pointer
SIGABRT: Aborted

Perhaps this is because string has a destructor, to release dynamic memory, whereas double doesn’t. Don’t count on it.

How not to do it

int *p;
cout << "I don’t have much faith in this." << endl;
*p = 42;  // 🦡 Wait--where is p pointing to!?
c.cc:3: warning: ‘p’ is used uninitialized
c.cc:1: note: ‘p’ was declared here
I don’t have much faith in this.
SIGSEGV: Segmentation fault

Just because you have a pointer, doesn’t mean that it’s pointing anywhere useful. *p = 42 assumes that p points to a piece of memory that we can write to. Not so, in the code above.

Not initialized

auto *a = new short,
     *b = new short(42),   // or short{42}
     *c = new short[10],
     *d = new short[2]{11,22};
cout << a << ": ⁇⁇\n"
     << b << ": " << *b << "\n"
     << c << ": ⁇⁇\n"
     << d << ": " << d[0] << ' ' << d[1] << '\n';
0x14742b0: ⁇⁇
0x14742d0: 42
0x14742f0: ⁇⁇
0x1474310: 11 22

Java Thinking

Java programmers, remember that objects do not have to be dynamically allocated. You can, but you don’t have to.

string s = new string;  // 🦡
c.cc:1: error: conversion from ‘std::string*’ {aka 
   ‘std::__cxx11::basic_string<char>*’} to non-scalar type 
   ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’} requested

Instead, just declare the string:

string s = "Hi there\n";
cout << s;
Hi there

Sure, the string allocates dynamic memory, internally, but that’s none of your business.

Avoid all of this

In general, use standard containers such as string, vector, or list when you can. They handle the dynamic memory allocation, so you don’t have to.

Besides, code that deals with dynamic memory is easy to get wrong. Code that you write will have bugs. vector, on the other hand, had its bugs discovered & fixed long ago.

If you must use dynamic memory, then consider unique_ptr and shared_ptr.

A shortcut that will bite you

An array with a non-constant size is not allowed in C++, though some compilers with some settings (g++ without -Wpedantic) allow it as a non-portable extension.

int n = 42;
int data[n];  // 🦡
data[3] = 253;
cout << data[3] << '\n';
c.cc:2: warning: ISO C++ forbids variable length array ‘data’
253

Double delete

Every call to new must be matched (later) by exactly one delete.

Not zero, and not two. One.

Similarly, every call to new[] must be matched by exactly one delete[].

Zero delete

If you don’t call delete, then the memory is forgotten. We call this a memory leak.

Sure, the memory will be implicitly freed when the program ends. However, some programs run for a good long time before they end. This program allocates memory every second, and forgets to free it.

// Clock program
draw_clock_face();
while (sleep(1)) {
    GraphicsContext *gc = new GraphicsContext;
    gc->redraw();
    // forget to free GraphicsContext
}

Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip. Drip.

Multiple delete

What happens if you call delete more than once? That’s undefined behavior. Let’s see what it does on this computer:

float *p = new float[100];
delete[] p;
delete[] p;  // 🦡
free(): double free detected in tcache 2
SIGABRT: Aborted

Pick Up Your Own Trash

Dangling Pointers

auto laurel = new float(12.34);  // hey, no *!
cout << *laurel << '\n';         // should be 12.34
delete laurel;
cout << *laurel << '\n';         // 🦡 value is unknown

auto hardy = new float(56.78);   // will probably re-use space
cout << *laurel << '\n';         // 🦡 most likely 56.78 for laurel
delete hardy;
12.34
4.97461e-42
56.78

The second cout displayed *laurel, whose value changed!

Magical Thinking

Another way to produce a dangling pointer

Don’t return a pointer to something that will soon go away.

// Return a cheery message:
const char *message() {
    char buf[] = "Hello there, folks!\n";
    const char *p = buf;
    return p;  // 🦡
}

int main() {
    cout << "I say: " << flush << message();  // Why does this fail‽
    return 0;
}
I say: 0␈@

No Reference Counting

C++ has no garbage collection, and so has no built-in reference counting.

string *puppycorn = new string("Unikitty!");
string *dr_fox = puppycorn;  // a shallow copy
cout << *dr_fox << endl;
delete puppycorn;            // the one & only copy is gone
cout << *dr_fox << '\n';     // 🦡 a dangling pointer
Unikitty!
SIGSEGV: Segmentation fault

No Dynamic Memory

Really—C++ has no garbage collection. Just use a container, like vector or string, which handles the memory management for you. If we had garbage collection, this would run forever:

for (auto count=1ULL; ; ++count) {
    char *p = new char[1'000'000'000];
    p[0] = 'X';
    // 🦡 Oops--forgot to delete!
    if (count % 20'000 == 0)
        cout << count/1000 << "TB" << endl;
}
20TB
40TB
60TB
80TB
100TB
120TB
140TB
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
SIGABRT: Aborted