Allocating Memory at Runtime

Dynamic Memory Allocation

Up until now, all memory allocation has been static or automatic The two primary functions required for dynamic memory management are malloc and free.
void *malloc(size_t size); /* Allocate a block of memory   */
void free(void *pointer);  /* Deallocate a block of memory */
To use malloc and free:
#include <stdlib.h> /* malloc, free */
The argument to malloc is the number of bytes to allocate:
char *pc = malloc(10); /* allocate memory for 10 chars */
int *pi = malloc(40);  /* allocate memory for 10 ints  */
10 chars (10 bytes)10 ints (40 bytes)


The stack vs. the heap:

Notice that there is no type information associated with malloc, so the return from malloc may need to be cast to the correct type:
  /* Casting the return from malloc to the proper type */
char *pc = (char *) malloc(10); /* allocate memory for 10 chars */
int *pi = (int *) malloc(40);   /* allocate memory for 10 ints  */

Note: The original version of malloc (from K&R C) returned a char * and so the returned pointed had to be cast to the correct type. Newer ANSI C compilers have malloc returning a void * pointer so the cast is not necessary in C. The cast is required in C++, however.

You should never hard-code the size of the data types, since they may change. Do this instead:
  /* Proper memory allocation for 10 chars */
char *pc = (char *) malloc(10 * sizeof(char)); 

  /* Proper memory allocation for 10 ints  */
int *pi = (int *) malloc(10 * sizeof(int));
If the allocation fails, NULL is returned so you should check the pointer after calling malloc.
  /* Allocate some memory for a string */
char *pc = (char *) malloc(10 * sizeof(char));

  /* If the memory allocation was successful */
if (pc != NULL)
{
  strcpy(pc, "Digipen"); /* Copy some text into the memory */
  printf("%s\n", pc);    /* Print out the text             */
  free(pc);              /* Release the memory             */
}
else
  printf("Memory allocation failed!\n");
After allocationAfter strcpy


Notes: Examples:

int main(void)
{
  int SIZE = 10;
  int *pi;

    /* allocate memory */
  pi = (int *)malloc(SIZE * sizeof(int));

    /* check for valid pointer */
  if (!pi)
  {
    printf("Failed to allocate memory.\n");
    return -1;
  }

  /* do stuff */

    /* free memory */
  free(pi);
  
  return 0;
}
Accessing the allocated block:
void test_malloc(void)
{
  int SIZE = 10;
  int i, *pi;

    /* allocate memory */
  pi = (int *)malloc(SIZE * sizeof(int));

    /* check for valid pointer */
  if (!pi)
  {
    printf("Failed to allocate memory.\n");
    return;
  }


    /* using pointer notation */
  for (i = 0; i < SIZE; i++)
    *(pi + i) = i;

    /* using subscripting */
  for (i = 0; i < SIZE; i++)
    pi[i] = i;

  for (i = 0; i < SIZE; i++)
    printf("%i \n", *(pi + i));

    /* free memory */
  free(pi);
}

Note: By now it should be clear why we learned that pointers can be used to access array elements. With dynamic memory allocation, there are no named arrays, just pointers to contiguous (array-like) memory and pointers must be used. Of course, pointers can be subscripted just like arrays.

Dynamically Allocated Structures

Revisiting our FILEINFO example:
#define MAX_PATH 12
struct DATE 
{
  int month;
  int day;
  int year;
};
struct TIME 
{
  int hours;
  int minutes;
  int seconds;
};
struct DATETIME 
{
  struct DATE date;
  struct TIME time;
};
struct FILEINFO 
{
  int length;
  char name[MAX_PATH];
  struct DATETIME dt;
};
Function to print a single FILEINFO structure:
void PrintFileInfo(const struct FILEINFO *fi)
{
  printf("Name: %s\n", fi->name);
  printf("Size: %i\n", fi->length);
  printf("Time: %2i:%02i:%02i\n", fi->dt.time.hours, fi->dt.time.minutes, fi->dt.time.seconds);
  printf("Date: %i/%i/%i\n", fi->dt.date.month, fi->dt.date.day, fi->dt.date.year);
}

Dynamically allocate a FILEINFO structure and print it:

