Extreme C
上QQ阅读APP看书,第一时间看更新

Heap

Almost any code, written in any programming language, uses Heap memory in some way. That's because the Heap has some unique advantages that cannot be achieved by using the Stack.

On the other hand, it also has some disadvantages; for example, it is slower to allocate a region of Heap memory in comparison to a similar region in Stack memory.

In this section, we are going to talk more about the Heap itself and the guidelines we should keep in mind when using Heap memory.

Heap memory is important because of its unique properties. Not all of them are advantageous and, in fact, some of them can be considered as risks that should be mitigated. A great tool always has good points and some bad points, and if you are going to use it properly, you are required to know both sides very well.

Here, we are going to list these features and see which ones are beneficial and which are risky:

  1. The Heap doesn't have any memory blocks that are allocated automatically. Instead, the programmer must use malloc or similar functions to obtain Heap memory blocks, one by one. In fact, this could be regarded as a weak point for Stack memory that is resolved by Heap memory. Stack memory can contain stack frames, which are not allocated and pushed by the programmer but as a result of function calls, and in an automatic fashion.
  2. The Heap has a large memory size. While the size of the Stack is limited and it is not a good choice for keeping big objects, the Heap allows the storing of very big objects even tens of gigabytes in size. As the Heap size grows, the allocator needs to request more heap pages from the operating system, and the Heap memory blocks are spread among these pages. Note that, unlike the Stack segment, the allocating addresses in the Heap memory move forward to bigger addresses.
  3. Memory allocation and deallocation inside Heap memory are managed by the programmer. This means that the programmer is the sole responsible entity for allocating the memory and then freeing it when it is not needed anymore. In many recent programming languages, freeing allocated Heap blocks is done automatically by a parallel component called garbage collector. But in C and C++, we don't have such a concept and freeing the Heap blocks should be done manually. This is indeed a risk, and C/C++ programmers should be very careful while using heap memory. Failing to free the allocated Heap blocks usually leads to memory leaks, which can be fatal in most cases.
  4. Variables allocated from the Heap do not have any scope, unlike variables in the Stack.
  5. This is a risk because it makes memory management much harder. You don't know when you need to deallocate the variable, and you have to come up with some new definitions for the scope and the owner of the memory block in order to do the memory management effectively. Some of these methods are covered in the upcoming sections.
  6. We can only use pointers to address a Heap memory block. In other words, there is no such concept as a Heap variable. The Heap region is addressed via pointers.
  1. Since the Heap segment is private to its owner process, we need to use a debugger to probe it. Fortunately, C pointers work with the Heap memory block exactly the same as they work with Stack memory blocks. C does this abstraction very well, and because of this, we can use the same pointers to address both memories. Therefore, we can use the same methods that we used to examine the Stack to probe the Heap memory.

In the next section, we are going to discuss how to allocate and deallocate a heap memory block.

Heap memory allocation and deallocation

