Any value produced by a program takes up space in memory. The language carves the program's address space up into several divisions, each with different rules for how long its objects can live, and who is responsible for cleaning them up. The two areas that are most important for everyday variables are the stack and the heap.
An object that resides on the stack is created automatically when a given scope is entered and destroyed automatically when the scope is exited. The most commonplace stack objects are non pointer local variables.
#include <iostream>
int main()
{
int value { 42 };
std::cout << value << '\n';
return 0;
}
When main is entered, storage for the int value is reserved on the stack, and the value 42 is stored there. The value is printed by a read of this storage. When main is exited, the storage is automatically deallocated; the int is destroyed, and the program no longer owns that location in memory. The programmer does not need to make a decision for when the object ceases to exist, the language decides that for them.
This same lifetime rule holds for every nested scope and every function call. Every new scope pushes its local objects onto the stack; they are popped off when the scope is exited. Because the stack grows and shrinks in a strict last in, first out manner, the address assigned to a local variable only lives so long as the object is guaranteed to reside in memory.
Not every object has a short lifetime; some objects need to live beyond the scope they are created in. In these cases, the object needs to reside on the heap. An object is placed on the heap by use of the new expression, and removed with delete when it is no longer needed.
#include <iostream>
int main()
{
int* ptr { new int{ 99 } };
std::cout << *ptr << '\n';
// Release the dynamically allocated object
delete ptr;
return 0;
}
The expression new int{ 99 } asks for memory on the heap to allocate one int, stores 99 in it, and returns a pointer to it. This pointer is stored in ptr. After the value is printed, the statement delete ptr reclaims the memory, handing it back over to the heap where it may be reused.
If this delete were never called, then even though ptr may go out of scope, that memory location is still owned by the program and cannot be used until the program terminates. That kind of leaked memory may well become a problem if there are too many other such allocations.
Because the programmer, and not the language, is responsible for a heap object's lifetime, it is quite possible to have the pointer outlive the memory that it points to or vice versa. The first case, for example, creates a pointer which is technically valid but points to memory which is not owned by the program; the second creates an object on the heap, and then destroys it while retaining a pointer to the location. The first situation of such program error leads to undefined behavior, as does the latter.
#include <iostream>
int main()
{
int* ptr { new int{ 7 } };
delete ptr;
// std::cout << *ptr << '\n'; // Undefined behavior
return 0;
}
After delete ptr, the integer no longer exists. But the pointer ptr still stores the numeric address that was assigned to it. A read or a write to this address results in undefined behavior. Forcing ptr to null after the delete eliminates this problem, and thus eliminates any possible error in accessing the memory afterward.
#include <iostream>
int main()
{
// Allocate this object dynamically
int* ptr { new int{ 7 } };
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
return 0;
}
With ptr at null, any later access of it fails safely because null is invalid as an address. If ptr has not already gone out of scope, it now points to nowhere useful, which may save the programmer the trouble later.
The two memory divisions are thus drastically different: heap memory belongs to the programmer until it is released, stack memory is allocated and deallocated entirely under program control according to scope. Allocation on the stack is generally very fast because only one stack pointer needs to be manipulated, whereas heap allocation takes longer as it involves seeking appropriate memory locations. Stack memory is limited and can run out, which can crash the program, whereas the heap is generally much larger, but may fragment over time.
A program requiring only transient values to live for short periods of time on the stack is a simple situation that lets the language do the memory management. Applications where objects must persist beyond their containing scope use the heap to achieve longevity for the program. However, it means the programmer must consciously perform a matched pair of operations for every allocation.
Exercise: Stack Memory Versus Heap MemoryStarter code, hints and solutionOpenClose
Practice: Stack Memory Versus Heap Memory
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Add an automatic int local initialized to 3. Print it and the dynamic int before delete, then print local after delete to compare their lifetimes.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
int main()
{
// Allocate this object dynamically
int* ptr { new int{ 7 } };
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
return 0;
}Hints
Separate the automatic pointer variable ptr from the int created by new int{7}. They are two objects with different lifetime management.
delete releases the dynamic int, while ptr itself stays in scope until main ends. After it is set to nullptr, the guarded output is skipped.
Worked solution
The solution adds an automatic int named local and prints it alongside the dynamically allocated int before release. Deleting the dynamic int does not affect local, which remains alive until main exits.
#include <iostream>
int main()
{
// Allocate this object dynamically
int* ptr { new int{ 7 } };
int local{3};
std::cout << "Automatic: " << local << ", dynamic: " << *ptr << '\n';
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
std::cout << "Automatic still alive: " << local << '\n';
return 0;
}
Example output
Automatic: 3, dynamic: 7
Automatic still alive: 3
0 comments
No comments yet. Be the first.