
The heap
We described the stack as a manager of variables with automatic storage duration. The word automatic suggests that programmers shouldn't care about the actual memory allocation and deallocation. Automatic storage duration could be achieved only if the size of data or a collection of the data is known beforehand. This way, the compiler is aware of the number and type of function arguments and local variables. At this point, it seems more than fine, but programs tend to work with dynamic data— data of an unknown size. We will study dynamic memory management in detail in Chapter 5, Memory Management and Smart Pointers; for now, let's look at a simplified diagram of memory segments and find out what the heap is used for:
The program uses the heap segment in order to request more memory space than has been required before. This is done at runtime, which means the memory is being allocated dynamically during the program execution. The program requests the OS for new memory space whenever required. The OS doesn't actually know whether the memory is required for an integer, or for a user-defined Point, or even for an array of user-defined Point. The program requests the memory by passing the actual size of bytes that it requires. For example, to request a space for an object of type Point, the malloc() function can be used as follows:
#include <cstdlib>
struct Point {
float x;
float y;
};
int main() {
std::malloc(sizeof(Point));
}
The malloc() function allocates a contiguous memory space of sizeof(Point) bytes— let's say 8 bytes. It then returns the address of the first byte of that memory as it is the only way to provide access to space. And the thing is, malloc() doesn't actually know whether we requested memory space for a Point object or an int, and it simply returns void*. void* stores the address of the first byte of allocated memory, but it definitely cannot be used to fetch the actual data by dereferencing the pointer simply because void does not define the size of the data. Take a look at the following diagram; it shows that malloc allocates memory on the heap:
To actually use the memory space, we need to cast the void pointer to the desired type:
void* raw = std::malloc(sizeof(Point));
Point* p = static_cast<Point*>(raw);
Or, simply declare and initialize the pointer with a cast result:
Point* p = static_cast<Point*>(std::malloc(sizeof(Point)));
C++ solves this headache by introducing the new operator, which automatically fetches the size of the memory space to be allocated and converts the result to the desired type:
Point* p = new Point;
What happens when we access the members of the Point object pointed to by p? Dereferencing p returns the full Point object, so to change the value of member x, we should do the following:
(*p).x = 0.24;
Or, better still, access it with the arrow operator:
p->x = 0.24;
We will dive into the user-defined types and structs in particular in Chapter 3, Details of Object-Oriented Programming.