When you dynamically allocate memory using the new operator in C++, it triggers a complex process involving interaction between the operating system and your program.
Steps Involved:
- User Request: During program execution, a point is reached where additional memory is required. The new operator is used to specify the amount of memory to be allocated.
- System Call: Invoking the new operator results in a system call being made to the operating system. Essentially, this system call is a request to the operating system to provide a free memory block of a specified size for the program.
- Searching for Free Memory: The operating system searches for available memory blocks. Free memory is typically managed as a contiguous block, but due to allocations and deallocations during program execution, it may become fragmented. The operating system must find a free block large enough to satisfy the requested size.
- Memory Allocation and Address Return: If a suitable free block is found, the operating system allocates this portion and returns a memory address to the program. This address is the location where the program can read from and write to within the newly allocated memory.
- Updating Memory Management Tables: The operating system updates its internal memory management tables to reflect that the allocated memory is no longer free.
- Returning a Pointer: The system call returns to the program, passing the returned memory address as a pointer. The program can use this pointer to access the allocated memory.
How does the heap work in practice? The heap, a dynamically allocated memory region, is distinct from the fixed-size sections created at compile time. While the heap can grow to accommodate new allocations, the boundaries of the pre-allocated sections remain unchanged. This separation ensures that dynamic memory allocation does not interfere with the program's code or static data. The heap is also capable of shrinking.
How does the stack work in practice? Stack memory is allocated at the beginning of program execution and maintains a fixed size and cannot be arbitrarily increased. Attempting to exceed the stack's capacity results in a stack overflow.
Allocating memory with the new operator does not ensure a contiguous memory block. Several factors contribute to the ability to treat allocated memory as a contiguous block, even if it's not physically contiguous:
- Abstraction:C++ (and other high-level languages) hides the complexities of memory management. When you allocate memory using
new, you receive a logical memory address. This address represents the beginning of a contiguous block of memory from your program's perspective, regardless of how the memory is physically fragmented.
Pointer arithmetic reinforces this logical view. As you iterate through an array using a pointer, the language ensures that the pointer always points to the next element, even if the underlying physical memory is non-contiguous. - Allocators: The
newoperator typically invokes an allocator behind the scenes. Allocators are responsible for managing memory allocation and deallocation. Modern allocators are often sophisticated enough to find the largest contiguous blocks of memory possible for allocation. Even if the physical memory is fragmented, the allocator attempts to hide this fragmentation from the program.
What happens if you try to access a memory location that has not been reserved for your program? The operating system protects memory by ensuring that each program has its own designated memory space. If a program tries to access memory that it's not supposed to, the OS will usually stop the program. This is done through mechanisms like memory protection and page faults. The hardware also plays a role, with the MMU preventing programs from accessing invalid memory addresses.
A memory leak occurs when a program reserves a specific amount of memory but doesn't free it before the program ends. This is a frequent programming error that can be difficult to identify.
Upon program termination, the operating system ends the corresponding process. Consequently, all resources consumed by the program, such as memory, are supposed to be freed.
To make our jobs easier and avoid tool-related issues, let's use a common environment where everything is already set up.
Here's how to do it.
Within the .devcontainer folder, you'll find a file that describes a C++ development environment. By utilizing this file in Visual Studio Code, you can establish a connection to a Linux-based development environment. The steps involved are as follows:
- Make sure you have Docker Desktop installed and running on your computer.
- Install the Remote Development extension pack in Visual Studio Code. Once the installations are complete, follow these steps:
- Open the Command Palette (usually by pressing Ctrl+Shift+P) and search for
Dev Containers: Clone Repository in Container Volume - You may need to authorize the extension for GitHub authentication. You will be redirected to your web browser to complete this process.
- A development container will be created and started in the background. You may see a terminal window open while the container is being created.
The source file for the next task is located in the ./dynamic_memory directory.
Hint: you may find the following instructions useful for completing this task:
- compile the project:
g++ ./main.cpp ./Logger.cpp -o main- run a memory checker:
valgrind --leak-check=full ./mainAnalyze the program's architecture and behavior. Determine if there is a memory leak present.
Resolve the memory leak by deallocating the memory that is no longer being used!
Fix the leak by using smart pointers!
Resolve the memory leak by minimizing or eliminating the use of pointers.