As we said in the previous section, Heap memory should be obtained and released manually. This means that the programmer should use a set of functions or API (the C standard library's memory allocation functions) in order to allocate or free a memory block in the Heap.

These functions do exist, and they are defined in the header, stdlib.h. The functions used for obtaining a Heap memory block are malloc, calloc, and realloc, and the sole function used for releasing a Heap memory block is free. Example 5.3 demonstrates how to use some of these functions.

Note:

In some texts, dynamic memory is used to refer to Heap memory. Dynamic memory allocation is a synonym for Heap memory allocation.

The following code box shows the source code of example 5.3. It allocates two Heap memory blocks, and then it prints its own memory mappings:

#include <stdio.h> // For printf function

#include <stdlib.h> // For C library's heap memory functions

void print_mem_maps() {

#ifdef __linux__

FILE* fd = fopen("/proc/self/maps", "r");

if (!fd) {

printf("Could not open maps file.\n");

exit(1);

}

char line[1024];

while (!feof(fd)) {

fgets(line, 1024, fd);

printf("> %s", line);

}

fclose(fd);

#endif

}

int main(int argc, char** argv) {

// Allocate 10 bytes without initialization

char* ptr1 = (char*)malloc(10 * sizeof(char));

printf("Address of ptr1: %p\n", (void*)&ptr1);

printf("Memory allocated by malloc at %p: ", (void*)ptr1);

for (int i = 0; i < 10; i++) {

printf("0x%02x ", (unsigned char)ptr1[i]);

}

printf("\n");

// Allocation 10 bytes all initialized to zero

char* ptr2 = (char*)calloc(10, sizeof(char));

printf("Address of ptr2: %p\n", (void*)&ptr2);

printf("Memory allocated by calloc at %p: ", (void*)ptr2);

for (int i = 0; i < 10; i++) {

printf("0x%02x ", (unsigned char)ptr2[i]);

}

printf("\n");

print_mem_maps();

free(ptr1);

free(ptr2);

return 0;

}

Code Box 5-6 [ExtremeC_examples_chapter5_3.c]: Example 5.3 showing the memory mappings after allocating two Heap memory blocks

The preceding code is cross-platform, and you can compile it on most Unix-like operating systems. But the print_mem_maps function only works on Linux since the __linux__ macro is only defined in Linux environments. Therefore, in macOS, you can compile the code, but the print_mem_maps function won't do anything.

The following shell box is the result of running the example in a Linux environment:

$ gcc ExtremeC_examples_chapter5_3.c -o ex5_3.out

$ ./ex5_3.out

Address of ptr1: 0x7ffe0ad75c38

Memory allocated by malloc at 0x564c03977260: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Address of ptr2: 0x7ffe0ad75c40

Memory allocated by calloc at 0x564c03977690: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

> 564c01978000-564c01979000 r-xp 00000000 08:01 5898436 /home/kamranamini/extreme_c/5.3/ex5_3.out

> 564c01b79000-564c01b7a000 r--p 00001000 08:01 5898436 /home/kamranamini/extreme_c/5.3/ex5_3.out

> 564c01b7a000-564c01b7b000 rw-p 00002000 08:01 5898436 /home/kamranamini/extreme_c/5.3/ex5_3.out

> 564c03977000-564c03998000 rw-p 00000000 00:00 0 [heap]

> 7f31978ec000-7f3197ad3000 r-xp 00000000 08:01 5247803 /lib/x86_64-linux-gnu/libc-2.27.so

...

> 7f3197eef000-7f3197ef1000 rw-p 00000000 00:00 0

> 7f3197f04000-7f3197f05000 r--p 00027000 08:01 5247775 /lib/x86_64-linux-gnu/ld-2.27.so

> 7f3197f05000-7f3197f06000 rw-p 00028000 08:01 5247775 /lib/x86_64-linux-gnu/ld-2.27.so

> 7f3197f06000-7f3197f07000 rw-p 00000000 00:00 0

> 7ffe0ad57000-7ffe0ad78000 rw-p 00000000 00:00 0 [stack]

> 7ffe0adc2000-7ffe0adc5000 r--p 00000000 00:00 0 [vvar]

> 7ffe0adc5000-7ffe0adc7000 r-xp 00000000 00:00 0 [vdso]

> ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

$

Shell Box 5-17: Output of example 5.3 in Linux

The preceding output has a lot to say. The program prints the addresses of the pointers ptr1 and ptr2. If you find the memory mapping of the Stack segment, as part of the printed memory mappings, you see that the Stack region starts from 0x7ffe0ad57000 and ends at 0x7ffe0ad78000. The pointers are within this range.

This means that the pointers are allocated from the Stack, but they are pointing to a memory region outside of the Stack segment, in this case, the Heap segment. It is very common to use a Stack pointer to address a Heap memory block.

Keep in mind that the ptr1 and ptr2 pointers have the same scope and they will be freed when the main function returns, but there is no scope to the Heap memory blocks obtained from the Heap segment. They will remain allocated until the program frees them manually. You can see that before returning from the main function, both memory blocks are freed using the pointers pointing to them and using the free function.

As a further note regarding the above example, we can see that the addresses returned by the malloc and calloc functions are located inside the Heap segment. This can be investigated by comparing the returned addresses and the memory mapping described as [heap]. The region marked as heap starts from 0x564c03977000 and ends at 0x564c03998000. The ptr1 pointer points to the address 0x564c03977260 and the ptr2 pointer points to the address 0x564c03977690, which are both inside the heap region.

Regarding the Heap allocation function, as their names imply, calloc stands for clear and allocate and malloc stands for memory allocate. So, this means that calloc clears the memory block after allocation, but malloc leaves it uninitialized until the program does it itself if necessary.

Note:

In C++, the new and delete keywords do the same as malloc and free respectively. Additionally, new operator infers the size of the allocated memory block from the operand type and also converts the returned pointer to the operand type automatically.

But if you look at the bytes in the two allocated blocks, both of them have zero bytes. So, it seems that malloc has also initialized the memory block after the allocation. But based on the description of malloc in the C Specification, malloc doesn't initialize the allocated memory block. So, why is that? To move this further, let's run the example in a macOS environment:

$ clang ExtremeC_examples_chapter5_3.c -o ex5_3.out

$ ./ ex5_3.out

Address of ptr1: 0x7ffee66b2888

Memory allocated by malloc at 0x7fc628c00370: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80 0x00 0x00

Address of ptr2: 0x7ffee66b2878

Memory allocated by calloc at 0x7fc628c02740: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

$

Shell Box 5-18: Output of example 5.3 on macOS

If you look carefully, you can see that the memory block allocated by malloc has some non-zero bytes, but the memory block allocated by calloc is all zeros. So, what should we do? Should we assume that the memory block allocated by malloc in Linux is always zeros?

If you are going to write a cross-platform program, always be aligned with the C specification. The specification says malloc does not initialize the allocated memory block.

Even when you are writing your program only for Linux and not for other operating systems, be aware that future compilers may behave differently. Therefore, according to the C specification, we must always assume that the memory block allocated by the malloc is not initialized and it should be initialized manually if necessary.

Note that since malloc doesn't initialize the allocated memory, it is usually faster than calloc. In some implementations, malloc doesn't actually allocate the memory block and defer the allocation until when the memory block is accessed (either read or write). This way, memory allocations happen faster.

If you are going to initialize the memory after malloc, you can use the memset function. Here is an example:

#include <stdlib.h> // For malloc

#include <string.h> // For memset

int main(int argc, char** argv) {

char* ptr = (char*)malloc(16 * sizeof(char));

memset(ptr, 0, 16 * sizeof(char)); // Fill with 0

memset(ptr, 0xff, 16 * sizeof(char)); // Fill with 0xff

...

free(ptr);

return 0;

}

Code Box 5-7: Using the memset function to initialize a memory block

The realloc function is another function that is introduced as part of the Heap allocation functions. It was not used as part of example 5.3. It actually reallocates the memory by resizing an already allocated memory block. Here is an example:

int main(int argc, char** argv) {

char* ptr = (char*)malloc(16 * sizeof(char));

...

ptr = (char*)realloc(32 * sizeof(char));

...

free(ptr);

return 0;

}

Code Box 5-8: Using the realloc function to change the size of an already allocated block

The realloc function does not change the data in the old block and only expands an already allocated block to a new one. If it cannot expand the currently allocated block because of fragmentation, it will find another block that's large enough and copy the data from the old block to the new one. In this case, it will also free the old block. As you can see, reallocation is not a cheap operation in some cases because it involves many steps, hence it should be used with care.

The last note about example 5.3 is on the free function. In fact, it deallocates an already allocated Heap memory block by passing the block's address as a pointer. As it is said before, any allocated Heap block should be freed when it is not needed. Failing to do so leads to memory leakage. Using a new example, example 5.4, we are going to show you how to detect memory leaks using the valgrind tool.

Let's first produce some memory leaks as part of example 5.4:

#include <stdlib.h> // For heap memory functions

int main(int argc, char** argv) {

char* ptr = (char*)malloc(16 * sizeof(char));

return 0;

}

Code Box 5-9: Producing a memory leak by not freeing the allocated block when returning from the main function

The preceding program has a memory leak because when the program ends, we have 16 bytes of Heap memory allocated and not freed. This example is very simple, but when the source code grows and more components are involved, it would be too hard or even impossible to detect it by sight.

Memory profilers are useful programs that can detect the memory issues in a running process. The famous valgrind tool is one of the most well knowns.

In order to use valgrind to analyze example 5.4, first we need to build the example with the debug option, -g. Then, we should run it using valgrind. While running the given executable object file, valgrind records all of the memory allocations and deallocations. Finally, when the execution is finished or a crash happens, valgrind prints out the summary of allocations and deallocations and the amount of memory that has not been freed. This way, it can let you know how much memory leak has been produced as part of the execution of the given program.

The following shell box demonstrates how to compile and use valgrind for example 5.4:

$ gcc -g ExtremeC_examples_chapter5_4.c -o ex5_4.out

$ valgrind ./ex5_4.out

==12022== Memcheck, a memory error detector

==12022== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.

==12022== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info

==12022== Command: ./ex5_4.out

==12022==

==12022==

==12022== HEAP SUMMARY:

==12022== in use at exit: 16 bytes in 1 blocks

==12022== total heap usage: 1 allocs, 0 frees, 16 bytes allocated

==12022==

==12022== LEAK SUMMARY:

==12022== definitely lost: 16 bytes in 1 blocks

==12022== indirectly lost: 0 bytes in 0 blocks

==12022== possibly lost: 0 bytes in 0 blocks

==12022== still reachable: 0 bytes in 0 blocks

==12022== suppressed: 0 bytes in 0 blocks

==12022== Rerun with --leak-chck=full to see details of leaked memory

==12022==

==12022== For counts of detected and suppressed errors, rerun with: -v

==12022== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

$

Shell Box 5-19: Output of valgrind showing the 16-byte memory leak as part of the execution of example 5.4

If you look into the HEAP SUMMARY section in Shell Box 5-19, you can see that we had 1 allocation and 0 frees, and 16 bytes remained allocated while exiting. If you come down a bit to the LEAK SUMMARY section, it states that 16 bytes are definitely lost, and this means a memory leak!

If you want to know exactly at which line the mentioned leaking memory block has been allocated, you can use valgrind with a special option designed for this. In the following shell box, you will see how to use valgrind to find the lines responsible for the actual allocation:

$ gcc -g ExtremeC_examples_chapter5_4.c -o ex5_4.out

$ valgrind --leak-check=full ./ex5_4.out

==12144== Memcheck, a memory error detector

==12144== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.

==12144== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info

==12144== Command: ./ex5_4.out

==12144==

==12144==

==12144== HEAP SUMMARY:

==12144== in use at exit: 16 bytes in 1 blocks

==12144== total heap usage: 1 allocs, 0 frees, 16 bytes allocated

==12144==

==12144== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1

==12144== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)

