- As opposed to local variables on the stack, memory can now be allocated in an arbitrary scope (e.g. inside a function) without it being deleted when the scope is left. Thus, as long as the address to an allocated block of memory is returned by a function, the caller can freely use it.
- Local variables on the stack are allocated at compile-time. Thus, the size of e.g. a string variable might not be appropriate as the length of the string will not be known until the program is executed and the user inputs it. With local variables, a solution would be to allocate a long-enough array of and hope that the actual length does not exceed the buffer size. With dynamically allocated heap memory, variables are allocated at run-time. This means that the size of the above-mentioned string variable can be tailored to the actual length of the user input.
- Heap memory is only constrained by the size of the address space and by the available memory. With modern 64 bit operating systems and large RAM memory and hard disks the programmer commands a vast amount of memory. However, if the programmer forgets to deallocate a block of heap memory, it will remain unused until the program is terminated. This is called a "memory leak".
- Unlike the stack, the heap is shared among multiple threads, which means that memory management for the heap needs to take concurrency into account as several threads might compete for the same memory resource.
- When memory is allocated or deallocated on the stack, the stack pointer is simply shifted upwards or downwards. Due to the sequential structure of stack memory management, stack memory can be managed (by the operating system) easily and securely. With heap memory, allocation and deallocation can occur arbitrarily, depending on the lifetime of the variables. This can result in fragmented memory over time, which is much more difficult and expensive to manage.
A classic symptom of memory fragmentation is that you try to allocate a large block and you can’t, even though you appear to have enough memory free. On systems with virtual memory however, this is less of a problem, because large allocations only need to be contiguous in virtual address space, not in physical address space.
When memory is heavily fragmented however, memory allocations will likely take longer because the memory allocator has to do more work to find a suitable space for the new object.
To allocate dynamic memory on the heap means to make a contiguous memory area accessible to the program at runtime and to mark this memory as occupied so that no one else can write there by mistake.
To reserve memory on the heap, one of the two functions malloc
(stands for Memory Allocation) or calloc
(stands for Cleared Memory Allocation) is used. The header file stdlib.h
or malloc.h
must be included to use the functions.
Here is the syntax of malloc
and calloc
in C/C++:
pointer_name = (cast-type*) malloc(size);
pointer_name = (cast-type*) calloc(num_elems, size_elem);
malloc
is used to dynamically allocate a single large block of memory with the specified size. It returns a pointer of type void
which can be cast into a pointer of any form.
calloc
is used to dynamically allocate the specified number of blocks of memory of the specified type. It initializes each block with a default value '0'.
Both functions return a pointer of type void
which can be cast into a pointer of any form. If the space for the allocation is insufficient, a NULL pointer is returned.
int *p = (int*)malloc(sizeof(int));
At compile time, only the space for the pointer is reserved (on the stack). When the pointer is initialized, a block of memory of sizeof(int)
bytes is allocated (on the heap) at program runtime. The pointer on the stack then points to this memory location on the heap.
Since arrays and pointers are displayed and processed identically internally, individual blocks of data can also be accessed using array syntax:
int *p = (int*)malloc(3*sizeof(int));
p[0] = 1; p[1] = 2; p[2] = 3;
printf("address=%p, second value=%d\n", p, p[1]);
struct MyStruct {
int i;
double d;
char a[5];
};
MyStruct *p = (MyStruct*)calloc(4,sizeof(MyStruct));
p[0].i = 1; p[0].d = 3.14159; p[0].a[0] = 'a';
The size of the memory area reserved with malloc
or calloc
can be increased or decreased with the realloc
function.
pointer_name = (cast-type*) realloc( (cast-type*)old_memblock, new_size );
-
free
can only free memory that was reserved withmalloc
orcalloc
. -
free
can only release memory that has not been released before. Releasing the same block of memory twice will result in an error. -
Memory allocated with
malloc
orcalloc
is not subject to the familiar rules of variables in their respective scopes. This means that they exist independently of block limits until they are released again or the program is terminated. However, the pointers which refer to such heap-allocated memory are created on the stack and thus only exist within a limited scope. As soon as the scope is left, the pointer variable will be lost - but not the heap memory it refers to.
The functions malloc
and free
are library function and represent the default way of allocating and deallocating memory in C. In C++, they are also part of the standard and can be used to allocate blocks of memory on the heap.
With the introduction of classes and object oriented programming in C++ however, memory allocation and deallocation has become more complex: When an object is created, its constructor needs to be called to allow for member initialization. Also, on object deletion, the destructor is called to free resources and to allow for programmer-defined clean-up tasks. For this reason, C++ introduces the operators new
/ delete
, which represent the object-oriented counterpart to memory management with malloc
/ free
.
If we were to create a C++ object with malloc
, the constructor and destructor of such an object would not be called.
#include <stdlib.h>
#include <iostream>
class MyClass
{
private:
int *_number;
public:
MyClass()
{
std::cout << "Allocate memory\n";
_number = (int *)malloc(sizeof(int));
}
~MyClass()
{
std::cout << "Delete memory\n";
free(_number);
}
void setNumber(int number)
{
*_number = number;
std::cout << "Number: " << _number << "\n";
}
};
int main()
{
// allocate memory using malloc
// comment these lines out to run the example below
MyClass *myClass = (MyClass *)malloc(sizeof(MyClass));
myClass->setNumber(42); // EXC_BAD_ACCESS
free(myClass);
// allocate memory using new
MyClass *myClass = new MyClass();
myClass->setNumber(42); // works as expected
delete myClass;
return 0;
}
Major differences between malloc
/free
and new
/delete
:
-
Constructors / Destructors Unlike
malloc( sizeof(MyClass) )
, the callnew MyClass()
calls the constructor. Similarly,delete
calls the destructor. -
Type safety
malloc
returns a void pointer, which needs to be cast into the appropriate data type it points to. This is not type safe, as you can freely vary the pointer type without any warnings or errors from the compiler as in the following small example:MyObject *p = (MyObject*)malloc(sizeof(int));
-
Operator Overloading As
malloc
andfree
are functions defined in a library, their behavior can not be changed easily. The new and delete operators however can be overloaded by a class in order to include optional proprietary behavior. We will look at an example of overloadingnew
further down in this section.
In some cases, it makes sense to separate memory allocation from object construction. Consider a case where we need to reconstruct an object several times. If we were to use the standard new
/delete
construct, memory would be allocated and freed unnecessarily as only the content of the memory block changes but not its size. By separating allocation from construction, we can get a significant performance increase.
C++ allows us to do this by using a construct called placement new: With placement new
, we can pass a preallocated memory and construct an object at that memory location. Consider the following code:
void *memory = malloc(sizeof(MyClass));
MyClass *object = new (memory) MyClass;
The syntax new (memory)
is denoted as placement new. The difference to the "conventional" new
we have been using so far is that that no memory is allocated. The call constructs an object and places it in the assigned memory location. There is however, no delete
equivalent to placement new
, so we have to call the destructor explicitly in this case instead of using delete
as we would have done with a regular call to new
:
object->~MyClass();
free(memory);
Important: Note that this should never be done outside of placement new
.
One of the major advantages of new
/delete
over free
/malloc
is the possibility of overloading. While both malloc
and free
are function calls and thus can not be changed easily, new
and delete
are operators and can thus be overloaded to integrate customized functionality, if needed.
The syntax for overloading the new operator looks as follows:
void* operator new(size_t size);
The operator receives a parameter size
of type size_t
, which specifies the number of bytes of memory to be allocated. The return type of the overloaded new
is a void pointer, which references the beginning of the block of allocated memory.
The syntax for overloading the delete operator looks as follows:
void operator delete(void*);
In addition to the new
and delete
operators we have seen so far, we can use the following code to create an array of objects:
void* operator new[](size_t size);
void operator delete[](void*);
Let us consider the example on the right, which has been slightly modified to allocate an array of objects instead of a single one.
-
Memory Leaks Memory leaks occur when data is allocated on the heap at runtime, but not properly deallocated. A program that forgets to clear a memory block is said to have a memory leak - this may be a serious problem or not, depending on the circumstances and on the nature of the program. For a program that runs, computes something, and quits immediately, memory leaks are usually not a big concern. Memory leaks are mostly problematic for programs that run for a long time and/or use large data structures. In such a case, memory leaks can gradually fill the heap until allocation requests can no longer be properly met and the program stops responding or crashes completely. We will look at an example further down in this section.
-
Buffer Overruns Buffer overruns occur when memory outside the allocated limits is overwritten and thus corrupted. One of the resulting problems is that this effect may not become immediately visible. When a problem finally does occur, cause and effect are often hard to discern. It is also sometimes possible to inject malicious code into programs in this way, but this shall not be discussed here.
In this example, the allocated stack memory is too small to hold the entire string, which results in a segmentation fault:
char str[5]; strcpy(str,"BufferOverrun"); printf("%s",str);
-
Uninitialized Memory Depending on the C++ compiler, data structures are sometimes initialized (most often to zero) and sometimes not. So when allocating memory on the heap without proper initialization, it might sometimes contain garbage that can cause problems.
Generally, a variable will be automatically initialized in these cases:
- it is a class instance where the default constructor initializes all primitive types
- array initializer syntax is used, such as int a[10] = {}
- it is a global or extern variable
- it is defined
static
The behavior of the following code is potentially undefined:
int a; int b=a*42; printf("%d",b);
-
Incorrect pairing of allocation and deallocation Freeing a block of memory more than once will cause a program to crash. This can happen when a block of memory is freed that has never been allocated or has been freed before. Such behavior could also occur when improper pairings of allocation and deallocation are used such as using
malloc()
withdelete
ornew
withfree()
.In this first example, the wrong
new
anddelete
are paireddouble *pDbl=new double[5]; delete pDbl;
In this second example, the pairing is correct but a double deletion is performed:
char *pChr=new char[5]; delete[] pChr; delete[] pChr;
-
Invalid memory access This error occurs when trying to access a block of heap memory that has not yet or has already been deallocated.
In this example, the heap memory has already been deallocated at the time when
strcpy()
tries to access it:char *pStr=new char[25]; delete[] pStr; strcpy(pStr, "Invalid Access");
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --log-file=/home/workspace/valgrind-out.txt /home/workspace/a.out
Let us look at the call parameters one by one:
- --leak-check : Controls the search for memory leaks when the client program finishes. If set to
summary
, it says how many leaks occurred. If set tofull
, each individual leak will be shown in detail. - --show-leak-kinds : controls the set of leak kinds to show when —leak-check=full is specified. Options are
definite
,indirect
,possible
reachable
,all
andnone
- --track-origins : can be used to see where uninitialised values come from.