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
- In C++, we use keywords to allocate & free memory:
- Single items:
- Many items:
- You have to delete the memory that you allocate.
- Yes, all your memory gets freed when the program terminates.
However, some programs run a loooong time. In CS253, it is required
that you delete your memory, to build good habits.
- Don’t delete it more than once!
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
- For each call to new, call delete.
Similarly,
new[]
& delete[]
.
- What happens if you call the wrong one? Undefined behavior.
Anything could happen.
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
- I didn’t show what the int value at
*a
happens to be—it’s
not guaranteed. Don’t you dare tell me that it will be null.
- Scalars aren’t initialized, unless you initialize them.
Objects have ctors for initialization.
- The OS might clear the memory when it’s allocated for you,
to stop you from seeing others’ data. This is not guaranteed,
and won’t happen after you delete and then re-allocate the
same chunk of memory later in your program.
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
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[]
.
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.
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
- Sure, it would be possible to implement new & delete
so that they detect a double delete, and respond with a sensible
error message.
- That would cost time, space, or probably both.
- Why should my clean code be slowed down to catch your errors?
Dangling Pointers
- After a pointer is deleted, its value (address) is, theoretically,
indeterminate. It is not automatically assigned nullptr,
though it could be.
Accessing the value invokes undefined behavior. ☠️
- In nearly all implementations, the address is simply left unchanged.
- After a pointer is deleted, the pointer is “stale” or “dangling”.
Even though you still have a pointer to the data, you’re not
allowed to access the data. It’s not yours!
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
- Students will often argue the previous point.
- “The pointer won’t point to the old location;
it’ll point nowhere ”, they patiently explain.
- They think that, somehow, the pointer no longer contains
an address—that it doesn’t contain bits that form an address.
- How would that work? It’s still 64 (say) bits,
each a 0 or a 1. It’ll make an address.
- Or, they think that the pointer will automatically become a nullptr.
- Do you think that the compiler will produce extra instructions
to clear out a pointer that you’re not suppose to use, anyway?
- Would you wash your old socks before throwing them out?
🧦
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