void f14(void)
{
    /* Pointer to a FILEINFO struct (The 1 is redundant but instructive) */
  struct FILEINFO *pfi = (struct FILEINFO *)malloc(1 * sizeof(struct FILEINFO));

    /* Check that the allocation succeeded */
    /* Set the fields of the struct ....   */

  PrintFileInfo(pfi); /* Print the fields */
  free(pfi);          /* Free the memory  */
}
View of memory after allocation: (32-bit system)
The stack vs. the heap:


Function to print an array of FILEINFO structures:
void PrintFileInfos(const struct FILEINFO *records, int count)
{
  int i;
  for (i = 0; i < count; i++)
    PrintFileInfo(records++); /* Code reuse! */
}
Remember, for function parameters, we could have written the function like this:
  /* Use array notation instead of pointer notation */
void PrintFileInfos(const struct FILEINFO records[], int count)
{
  . . .
}

#include <assert.h> /* assert */

#define SIZE 10
#define MAX_PATH 12

void TestHeapStruct(void)
{
  int i;                  /* Loop counter               */
  struct FILEINFO *pfi;   /* For the "array" of structs */
  struct FILEINFO *saved; /* Need to remember address   */

    /* Allocate and initialize all fields of all structs to 0 */
  pfi = (struct FILEINFO *)calloc(SIZE, sizeof(struct FILEINFO));
    
  assert(pfi != NULL); /* Check that it was successful */
  saved = pfi;         /* Save pointer for later...    */

    /* Set the date and name of each structure */
  for (i = 0; i < SIZE; i++)
  {
    char name[MAX_PATH]; /* Declare in scope where used. */

      /* Format dates from 12/1/2020 through 12/10/2020 */
    pfi->dt.date.month = 12;
    pfi->dt.date.day = i + 1;
    pfi->dt.date.year = 2020;

      /* Format and store the filenames (foo-1.txt through foo-10.txt) */
    sprintf(name, "foo-%i.txt", i + 1);
    strcpy(pfi->name, name); 

      /* Point to next FILEINFO struct in the array */
    pfi++;
  }

    /* Reset pointer to beginning */
  pfi = saved;

    /* Print info */
  PrintFileInfos(pfi, SIZE);

    /* Release the memory */
  free(pfi);
}
     Output:
Name: foo-1.txt
Size: 0
Time:  0:00:00
Date: 12/1/2020
Name: foo-2.txt
Size: 0
Time:  0:00:00
Date: 12/2/2020
Name: foo-3.txt
Size: 0
Time:  0:00:00
Date: 12/3/2020
Name: foo-4.txt
Size: 0
Time:  0:00:00
Date: 12/4/2020
Name: foo-5.txt
Size: 0
Time:  0:00:00
Date: 12/5/2020
Name: foo-6.txt
Size: 0
Time:  0:00:00
Date: 12/6/2020
Name: foo-7.txt
Size: 0
Time:  0:00:00
Date: 12/7/2020
Name: foo-8.txt
Size: 0
Time:  0:00:00
Date: 12/8/2020
Name: foo-9.txt
Size: 0
Time:  0:00:00
Date: 12/9/2020
Name: foo-10.txt
Size: 0
Time:  0:00:00
Date: 12/10/2020	

Quick pointer arithmetic review.

Note:

A Brief Look At realloc

What happens if the block of memory that was allocated was too small? This is a problem that happens often. Consider this code:
void f0(void)
{
  char *p1 = malloc(1000);
  char *p2;

  /* put some values into the memory  */

  /* oops, should have allocated more */

  p2 = malloc(2000);            /* allocate a bigger block */
  memcpy(p2, p1, sizeof(char)); /* copy over existing data */
  free(p1);                     /* free the old memory     */

  /* do something with new memory */

  free(p2);
}
This is such a common occurrence that there is a function in the library (realloc) to handle this situation. The realloc function can extend (or shrink) the size of a previously-allocated block.

Here is the prototype:

void *realloc(void *ptr, size_t size);
On its surface, that sounds great. However, in practice, it generally doesn't work when growing the block. I'm showing you this so that, sometime in the future, you may find that you need this possible behavior. Examples are worth a 1000 words.

