CSE332S Object-Oriented Programming in C++ (Lecture 13)
Memory layout of a C++ program, variables and their lifetimes
C++ Memory Overview
4 major memory segments
- Global: variables outside stack, heap
- Code (a.k.a. text): the compiled program
- Heap: dynamically allocated variables
- Stack: parameters, automatic and temporary variables (all the variables that are declared inside a function, managed by the compiler, so must be fixed size)
- For the dynamically allocated variables, they will be allocated in the heap segment, but the pointer (fixed size) to them will be stored in the stack segment.
Key differences from Java
- Destructors of automatic variables called when stack frame where declared pops
- No garbage collection: program must explicitly free dynamic memory
Heap and stack use varies dynamically
Code and global use is fixed
Code segment is “read-only”
int g_default_value = 1;
int main (int argc, char **argv) {
Foo *f = new Foo;
f->setValue(g_default_value);
delete f; // programmer must explicitly free dynamic memory
return 0;
}
void Foo::setValue(int v) {
this->m_value = v;
}
Memory, Lifetimes, and Scopes
Temporary variables
- Are scoped to an expression, e.g.,
a = b + 3 * c;
Automatic (stack) variables
- Are scoped to the duration of the function in which they are declared
Dynamically allocated variables
- Are scoped from explicit creation (new) to explicit destruction (delete)
Global variables
- Are scoped to the entire lifetime of the program
- Includes static class and namespace members
- May still have initialization ordering issues
Member variables
- Are scoped to the lifetime of the object within which they reside
- Depends on whether object is temporary, automatic, dynamic, or global
Lifetime of a pointer/reference can differ from the lifetime of the location to which it points/refers
Direct Dynamic Memory Allocation and Deallocation
#include <iostream>
using namespace std;
int main (int, char *[]) {
int * i = new int; // any of these can throw bad_alloc
int * j = new int(3);
int * k = new int[*j];
int * l = new int[*j];
for (int m = 0; m < *j; ++m) { // fill the array with loop
l[m] = m;
}
delete i; // call int destructor
delete j; // single destructor call
delete [] k; // call int destructor for each element
delete [] l;
return 0;
}Issues with direct memory management
A Basic Issue: Multiple Aliasing
int main (int argc, char **argv) {
Foo f;
Foo *p = &f;
Foo &r = f;
delete p;
return 0;
}Multiple aliases for same object
fis a simple alias, the object itselfpis a variable holding a pointerris a variable holding a reference
What happens when we call delete on p?
- Destroy a stack variable (may get a bus error there if we’re lucky)
- If not, we may crash in destructor of f at function exit
- Or worse, a local stack corruption that may lead to problems later
Problem: object destroyed but another alias to it was then used (dangling pointer issue)
Memory Lifetime Errors
Foo *bad() {
Foo f;
return &f; // return address of local variable, f is destroyed after function returns
}
Foo &alsoBad() {
Foo f;
return f; // return reference to local variable, f is destroyed after function returns
}
Foo mediocre() {
Foo f;
return f; // return copy of local variable, f is destroyed after function returns, danger when f is a large object
}
Foo * good() {
Foo *f = new Foo;
return f; // return pointer to local variable, with new we can return a pointer to a dynamically allocated object, but we must remember to delete it later
}
int main() {
Foo *f = &mediocre(); // f is a pointer to a temporary object, which is destroyed after function returns, f is invalid after function returns
cout << good()->value() << endl; // good() returns a pointer to a dynamically allocated object, but we did not store the pointer, so it will be lost after function returns, making it impossible to delete it later.
return 0;
}Automatic variables
- Are destroyed on function return
- But in bad, we return a pointer to a variable that no longer exists
- Reference from also_bad similar
- Like an un-initialized pointer
What if we returned a copy?
- Ok, we avoid the bad pointer, and end up with an actual object
- But we do twice the work (why?)
- And, it’s a temporary variable (more on this next)
We really want dynamic allocation here
Dynamically allocated variables
- Are not garbage collected
- But are lost if no one refers to them: called a “memory leak”
Temporary variables
- Are destroyed at end of statement
- Similar to problems w/ automatics
Can you spot 2 problems?
- One with a temporary variable
- One with dynamic allocation
Double Deletion Errors
int main (int argc, char **argv) {
Foo *f = new Foo;
delete f;
// ... do other stuff
delete f; // will throw an error because f is already deleted
return 0;
} What could be at this location?
- Another heap variable
- Could corrupt heap
Shared pointers and the RAII idiom
A safer approach using smart pointers
C++11 provides two key dynamic allocation features
shared_ptr: a reference counted pointer template to alias and manage objects allocated in dynamic memory (we’ll mostly use the shared_ptr smart pointer in this course)make_shared: a function template that dynamically allocates and value initializes an object and then returns a shared pointer to it (hiding the object’s address, for safety)
C++11 provides 2 other smart pointers as well
unique_ptr: a more complex but potentially very efficient way to transfer ownership of dynamic memory safely (implements C++11 “move semantics”)weak_ptr: gives access to a resource that is guarded by a shared_ptr without increasing reference count (can be used to prevent memory leaks due to circular references)
Resource Acquisition Is Initialization (RAII)
Also referred to as the “Guard Idiom”
- However, the term “RAII” is more widely used for C++
Relies on the fact that in C++ a stack object’s destructor is called when stack frame pops
Idea: we can use a stack object (usually a smart pointer) to hold the ownership of a heap object, or any other resource that requires explicit clean up
- Immediately initialize stack object with the allocated resource
- De-allocate resource in the stack object’s destructor
Example: Resource Acquisition Is Initialization (RAII)
shared_ptr<Foo> createAndInit() {
shared_ptr<Foo> p =
make_shared<Foo> ();
init(p);// may throw exception
return p;
}
int run () {
try {
shared_ptr<Foo> spf =
createAndInit();
cout << “*spf is ” << *spf;
} catch (...) {
return -1
}
return 0;
}RAII idiom example using shared_ptr
#include <memory>
using namespace std;shared_ptr<X>assumes and maintains ownership of aliased X- Can access the aliased X through it (*spf)
shared_ptr<X>destructor calls delete on address of owned X when it’s safe to do so (per reference counting idiom discussed next)- Combines well with other memory idioms
Reference Counting
Basic Problem
- Resource sharing is often more efficient than copying
- But it’s hard to tell when all are done using a resource
- Must avoid early deletion
- Must avoid leaks (non-deletion)
Solution Approach
- Share both the resource and a counter for references to it
- Each new reference increments the counter
- When a reference is done, it decrements the counter
- If count drops to zero, also deletes resource and counter
- “last one out shuts off the lights”
Reference Counting Example
shared_ptr<Foo> createAndInit() {
shared_ptr<Foo> p =
make_shared<Foo> ();
init(p);// may throw exception
return p;
}
int run () {
try {
shared_ptr<Foo> spf =
createAndInit();
shared_ptr<Foo> spf2 = spf;
// object destroyed after
// both spf and spf2 go away
} catch (...) {
return -1
}
return 0;
}Again starts with RAII idiom via shared_ptr
spfinitially has sole ownership of aliased Xspf.unique()would return truespf.use_countwould return 1
shared_ptr<X> copy constructor increases count, and its destructor decreases count
shared_ptr<X> destructor calls delete on the pointer to the owned X when count drops to 0