==12144== by 0x108662: main (ExtremeC_examples_chapter5_4.c:4)

==12144==

==12144== LEAK SUMMARY:

==12144== definitely lost: 16 bytes in 1 blocks

==12144== indirectly lost: 0 bytes in 0 blocks

==12144== possibly lost: 0 bytes in 0 blocks

==12144== still reachable: 0 bytes in 0 blocks

==12144== suppressed: 0 bytes in 0 blocks

==12144==

==12144== For counts of detected and suppressed errors, rerun with : -v

==12144== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

$

Shell Box 5-20: Output of valgrind showing the line that is responsible for the actual allocation

As you can see, we have passed the --leak-check=full option to valgrind, and now it shows the line of code that is responsible for the leaking Heap memory. It clearly shows that line 4 in Code Box 5-9, which is a malloc call, is where the leaking Heap block has been allocated. This can help you to trace it further and find the right place that the mentioned leaking block should be freed.

OK, let's change the preceding example so that it frees the allocated memory. We just need to add the free(ptr) instruction before the return statement, as we can see here:

#include <stdlib.h> // For heap memory functions

int main(int argc, char** argv) {

char* ptr = (char*)malloc(16 * sizeof(char));

free(ptr);

return 0;

}

Code Box 5-10: Freeing up the allocated memory block as part of example 5.4