Note: I've purposely left out the calls to free in the code below. Don't do that in real code! (Also, the addresses will change with each run.)

Example 1 (grow):
void f1(void)
{
  char *p1 = malloc(1000);
  char *p2;

  printf("%p\n", (void *)p1); /* 0x1c78010        */
  p2 = realloc(p1, 2000);     /* grow the block   */
  printf("%p\n", (void *)p2); /* 0x1c78010 (same) */
}
Example 2 (grow):
void f2(void)
{
  char *p1 = malloc(1000);
  char *p2;

  printf("%p\n", (void *)p1); /* 0x1da5010                */
  malloc(10);                 /* any arbitrary allocation */
  p2 = realloc(p1, 2000);     /* grow the block           */
  printf("%p\n", (void *)p2); /* 0x1da5420 (different)    */
}
Example 3 (shrink):
void f3(void)
{
  char *p1 = malloc(1000);
  char *p2;

  printf("%p\n", (void *)p1); /* 0x122a010                */
  malloc(10);                 /* any arbitrary allocation */
  p2 = realloc(p1, 200);      /* shrink the block         */
  printf("%p\n", (void *)p2); /* 0x122a010 (same)         */
}
Example 4 (grow):
void f4(void)
{
  char *p1 = malloc(1000);
  char *p2;

  printf("%p\n", (void *)p1); /* 0x92b010                  */
  strdup("hello");            /* call arbitrary function   */
  p2 = realloc(p1, 2000);     /* grow the block            */
  printf("%p\n", (void *)p2); /* 0x92b420 (different!)     */
}
Example 5 (shrink/grow):
void f5(void)
{
  char *p1 = malloc(1000);
  char *p2;

  printf("%p\n", (void *)p1); /* 0x12c8010                */
  malloc(10);                 /* any arbitrary allocation */
  p2 = realloc(p1, 200);      /* shrink the block         */
  printf("%p\n", (void *)p2); /* 0x12c8010 (same)         */
  p2 = realloc(p1, 1000);     /* grow the block           */
  printf("%p\n", (void *)p2); /* 0x12c8010 (same)         */
  p2 = realloc(p1, 200);      /* shrink the block         */
  printf("%p\n", (void *)p2); /* 0x12c8010 (same)         */
  p2 = realloc(p1, 1100);     /* grow the block           */
  printf("%p\n", (void *)p2); /* 0x12c8420 (different)    */
}
Output from various systems:
mayarebeccapiWindows 10
0x55c4d53fd2a0
0x55c4d53fd2a0
0x55c4d53fdac0
0x55c4d53fd2a0
free(): double free detected in tcache 2
Aborted (core dumped)
0x7fee07c02a20
0x7fee07c02a20
0x7fee07c02a20
0x7fee07c02a20
0x7fee08000000
0xcff150
0xcff150
0xcff150
0xcff150
0xcff958
00000000001B7600
00000000001B7600
00000000001B2260
(silently crashed)
Using separate variables:
char *p1 = malloc(1000);
char *p2, *p3, *p4, *p5;

printf("%p\n", (void *)p1); 
malloc(10);                 /* any arbitrary allocation */
p2 = realloc(p1, 200);      /* shrink the block         */
printf("%p\n", (void *)p2); 
p3 = realloc(p2, 1000);     /* grow the block           */
printf("%p\n", (void *)p3); 
p4 = realloc(p3, 200);      /* shrink the block         */
printf("%p\n", (void *)p4); 
p5 = realloc(p4, 1100);     /* grow the block           */
printf("%p\n", (void *)p5); 
Output:
mayaWindows 10
0x55b231e5c2a0
0x55b231e5c2a0
0x55b231e5cac0
0x55b231e5cac0
0x55b231e5ceb0
0000000000027600
0000000000027600
0000000000022280
0000000000022280
0000000000022280

The reason I wanted to show this is because beginning programmers often look at the realloc function without really understanding how it works and think that it's a panacea for growing (extending) a block of memory. It isn't, but it does have it's uses, if you know what to expect.

Summary

Summary for malloc/calloc Summary for free Other issues: