Exploring Memory: C

Acess Codespaces

In this lab you will be using C programming langauage as a tool to understand computer memory.

To do this you will need to do one of the following:

  • Open up Vscode and start new terminal
  • Accept this activity, then use codespaces (as shown below)to carry out the tasks below:

centre

centre

Create a new directory and call it FCS/Exploring_Memory we can do this using the following commands in the terminal.

$  mkdir FCS/Exploring_Memory

Pointers

Now it's time to see why C is used as the basis of nearly all programming languages, operating systems, and embedded code.

Pointers in C are relatively easy and fun to learn. Some C programming tasks are performed more easily with pointers, and other tasks, such as dynamic memory allocation, cannot be performed without using pointers.

So it becomes necessary to learn pointers to become a perfect C programmer.

Let's start learning them in simple and easy steps.

Every variable is a memory location and every memory location has its address defined which can be accessed using the ampersand & operator, which denotes an address in memory.

Create a new file, call it pointersOne.c and enter the following code:

$ nano pointersOne.c [Or any file name of your liking but has to ends with `.c`]

Or using the GUI instruction above.

write the following code:

#include <stdio.h>

int main()
{
    int a=1, b =2 ,c =0;
  
    printf("Address for the variable a: %lu \n", (long)&a); // long casts the value in &a to a long integer, upto 32 bits
    printf("Address for the variable b: %lu\n", (long)&b);
    printf("Address for the variable c: %lu \n", (long)&c);
    return 0;
}

Remember to compile the code fileName. is the name of the file you want to compile, and output to.

$ gcc fileName.c -o fileName.exe

Then run:

$ ./fileName.exe

Click for Expected Output

Address for the variable a: 140729126914124 
Address for the variable b: 140729126914128
Address for the variable c: 140729126914132 

So it looks like the address is almost next to each other, we call this contiguous.

Notes:

  • We must use format specifiers to tell the printf() how we would like our variables to be displayed.
  • So, % is the character for promising a specifier -> %lu means unsigned long and is 32 bits in size. memory address are never negative.

What would happen to the addresses if you ran the code again? Try it!

What are Pointers?

A pointer is a variable whose value is the address of another variable, i.e., the direct address of the memory location. Like any variable or constant, you must declare a pointer before using it to store any variable address. The general form of a pointer variable declaration is type var-name.

Here, type is the pointer's base type; it must be a valid C data type and var-name is the name of the pointer variable. The asterisk * used to declare a pointer is the same asterisk used for multiplication.

However, in this statement, the asterisk is being used to designate a variable as a pointer. Take a look at some of the valid pointer declarations:

int    *ip;    /* pointer to an integer */
double *dp;    /* pointer to a double */
float  *fp;    /* pointer to a float */
char   *ch     /* pointer to a character */

The actual data type of the value of all pointers, whether integer, float, character, or otherwise, is the same, a long hexadecimal number that represents a memory address. The only difference between pointers of different data types is the data type of the variable or constant that the pointer points to.

How to Use Pointers?

There are a few important operations, which we will do with the help of pointers very frequently. (a) We define a pointer variable, (b) assign the address of a variable to a pointer and (c) finally access the value at the address available in the pointer variable.

This is done by using the unary operator * that returns the value of the variable located at the address specified by its operand. The following example makes use of these operations.

Again create a new file and call it pointersTwo.c and edit the contents to match below:

#include <stdio.h>

int main () {

   int  a = 15;      /* actual variable declaration */
   int  *pointerToA; /* pointer variable declaration */

   pointerToA = &a;  /* store address of var in pointer variable*/

   printf("Address of A variable: %lu\n", (long)&a);

   /* address stored in pointer variable */
   printf("Address stored in pointerToA variable: %lu\n", (long)pointerToA);

   /* access the value using the pointer */
   printf("Value of *pointerToA variable: %d\n", *pointerToA);
   
   /* address of the pointer itself */
    printf("Address of pointerToA: %lu\n", (long)&pointerToA);

   return 0;
}

Run the code and you should see something similar to below, remember to compile first:


Click for Expected Output

Address of A variable: 140720967560444
Address stored in pointerToA variable: 140720967560444
Value of *pointerToA variable: 15
Address of pointerToA: 140720967560448

Null Pointers

It is always a good practice to assign a NULL value to a pointer variable in case you do not have an exact address to be assigned.

This is done at the time of variable declaration. A pointer that is assigned NULL is called a NULL pointer.

The NULL pointer is a constant with a value of zero defined in several standard libraries.

Create another new file called pointersThree.c, and enter the following:

#include <stdio.h>

int main () {

   int  *ptr = NULL;

   printf("The value of ptr is : %p\n", ptr  );
 
   return 0;
}

Compile and run.


Click for Expected Output

The value of ptr is : (nil)

In most operating systems, programs are not permitted to access memory at address 0 because that memory is reserved by the operating system.

However, the memory address 0 has special significance; it signals that the pointer is not intended to point to an accessible memory location. But by convention, if a pointer contains the null (zero) value, it is assumed to point to nothing.

To check for a null pointer, you can use an if statement as follows:

if(ptr)     /* succeeds if p is not null */
if(!ptr)    /* succeeds if p is null */

Pointers in Detail

Pointers have many but easy concepts and they are very important to C programming. The following important pointer concepts should be clear to any C programmer:

  1. Pointer arithmetic

    There are four arithmetic operators that can be used in pointers: ++, --, +, -

  2. Array of pointers

    You can define arrays to hold a number of pointers.

  3. Pointer to pointer

    C allows you to have a pointer on a pointer and so on.

  4. Passing pointers to functions in C

    Passing an argument by reference or by address enables the passed argument to be changed in the calling function by the called function.

  5. Return pointer from functions in C

    C allows a function to return a pointer to the local variable, static variable, and dynamically allocated memory as well.

Exploring the memory

Now we can explore memory in a more detailed way.

So one crucial thing to note here is that accessing memory locations and changing their values can be fatal for a system.

It is relatively simple to access memory addresses around your own entry point, let's assume you assign a variable called a and then you get the memory address. After you have this address you have a starting point to explore.

Create a new file called exploring memory.c, with the following code snippets, remember to include the #include<stdio.h> and int main(){return 0;} lines of code:

  1. Define and assign an integer with the value 10, which we are going to use for looping.

    int bin = 10;
    
  2. Create another integer and this time give it the value 123456789.

    int value = 123456789:
    
  3. Initialise a new variable in the pointer region of the code to point to the address of value

    int* pointer = (&value);
    
  4. Now you need to write of the for loop (just like C#) to return the address the pointer holds and the value at that address.

    for (int i = 0; i < bin; ++i)
    {   
    
    
    }
    
  5. Inside the for loop between the braces { } enter this line to print out the values to the console.

    printf(" %lu      ||    %d \t\t \n",(unsigned long)
    
    pointer,(unsigned int)*pointer);
    
  6. Finally, we need to take one off of the pointer's value thereby decreasing the address. Add the following directly on the line belowprintf();

    pointer = pointer - 1;
    

Click for Full Code

#include <stdio.h>
int main () {
    // define your variables in this region 
    int bin = 10;
    int value = 123456789;
    // end of variabl region
    
    // create a pointer here
    int* pointer = (&value);
    // end of pointer region
    
    printf("Memory Address        ||    Value        \n");
    printf("------------------------------------------\n");
    
    // put the for loop here
    for (int i = 0; i < bin; ++i)
    {   
        printf(" %lu      ||    %d \t\t \n",(unsigned long)pointer,(unsigned int)*pointer); 
        pointer = pointer-1;
    }
    // end of for loop
    return 0;
}

Remember to compile then run.


Click for Expected Output

Memory Address        ||    Value        
------------------------------------------
 140732118813652      ||    123456789 		 
 140732118813648      ||    0 		 
 140732118813644      ||    32582 		 
 140732118813640      ||    1938514379 		 
 140732118813636      ||    0 		 
 140732118813632      ||    2 		 
 140732118813628      ||    21928 		 
 140732118813624      ||    1008046784 		 
 140732118813620      ||    21928 		 
 140732118813616      ||    999510800 		 

Now that the script has executed you can see we have a list of 10 memory addresses and the values those address hold.

Again we see contiguous memory seperated by 4 byte address spaces.

We can see our the that on the first time the loop executes we get the memory address of our variable bin and the subsequent value of stored in the address 123456789.

However, we can also see that as the for loop continues looping through we get our list of memory addresses and values inside those memory addresses.

Arrays

So lets quickly look at arrays from a memory prespective.

Like C# the C programming language can store arrays of int, float and char. Unlike C# though we use arrays of chars to form a string.

Create a new file and call it exploringMemoryTwo.c and add the following code to it, compile and run:

#include<stdio.h>
#include<stdlib.h>
int main ()
{
    int n = 11, i;
    char ptr[11] = "hello world";
    
    printf ("\nPrinting elements of 1-D array: \n\n");
    for (i = 0; i < n; i++)
    {
        printf ("%c ", ptr[i]);
    }
    printf ("\n\nNow what is the memory location for each index and the array itself: \n\n");
    printf("      Memory Address (HEX)  ||  Element        Value\n");
    printf("----------------------------------------------------\n");
    
    for (i = 0; i < n; i++)
    {
        printf ("\t%p      ||   ptr[%d]    =    %c\n", &ptr[i],i,ptr[i]);
    }
    printf("----------------------------------------------------\n");
    printf("\t%p      ||   ptr[]     =  %c (this is the array's address too!) \n", &ptr,*ptr);
    
   
    return 0;
}

Click for Expected Output

Printing elements of 1-D array: 

h e l l o   w o r l d 

Now what is the memory location for each index and the array itself: 

    Memory Address (HEX)  ||  Element        Value
----------------------------------------------------
      0x7ffc2407ce9d      ||   ptr[0]    =    h
      0x7ffc2407ce9e      ||   ptr[1]    =    e
      0x7ffc2407ce9f      ||   ptr[2]    =    l
      0x7ffc2407cea0      ||   ptr[3]    =    l
      0x7ffc2407cea1      ||   ptr[4]    =    o
      0x7ffc2407cea2      ||   ptr[5]    =     
      0x7ffc2407cea3      ||   ptr[6]    =    w
      0x7ffc2407cea4      ||   ptr[7]    =    o
      0x7ffc2407cea5      ||   ptr[8]    =    r
      0x7ffc2407cea6      ||   ptr[9]    =    l
      0x7ffc2407cea7      ||   ptr[10]   =    d
----------------------------------------------------
0x7ffc2407ce9d      ||   ptr[]     =  h (this is the array's address too!) 

So you should be able to see that arrays are indeed Contiguous, and that that the starting memory address of the array is the same the zeroth element.


Dynamically Allocation of Memory

As you know, an array is a collection of a fixed number of values. Once the size of an array is declared, you cannot change it.

Sometimes the size of the array you declared may be insufficient. To solve this issue, you can allocate memory manually during run-time. This is known as dynamic memory allocation in C programming.

To allocate memory dynamically, library functions are malloc(), calloc(), realloc() and free() are used. These functions are defined in the <stdlib.h> header file.

malloc()

The name malloc stands for memory allocation.

The malloc() function reserves a block of memory of the specified number of bytes. And, it returns a pointer of void which can be casted into pointers of any form.

ptr = (castType*) malloc(size);

Example

ptr = (float*) malloc(100 * sizeof(float));

The above statement allocates 400 bytes of memory. It's because the size of float is 4 bytes. And, the pointer ptr holds the address of the first byte in the allocated memory.

The expression results in a NULL pointer if the memory cannot be allocated.

calloc()

The name calloc stands for contiguous allocation.

The malloc() function allocates memory and leaves the memory uninitialised, whereas the calloc() function allocates memory and initializes all bits to zero.

ptr = (castType*)calloc(n, size);

Example:

ptr = (float*) calloc(25, sizeof(float));

The above statement allocates contiguous space in memory for 25 elements of type float.

the alt

C free()

Dynamically allocated memory created with either calloc() or malloc() doesn't get freed on their own. You must explicitly use free() to release the space.

free(ptr);

This statement frees the space allocated in the memory pointed by ptr.

Example 1:

You need to create a new file and call it sumofnumbers_malloc.c.

Once done you need to reproduce the following code that dynamically allocates the memory for n number of int using malloc() and free():

// Program to calculate the sum of n numbers entered by the user

#include <stdio.h>
#include <stdlib.h>

int main() {
  int n, i, *ptr, sum = 0;

  printf("Enter number of elements: ");
  scanf("%d", &n);

  ptr = (int*) malloc(n * sizeof(int));
 
  // if memory cannot be allocated
  if(ptr == NULL) {
    printf("Error! memory not allocated.");
    exit(0);
  }

  printf("Enter elements: ");
  for(i = 0; i < n; ++i) {
    scanf("%d", ptr + i);
    sum += *(ptr + i);
  }

  printf("Sum = %d", sum);
  
  // deallocating the memory
  free(ptr);

  return 0;
}

Now, compile the code:

$ gcc sumofnumbers_malloc.c -o sumofnumbers_malloc.exe

Run:

$ ./sumofnumbers_malloc.exe

Enter number of elements: 3
Enter elements: 100
20
36
Sum = 156

Example 2:

You need to create a new file and call it sumofnumbers_calloc.c.

Once done you need to reproduce the following code that dynamically allocates the memory for n number of int using calloc() and free():

// Program to calculate the sum of n numbers entered by the user

#include <stdio.h>
#include <stdlib.h>

int main() {
  int n, i, *ptr, sum = 0;
  printf("Enter number of elements: ");
  scanf("%d", &n);

  ptr = (int*) calloc(n, sizeof(int));
  if(ptr == NULL) {
    printf("Error! memory not allocated.");
    exit(0);
  }

  printf("Enter elements: ");
  for(i = 0; i < n; ++i) {
    scanf("%d", ptr + i);
    sum += *(ptr + i);
  }

  printf("Sum = %d", sum);
  free(ptr);
  return 0;
}

Now, compile the code:

$ gcc sumofnumbers_calloc.c -o sumofnumbers_calloc.exe

Run:

$ ./sumofnumbers_calloc.exe

Enter number of elements: 3
Enter elements: 100
20
36
Sum = 156

C realloc()

If the dynamically allocated memory is insufficient or more than required, you can change the size of previously allocated memory using the realloc() function.

ptr = realloc(ptr, x);

Here, ptr is reallocated with a new size x.

Example 3:

You need to create a new file and call it realloc.c.

Once done you need to reproduce the following code that dynamically allocates the memory for n number of int using malloc(), realloc() and free():

#include <stdio.h>
#include <stdlib.h>

int main() {
  int *ptr, i , n1, n2;
  printf("Enter size: ");
  scanf("%d", &n1);

  ptr = (int*) malloc(n1 * sizeof(int));

  printf("Addresses of previously allocated memory:\n");
  for(i = 0; i < n1; ++i)
    printf("%pc\n",ptr + i);

  printf("\nEnter the new size: ");
  scanf("%d", &n2);

  // rellocating the memory
  ptr = realloc(ptr, n2 * sizeof(int));

  printf("Addresses of newly allocated memory:\n");
  for(i = 0; i < n2; ++i)
    printf("%pc\n", ptr + i);
  
  free(ptr);

  return 0;
}

Now, compile the code:

$ gcc realloc.c -o realloc.exe

Run:

$ ./realloc.exe

Enter size: 2
Addresses of previously allocated memory:
26855472
26855476

Enter the new size: 4
Addresses of newly allocated memory:
26855472
26855476
26855480
26855484

Stack, Heap and Static

In addition to the lecture notes this morning here are more info about those cocepts.

Stack Memory

  • Description: Stack memory is a region of memory that is used for storing local variables and function call information. It operates in a Last-In-First-Out (LIFO) manner, and memory is automatically allocated and deallocated as functions are called and return.

  • Definition: Stack memory is a type of memory that stores local variables and function call information. It follows a Last-In-First-Out (LIFO) structure, and memory is automatically managed during function calls.

Heap Memory:

  • Description: Heap memory is a dynamic memory allocation area where memory is allocated and deallocated manually using functions like malloc and free. It allows for more flexible memory management but requires explicit memory cleanup.

  • Definition: Heap memory is a dynamic memory allocation area where memory is manually allocated and deallocated using functions such as malloc and free. It provides flexibility in memory management but requires explicit cleanup.

Static Memory:

  • Description: Static memory refers to memory allocated for variables that exist throughout the program's lifetime. It is typically used for global variables and static variables inside functions. Memory is allocated at compile-time and persists throughout the program's execution.

  • Definition: Static memory is memory allocated for variables that have a fixed lifetime throughout the program. It includes global variables and static variables inside functions. Memory is allocated at compile-time and persists during the program's execution.

Task

Create new file with Mem_Types.c, and write the following code:

  • Sack Memory (stackMemory function):

    • Declares and prints a simple integer variable (stackVariable) on the stack.
    • Demonstrates automatic memory management within the function's scope.
  • Heap Memory (heapMemory function):

    • Dynamically allocates memory for an integer array (heapArray) on the heap.
    • Initializes and prints the array, showcasing dynamic memory allocation.
    • Manually frees the allocated memory to prevent leaks.
  • Static Memory (staticMemory function):

    • Declares and prints a static integer array (staticArray) with five elements.
    • Demonstrates static memory allocation with a persistent lifetime.
  • Main Function (main function):

    • Calls each memory example function to showcase stack, heap, and static memory.
    • Highlights the unique features of each memory type.
    • Compile and run the program to observe the outputs for different memory types.
#include <stdio.h>
#include <stdlib.h>

void stackMemory() {
    // Stack memory example
    int stackVariable = 10;
    printf("Stack Variable: %d\n", stackVariable);
    // The stack variable exists only within this function's scope
}

//---------------

void heapMemory() {
    // Heap memory example
    int *heapArray;
    int size;

    printf("Enter the size of the array: ");
    scanf("%d", &size);

    // Dynamically allocate memory for an array on the heap
    heapArray = (int*)malloc(size * sizeof(int));

    if (heapArray == NULL) {
        printf("Memory allocation failed\n");
        return;
    }

    // Initialize the array
    for (int i = 0; i < size; i++) {
        heapArray[i] = i * 2;
    }

    // Print the original array
    printf("Original Heap Array: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", heapArray[i]);
    }
    printf("\n");

    // Don't forget to free the allocated memory when done
    free(heapArray);
}
//---------------

void staticMemory() {
    // Static memory example
    static int staticArray[5] = {1, 2, 3, 4, 5};

    // Print the static array
    printf("Static Array: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", staticArray[i]);
    }
    printf("\n");
}
//---------------

int main() {
    // Stack memory example
    stackMemory();

    // Heap memory example
    heapMemory();

    // Static memory example
    staticMemory();

    return 0;
}

By working through the examples and understanding the behaviors of variables in different memory regions, you should be better equipped to make informed decisions about memory allocation and deallocation in C programs.

Exercise:

While calloc and malloc are both dynamic memory allocation functions in C, they serve distinct purposes. The primary benefit of using calloc over malloc lies in its ability to initialise the allocated memory to zero. This can be advantageous when dealing with arrays or structures that need to be zero-initialised, ensuring a consistent and predictable state.

However, it's essential to note that while calloc provides this initialization advantage, it may not be as time-efficient as malloc.

Task:

Explore the trade-off between zero-initialisation benefits and time efficiency when using calloc compared to malloc in C programming. Implement a program, measure execution times (Hint: timestamps)(perhaps for allocating memory for an array type double with size of 1,000,000 items, or maybe more), and draw conclusions based on the collected data.