Now with this change, the only allocated Heap block is freed. Let's build and run valgrind again:

$ gcc -g ExtremeC_examples_chapter5_4.c -o ex5_4.out

$ valgrind --leak-check=full ./ex5_4.out

==12175== Memcheck, a memory error detector

==12175== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.

==12175== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info

==12175== Command: ./ex5_4.out

==12175==

==12175==

==12175== HEAP SUMMARY:

==12175== in use at exit: 0 bytes in 0 blocks

==12175== total heap usage: 1 allocs, 1 frees, 16 bytes allocated

==12175==

==12175== All heap blocks were freed -- no leaks are possible

==12175==

==12175== For counts of detected and suppressed errors, rerun with -v

==12175== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

$

Shell Box 5-20: Output of valgrind after freeing the allocated memory block

As you can see, valgrind says that All Heap blocks were freed, and this effectively means that we have no further memory leakage in our program. Running programs with valgrind can slow them down noticeably by a factor of 10 to 50, but it can help you to spot the memory issues very easily. It's a good practice to let your written programs run inside a memory profiler and catch memory leaks as soon as possible.

Memory leaks can be considered both as technical debts, if you have a bad design that causes the leaks, or as risks, where it's known that we have a leak, but we don't know what will happen if the leak continues to grow. But in my opinion, they should be treated as bugs; otherwise, it will take a while for you to look back at them. Usually, in teams, memory leaks are treated as bugs that should be fixed as soon as possible.

There are other memory profilers other than valgrind. LLVM Address Sanitizer (or ASAN) and MemProf are also other well-known memory profilers. Memory profilers can profile memory usage and allocations using various methods. Next, we discuss some of them:

  • Some profilers can behave like a sandbox, running the target program inside and monitoring all their memory activities. We've used this method to run example 5.4 inside a valgrind sandbox. This method does not require you to recompile your code.
  • Another method is to use the libraries provided by some memory profilers, which wrap memory-related system calls. This way, the final binary will contain all of the logic required for the profiling task.

    valgrind and ASAN can be linked to the final executable object file as a memory profiler library. This method requires the recompilation of your target source code and even making some modifications to your source code as well.

  • Programs can also preload different libraries instead of the default C standard libraries, which contain memory function interpositions of the C library's standard memory allocation functions. This way, you are not required to compile your target source code. You just need to specify the libraries of such profilers in the LD_PRELOAD environment variable to be preloaded instead of the default libc libraries. MemProf uses this method.

Note:

A function interposition is a wrapper function defined in a dynamic library loaded before the target dynamic library, which propagates calls to the target function. Dynamic libraries can be preloaded using the LD_PRELOAD environment variable.

Heap memory principles

As pointed out before, Heap memory is different from Stack memory in several ways. Therefore, heap memory has its own guidelines regarding memory management. In this section, we are going to focus on these differences and come up with some dos and don'ts that we should consider when working with the Heap space.

Every memory block (or a variable) in the Stack has a scope. So, it is an easy task to define the lifetime of a memory block based on its scope. Whenever we are out of scope, all of the variables in that scope are gone. But this is different and much more complex with Heap memory.

A Heap memory block doesn't have any scope, so its lifetime is unclear and should be redefined. This is the reason behind having manual deallocation or generational garbage collection in modern languages such as Java. The Heap lifetime cannot be determined by the program itself or the C libraries used, and the programmer is the sole person who defines the lifetime of a Heap memory block.

When the discussion comes to the programmer's decision, especially in this case, it is complicated and hard to propose a universal silver bullet solution. Every opinion is debatable and can lead to a trade-off.

One of the best proposed strategies to overcome the complexity of the Heap lifetime, which of course is not a complete solution, is to define an owner for a memory block instead of having a scope that encompasses the memory block.

The owner is the sole entity responsible for managing the lifetime of a Heap memory block and is the one who allocates the block in the first place and frees it when the block is not needed anymore.

There are many classic examples of how to use this strategy. Most of the well-known C libraries use this strategy to handle their Heap memory allocations. Example 5.5 is a very simple implementation of this method that is used to manage the lifetime of a queue object written in C. The following code box tries to demonstrate the ownership strategy:

#include <stdio.h> // For printf function

#include <stdlib.h> // For heap memory functions

#define QUEUE_MAX_SIZE 100

typedef struct {

int front;

int rear;

double* arr;

} queue_t;

void init(queue_t* q) {

q->front = q->rear = 0;

// The heap memory block allocated here is owned

// by the queue object.

q->arr = (double*)malloc(QUEUE_MAX_SIZE * sizeof(double));

}

void destroy(queue_t* q) {

free(q->arr);

}

