MODULES HOME CODING 1: COMPUTER MEMORY 2: POINTERS 2: PRACTICE 3: ADVANCED POINTERS 4: HEAP ALLOCATION


MODULE 1: Pointers, References, and Memory Allocation

This module contains resources for learning about pointers and memory in computer science. The module discusses these concepts in terms of the programming language C. Many of the same concepts apply to other programming languages.

The module contains multiple sections beginning with a discussion of how memory works in C.

It is best to start with Section 1. Before section 1 is an embedded instance of OnlineGDB, an editing environment that allows you to write and compile C code online.

Table of Contents:
1: C Programming Environment
2: Section 1: Computer Memory Basics
3: Section 2: Pointers
4: Section 2 Practice Problems
5: Section 3: Advanced Pointer Concepts Including Double Pointers, Void Pointers, and Pointer Math
6: Section 4: Dynamic Memory Allocation and Deallocation on the Heap


C CODING Environment



Section 1: COMPUTER MEMORY

Computers much like our brains need to have the ability to store, hold, and work with information. Typically we consider people to have short and long term memories. Short term memories are things we need to hold in our brains for a short time, such as the next step in a recipe when we are cooking. Long term memories are things we store for a longer time, such as meaningful events and things we have learned.

Computers work in much the same way. In our computer we have short term memory, commonly referred to as Random Access Memory or RAM. We also have long term memory or storage, typical in the form of a hard disk drive (HDD) or solid state drive (SSD). On our HDD or SSD, we find the files on our computer. In our RAM, we find the programs and files that are currently open or actively in use. Memory is typically referred to in terms of bits. 8 bits make up a byte of memory. Typically ram in current computers are gigabytes in size, with 8 to 32 gigabytes being commonplace amounts in consumer computers. In terms of programming, it is this RAM we care about the most.
bit conversion table

When we are programming in some languages, including C or C++, we have direct access to the RAM that our program is assigned. This assignment of memory is done by our computer and the compiler, but we need to give it directions for it to work properly. Memory in a computer can be thought of as being very similar to that of houses on a street. Each section of memory has an address. This address allows us to refer to what is held at this specific location in memory and keeps memory organized much like house addresses allow us to refer to a specific address on a road. These addresses refer to specifically one byte of memory. Memory addresses are represented using hexademical. The following are examples of valid memory addresses:

                0x00000000001
0x00034AEEFC2
0xDEADBEEF
0x3A39AFE45
As we can see, these addresses all have some things in common. Because they are hexadecimal numbers, we start them with the characters 0x. These characters tell us that the following number is represented in base 16, or hexadecimal. Because the number is hexadecimal, it is composed of the following characters: 0 1 2 3 4 5 6 7 8 9 A B C D E F.

The memory in our computer that we access in our programs are divided commonly into two types: the stack and the heap. The stack consists primarily of primitives such as integers, longs, and doubles. The stack behaves exactly as it sounds. On a stack, new elements (variables) are added to the top. Elements are also removed from the top of stack as we finish using them. This organizational pattern is called First-In-Last-Out or FILO. These primitives have a specific size in terms of how much memory these use. For example, an integer in a modern computer is 4 bytes or 32 bits in size. Below are examples of how we might create variables that will reside on this stack.
 
                int x = 44; #32 bit integer 
int y = 13; #32 bit integer
long int ex = 4398; #64 bit integer
double dub = 188.1114; #64 bit float
Creating variables in our code on the stack is very simple and does not require any additional steps. This is known as static memory allocation because we do not need to do anything to allocate memory for these variables. Our program compiler takes care of this step for us because it knows based on the type of our variable how much memory it needs to allocate on this stack. For example,
 int x = 44;
is easy for the compiler to figure out. It knows that the type is int, which needs 4 bytes of memory regardless of what the value is that we want to store. We can even reassign this variable to a different value later and the amount of memory needed doesn't change.

