# 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" ```cpp 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; } ``` ![Image of memory layout](https://notenextra.trance-0.com/CSE332S/CPP_Function_Memory.png) ### 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 ```cpp #include 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 ```cpp int main (int argc, char **argv) { Foo f; Foo *p = &f; Foo &r = f; delete p; return 0; } ``` Multiple aliases for same object - `f` is a simple alias, the object itself - `p` is a variable holding a pointer - `r` is 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 ```cpp 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 ```cpp 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) ```cpp shared_ptr createAndInit() { shared_ptr p = make_shared (); init(p);// may throw exception return p; } int run () { try { shared_ptr spf = createAndInit(); cout << “*spf is ” << *spf; } catch (...) { return -1 } return 0; } ``` RAII idiom example using shared_ptr ```cpp #include using namespace std; ``` - `shared_ptr` assumes and maintains ownership of aliased X - Can access the aliased X through it (*spf) - `shared_ptr` 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 ```cpp shared_ptr createAndInit() { shared_ptr p = make_shared (); init(p);// may throw exception return p; } int run () { try { shared_ptr spf = createAndInit(); shared_ptr spf2 = spf; // object destroyed after // both spf and spf2 go away } catch (...) { return -1 } return 0; } ``` Again starts with RAII idiom via shared_ptr - `spf` initially has sole ownership of aliased X - `spf.unique()` would return true - `spf.use_count` would return 1 `shared_ptr` copy constructor increases count, and its destructor decreases count `shared_ptr` destructor calls delete on the pointer to the owned X when count drops to 0