int size(queue_t* q) {

return q->rear - q->front;

}

void enqueue(queue_t* q, double item) {

q->arr[q->rear] = item;

q->rear++;

}

double dequeue(queue_t* q) {

double item = q->arr[q->front];

q->front++;

return item;

}

int main(int argc, char** argv) {

// The heap memory block allocated here is owned

// by the function main

queue_t* q = (queue_t*)malloc(sizeof(queue_t));

// Allocate needed memory for the queue object

init(q);

enqueue(q, 6.5);

enqueue(q, 1.3);

enqueue(q, 2.4);

printf("%f\n", dequeue(q));

printf("%f\n", dequeue(q));

printf("%f\n", dequeue(q));

// Release resources acquired by the queue object

destroy(q);

// Free the memory allocated for the queue object

// acquired by the function main

free(q);

return 0;

}

Code Box 5-11 [ExtremeC_examples_chapter5_5.c]: The example 5.5 demonstrating the ownership strategy for Heap lifetime management

The preceding example contains two different ownerships each of which owning a specific object. The first ownership is about the Heap memory block addressed by the arr pointer in the queue_t structure that is owned by the queue object. As long as the queue object exists, this memory block must remain in place and allocated.

The second ownership is regarding the Heap memory block acquired by the main function as a placeholder for the queue object, q, that is owned by the main function itself. It is very important to distinguish between the Heap memory blocks owned by the queue object and the Heap memory blocks owned by the main function because releasing one of them doesn't release another.

To demonstrate how a memory leak can happen in the preceding code, suppose that you forget to call the destroy function on the queue object. It will definitely lead to a memory leak because the Heap memory block acquired inside the init function would be still allocated and not freed.

Note that if an entity (an object, function, and so on) owns a Heap memory block, it should be expressed in the comments. Nothing should free a Heap memory block if it does not own the block.

Note that multiple deallocations of the same Heap memory block will lead to a double free situation. A double-free situation is a memory corruption issue and like any other memory corruption issue, it should be dealt with and resolved soon after detection. Otherwise, it can have serious consequences like sudden crashes.

Other than the ownership strategy, one could use a garbage collector. The garbage collector is an automatic mechanism that is embedded in a program and tries to collect memory blocks that have no pointer addressing them. One of the old well-known garbage collectors for C is the Boehm-Demers-Weiser Conservative Garbage Collector, which provides a set of memory allocation functions that should be called instead of malloc and other standard C memory allocation functions.

Further Reading:

More information about the Boehm-Demers-Weiser garbage collector can be found here: http://www.hboehm.info/gc/.

Another technique to manage the lifetime of a Heap block is using a RAII object. RAII stands for Resource Acquisition Is Initialization. It means that we can bind the lifetime of a resource, possibly a Heap allocated memory block, to the lifetime of an object. In other words, we use an object that upon its construction initializes the resource, and upon its destruction frees the resource. Unfortunately, this technique cannot be used in C because we are not notified about the destruction of an object. But in C++, using destructors, this technique can be used effectively. In RAII objects, resource initialization happens in the constructor and the code required to de-initialize the resource is put into the destructor. Note that in C++, the destructor is invoked automatically when an object is going out of scope or being deleted.

As a conclusion, the following guidelines are important when working with Heap memory:

  • Heap memory allocation is not free, and it has its own costs. Not all memory allocation functions have the same cost and, usually, malloc is the cheapest one.
  • All memory blocks allocated from the Heap space must be freed either immediately when they are not needed anymore or just before ending the program.
  • Since Heap memory blocks have no scope, the program must be able to manage the memory in order to avoid any possible leakage.
  • Sticking to a chosen memory management strategy for each Heap memory block seems to be necessary.
  • The chosen strategy and its assumptions should be documented throughout the code wherever the block is accessed so that future programmers will know about it.
  • In certain programming languages like C++, we can use RAII objects to manage a resource, possibly a Heap memory block.

So far, we have considered that we have enough memory to store big objects and run any kind of program. But in the following section, we are going to put some constraints on the available memory and discuss the environments where the memory is low, or it is costly (in terms of money, time, performance, and so on) to add further memory storage. In such cases, we need to use the available memory in the most efficient way.