The heap, on the other hand, behaves quite a bit differently. We can think of the heap as being a large mountain of memory. Just like before, the heap has addresses to refer to each byte of memory. However, we now need to specify in our code the amount of memory we actually want to use. This is because the heap allows us to define more complicated and larger things in our code. For example, strings, arrays, and Objects (C++) are placed on this heap. However, because their size is not guarunteed, we need to assign how much space on the heap we actually need for these elements.
The stack and heap

In section 2, below, we will discuss the concept of pointers, which are something we will need to further explore the heap. In section 3, we will explore more advanced uses of pointers and references beyond their basic usage. In section 4, we will continue looking at this idea of the heap.

This section has been composed from the following sources. If you want additional resources, see the following links and videos:
Relevant Links:
Basics of Memory and static memory addressing
W3Schools Memory in C

Relevant Videos:



SECTION 2: POINTERS

In the previous section we began discussing memory in a computer. In this section, we saw that creating primitive variables on the stack such as ints, longs, and doubles are very simple and the space they take up is assigned statically by the compiler when our code is executed. We also saw that the heap is our other area of memory we can access in our programs. This section will explore how we can refer to things we create on this heap, pointers.

First, lets take a slight detour and consider the other side of this coin: references. References are specifically an address in memory. They are the physical location of a value in memory. For example, consider the following code:

                int x = 43; 
printf("%p \n",&x);
If we run this code, we'll find that the printf statement will print a value that looks like a memory address. If you are confused by the printf statement, the following is a video explaining printf in C.



What is printed here is actually just the memory address where our variable x is being stored on the stack. The & symobol means we want to access the location of the variable x. We'll use this knowledge now to create a pointer. Pointers are as simple as they sound; a pointer is a variable created to provide a reference to an address in memory we want to access. This address then provides us access to something in memory we wish to work with more. Let us consider an example of a pointer and break down it's components.

                int x = 43; 
int* y = &x;
On this first line, we create a variable in the same way we normally would. In this line we assign the value of 43 to the int variable x. Now lets say we want to be able to create a pointer to x. To do this we say int*. The * here tells our compiler that what we are trying to create is a pointer. We then set our pointer variable y to be equal to a reference to x (&x). The & here means we want to make a reference to the variable x. What we are saying here is we want to create an int pointer y that is equal to the address of x. This allows us to do two different things. It allows us to access the value that has been assigned to the variable x through the reference. It also assigns y a value of the address where our variable x has been stored. Let us continue looking at our example from before:

            int main(){
                int x = 43; 
int* y = &x;
printf("Value at y = %p \n", y);
printf("Value at x = %d \n", x);
printf("Value at &y = %d \n", *y); return 0; }
This code will print the following three lines when it is printed:

            >     Value at y = 0x3A39AFE45 
> Value at x = 43
> Value at *y = 43
Here we see that our pointer y can be used for two things. When we state *y this means we can access the VALUE that our pointer is pointing to. When we do this, we get the value stored at the memory address our pointer is pointing to. In this case it is the value assigned to the variable x, 43. When we state y by itself then we get the value assigned to y, which as it's a pointer, is the address in memory we're pointing to. In this case, we get the value 0x3A39AFE45 because thats the address in memory where our variable x is located.

And thats the basics for pointers and references. In section 3 we'll explore this concept further and look at some more complex examples. This section contains some example questions below it. For these try and work on them yourself, then view the hidden answers by clicking on them.

Relevant Links:
Pointers in C
W3Schools Pointers in C

Relevant Videos:











SECTION 2: PRACTICE

Question 1: Given the following code snippet what will be printed when this is executed?
 
                int main(){
                    int datum = 300; 
                    int* ptr = &datum; 
                    int ptrval = *ptr; 

                    printf("%d \n",datum); 
                    printf("%p \n",ptr); 
                    printf("%d \n",*ptr); 
                    printf("%d \n",ptrval);
                    return 0;
                }
                
Answer

                    300 
0xAEC43532 #(the address of datum)
300
00
Explanation Our first print statement here is quite straightforward. It simply prints the value associated with our variable datum.
Our second print statement here will print the address that our pointer is pointing to.
Our third print statement here requests the value that our pointer variable ptr is pointing to, so the value of datum, or 300.
Our fourth print statement works similarly to our third, except instead of accessing the value we are pointing to using ptr, we have a new variable ptrval that has been set rqul to the value that ptr is pointing at.


SECTION 3: ADVANCED POINTER CONCEPTS

In our last section we discussed the basic uses of pointers and references! Now we're going to look at some more advanced uses of pointers. In the next section, we'll explore using these pointers for dynamic memory allocation.

This section will consider three specific concepts: double pointers, void pointers, and pointer math.

3.1: Double Pointers

Consider the following section of code.

              int main(){
                int val = 5;
                int *pval = &val;
                int **d_pval = &pval;
                printf("Value of val = %d\n", val);
                printf("Value of val using single pointer = %d\n", *pval);
                printf("Value of var using double pointer = %d\n", **d_pval);

                return 0;
              }

              >Value of val = 5
              >Value of val using a single pointer = 5
              >Value of val using a double pointer = 5

            
As we can see in this code we are defining three different variables. val is a simple integer with value 5. pval is a simple pointer much like what we've seen in section 2. d_pval is a pointer that we have defined to point to our first pointer. This is a pointer to a pointer or a double pointer. Let's consider another example, this time in terms of what's happening in memory. As we can see in the image below we have var which is a variable with value 10. ptr1 is a regular pointer. Its value is the memory address (location) of our variable var. ptr2 is a double pointer. Its value is the memory address (location) of our pointer ptr1.

bit conversion table

3.2: Void Pointers

So far with pointers we have defined pointers as having the same datatype as the data we are pointing to. Void pointers are a type of pointer that can point to any type of data and has no associated data type. Consider the following block of code:

                int main(){
                  int a = 10;
                  char b = 'x';
 
                  // void pointer holds address of int 'a'
                  void *p = &a;
                  // void pointer reassigned to hold address of char 'b'
                  p = &b;
                  }  
              
In this code, we define two variables a and b, with a being an int and b being a char. We also have defined our void pointer, named p. At first, we see that p has been assigned a reference to our variable a. However, we can seamlessly reassign p to a reference to our variable b, despite the fact that a and b are different types of data. This is great because we can access and reassign our pointer any time we want to anything we want. Getting access to the address we are pointing to is easy. However, getting access to the value that we are pointing at is tricky. If we were to run the code below we would receive an error from the C compiler. Relevant Links:

                printf("%d", *p);
              
This is because C needs to know the type of the data we are referring to when we dereference the pointer to get the value we are pointing to. Instead, when we need access to the value we are pointing to we need to use the following code wherein we specify the type we are referring to when we dereference the pointer.

                printf("%d",*(int*)ptr);
              


3.2: Pointer Arithmetic

So far we have seen how to define double pointers and how to create void pointers. One other concept that we often need to do is what is known as pointer arithmetic. Pointers simply reference a specific address in memory as we have established. Sometimes we want to reference a different address in memory than what we initially had referenced. Sometimes we can do this simply by reassigning the pointer to a new address manually. However, other times we cannot. Take for example the following code:

                int main(){
                  int arr[2] = {34,56};
                  void* parr = &arr;

                  printf("%d",*(int*)parr);

                  parr = parr+sizeof(int);
                  printf("%d",*(int*)parr);

                  return 0;
                }
              
In this example, we see that inside of our function, we first define an array containing two integers, 34 and 56. When we define a pointer to this array, we use a void pointer once more. The void pointer starts out pointing to the 0th index of the array. We can see this when we run the code and receive the following outputs.

                >34
                >56
              
The first print statement prints out 34 because our pointer initially references the address of the 0th index of our array. Then we modify our pointer by adding the size of an integer. This shifts the pointer to point to the next address in memory or the next element in the array. We can do this for arrays of any other type. We can also use this to shift by larger amounts or structs. We can even use this to iterate over arrays or other data structures. We will see some of this in section 4 where we explore Malloc and heap memory allocation.

Double Pointers Examples
Double Pointers Examples

Relevant Videos:










SECTION 4: DYNAMIC MEMORY ALLOCATION

Previously, we have looked at pointers, references, how to use them, and how stack memory allocation works. Thus far, all of the memory we have allocated has been on the stack. This includes variables and larger elements such as arrays which we looked at in the previous section with pointer math. The heap is our other area of memory in our computer. The heap can be considered to be a larger, unordered section of memory that needs to be managed manually. To store stuff on the heap we need to utilise a memory allocator. C happens to have an excellent built in memory allocator called Malloc which we will be considering today. Malloc allows us to simply specify the size of memory on the heap we want to allocate and the memory will be allocated for us. We don't need to do anything else. However, this means that we need to have some idea of how much memory we are going to need prior to allocating it. Malloc is a function within C that we can call that takes as a parameter the size in bytes that we want to allocate. Let us consider the following code involving integers and allocating an integer array for 100 integers to the heap.

            int main(){
              int* p = (int*) malloc(400);

              return 0;
            }
          
Here we have define a pointer utilising malloc to allocate some memory on our heap. Specifically we have allocated 400 bytes. Since an integer is 4 bytes this is room for an array of 100 integers. If we wanted to allocate a different number of integers or just speficy the number of integers we could instead do:

            int main(){
              int* p = (int*) malloc(200*sizeof(int));

              return 0;
            }
          
This code should work to allocate a section of memory. However, sometimes it is possible we will have run out of memory on the heap that we can allocate. When this happens our pointer will fail to be allocated. We can check this with the following:

            int main(){
              int* p = (int*) malloc(200*sizeof(int));
              if (p == NULL) {
                printf("memory failed to allocate. \n");
              }

              return 0;
            }
          
If our memory has been allocated correctly, we can now populate the array and access the array elements using our pointer and pointer arithmetic like we saw in the last section. For example, lets set each of the elements in an array of size 200 to be equal to the numbers 1-200 in order.

            int main(){
              int i;
              int* p = (int*) malloc(200*sizeof(int));
              if (p == NULL) {
                printf("memory failed to allocate. \n");
              }
              else{
                for(i = 0; i < 200; i++){
                  p[i]=i+1;
                }
              }
              //print the elements
              for(i=0; i< 200; i++){
                printf("%d, ",p[i]);
              }

              return 0;
            }
          
In this code block we have allocated 800 bytes for 200 integers, populated the memory, and printed out the results to our terminal. When we execute this code, we will receive the numbers 1 to 200 printed out.

Just like how we need to allocate memory we need to also free that memory up. We need to do this before we finish our code. Else we experience what is known as a memory leak. A memory leak just means that we have allocated memory that we can no longer access. Because of this the memory is taken up but we have no way of accessing it or freeing it. To avoid this happening, we need to use something called free(). free() simply allows us to dynamically de-allocate memory. Consider the following code, which is very similar to our code before but now contains a free() call.

            int main(){
              int i;
              int* p = (int*) malloc(200*sizeof(int));
              if (p == NULL) {
                printf("memory failed to allocate. \n");
              }
              else{
                for(i = 0; i < 200; i++){
                  p[i]=i+1;
                }
              }
              //print the elements
              for(i=0; i< 200; i++){
                printf("%d, ",p[i]);
              }

              //dynamically deallocate memory
              free(p);
              p=NULL;
              return 0;
            }
          
Here the code is almost the same until the last two steps. The first step is calling free() a function built into C. free() takes in our pointer and frees the memory that has been allocated using the pointer. Secondly, we set our pointer to be equal to NULL. This means that we cannot try and access the memory after it has been deallocated which can be done and should be avoided. This can cause severe issues including potential security issues.

Congrats! We have reached the end of Module 1 for now! More content will be added in the future but this is the basics of memory allocation both static and dynamic.

Below are video resources with a number of different explanations for how Pointers work in memory.
The videos are listed in order similarly to how the written sections above are organized.