ITP Week 9
ITP Week 9
In this reading, you will learn about dynamically allocating memory and dynamic memory management.
You will also learn about two different memory partitions in C, the stack, and the heap. Then, you will gain
insight into the different functions provided in the C libraries for dynamic memory management. Finally,
you will be introduced to the differences between static and dynamic arrays and how to return arrays from
functions.
Let us say we have written a C program P1. You would typically save it as a .c file, say P1.c. This file resides
on your disk. When you compile the program with a compiler such as gcc, the executable file is created and
stored on the disk, say P1.exe. Now, when you try to execute this file, the OS first loads the executable file
into the RAM, and then the processor executes the program sequentially. This is illustrated in Figure 1.
Now, the memory allocated to the program on the RAM consists of various sections. These are partitioned
in the form of segments. A C program executing in memory has four segments, i.e., Stack, Heap, Data, and
Text, as illustrated in Figure 2.
The stack segment is the place where all the auto (or local) variables in our functions are allocated memory.
Each function call gets allocated a frame and this frame contains all the local variables of that function.
When the function reaches the end of its execution and the flow returns to the calling function, the frame is
popped off the stack. In other words, the frame is destroyed, and the space occupied by it is released to the
Operating System to be used for other purposes.
A stack can be thought of as a stack of files that need to be attended to. Each new function call places a new
file on the stack and the height of the stack grows. After the file is attended to by the concerned individual
(or function executed by the CPU), the file is removed from the stack releasing space on the pile. At this
point, the CPU does not remember the contents of the popped file (variables of the popped function call
frame) anymore. They are lost.
When the above program starts to execute, a frame gets allocated in the stack for the main() function. This
is illustrated in Figure 3. This frame contains the local variable b. When the call to f1() is made, a frame gets
allocated for f1(). This frame contains, among other information, the local variable a. After f1() completes
its execution, when the control goes back to main, the frame for f1() is popped off the stack. When main()
ends, the frame for main() is also popped.
Figure 3. Illustration of the stack frames being allocated and popped as the program is executed
Heap Memory
Heap refers to that portion of memory that is dynamically and explicitly allocated by the programmer. Here,
since the compiler does not allocate memory by itself, the responsibility of freeing up the allocated memory
also lies with the user. We shall learn how to allocate this memory in the sections ahead. This memory is
accessible globally to all functions. For example, if you dynamically allocate 10 bytes of memory inside f1()
in the above code, that memory will not be freed up when the call stack for f1() is popped. It will persist
until the end of the program execution unless freed explicitly.
We saw that when we declare a local variable in a function, it is lost when the stack frame is popped.
However, if needed, it can be returned to the calling statement via the return statement. Now, consider the
case of arrays. If the array is declared in a function, and we try to return it, it would be returned by reference.
Thus, the calling function would receive a pointer to a location that is no longer reserved for the array and
is now actually free. These location contents can be filled by some other data unpredictably and would lead
to unpredictable behavior.
Another issue with arrays is that their size cannot be altered at run-time. So, we tend to oversize arrays to
accommodate our needs. This has two issues. First, it is not memory efficient as we would ideally want to
allocate exactly the amount of memory that we use. Second, it may be difficult to exactly estimate how
much oversizing would be sufficient. These and many more challenges can be solved by being able to
dynamically allocate and deallocate memory. Here, the term dynamic means the allocation is being done
at run-time, as opposed to static allocation where the memory requirements are known at compile-time.
Dynamic Memory Management provides immense flexibility to the programmer to allocate and use memory
as they wish. However, this comes with the caveat that the programmer needs to ensure that unused
allocated memory is also freed on time. C provides many functions to achieve Dynamic Memory
Management such as malloc(), calloc(), realloc(), and free(). We shall now see the working of these
functions in detail. These functions are included in the stdlib.h header file.
malloc()
malloc() is a function that accepts an integer number of bytes, allocates that amount of memory, and
returns a void pointer to the memory block of the required size. This allocated memory is present in the
heap.
The void pointer is a special type of pointer, which is used since the function does not know the type of data
that would be stored in the allocated memory. So, it is kept as void * by default and can be type-cast into
any type of pointer as per the need. This returned value would typically be stored in a local pointer so that
the newly allocated memory is accessible.
Here, we have requested malloc to allocate sizeof(int) number of bytes. The returned pointer (of type void
*) is cast into a pointer to an int (int *) and assigned to the int pointer pt.
Note that if malloc is not able to allocate the required amount of memory, it allocates a NULL pointer. Thus,
it is a good practice to always check the pointer returned by malloc() against a NULL pointer to check
whether the allocation was successful.
The allocated memory need not always be exactly the size of the data type that would be stored there. We
can request an integer multiple of it to simulate an array as well.
Here, since the requested amount of memory is small(10 integers), the OS would be able to fulfill the
requirement almost always. However, if we modify it to request very large amounts of memory, malloc may
fail to fulfill the request.
Thus, it is always recommended to check for the possibility of NULL pointers before dereferencing and
using the dynamically allocated memory.
free()
free() is a function that is used to free a dynamically allocated block of memory. It accepts a single argument,
namely an address pointing to a dynamically allocated block (i.e., a block allocated using malloc, calloc(),
or realloc(). If a block is not freed, that memory becomes blocked for the duration of the execution of the
program. Failure to free can result in heap depletion. This increases the chances of malloc() or calloc()
returning NULL signifying unsuccessful allocation. It is a good practice to always free blocks that are no
longer needed.
calloc()
In the above function, we allocated 10 integers by passing the request size as 10*sizeof(int) to malloc. This
calculation can be cumbersome at times. Also, at this point, the block is filled with garbage(random values).
So, a function calloc() is provided that encapsulates the size calculation and zero initialization while
allocating arrays. Its syntax is:
That is, we pass in the number of elements required and the size of a single element to calloc() while
allocating a block corresponding to an array of elements. calloc() allocates the block, explicitly sets the
elements of the allocated block to be all zeroes, and returns the required pointer.
realloc()
The function realloc() is used when we need to resize a block that has been previously allocated. For
example, consider the following code snippet:
Here, ptr was initially allocated a size equivalent to 5 integers. If, at a later point in time, we wish to increase
the size to 10 integers, we can use realloc() as shown above. realloc() accepts a dynamically allocated
address and a size in bytes. It allocates a block of the new size, copies the contents pointed in the old block
to the new block, and then frees the old block.
Thus, we can see that the above functions can be used to allocate and deallocate memory dynamically and
provide tremendous flexibility to the users.
Now, we can revisit the drawbacks of (static) arrays that we started the section on Dynamic Memory
Allocation with. We now know how to use both static and dynamic arrays. We have also seen the similarities
and differences between them. The following table encapsulates the differences between them:
We talked about the issue of returning arrays earlier. When we want to return static arrays from functions,
we are unable to do so since they are passed back by reference. If the array was declared in the called
function and returned to the calling function, the reference location returned no longer belongs to the array
on the stack. Thus, the returned address is of no use.
So, this issue can be solved in two ways. The first approach is to pass an empty array to the function,
populate it within the function, and return the populated array to the calling function. Consider the
following example:
Here, in main(), an array a is declared. We want to define a function copy() that copies the contents of a
into a new array b. We achieve it by passing a new empty array of the same size (here b) as well to copy().
copy() copies the contents of a to b and does not return anything. Since arrays are passed by reference,
this has the effect of actually copying a into b. Thus, functions can effectively "return" arrays by accepting
the array to be returned as an additional parameter and mutating it. Here, the scope is the calling function
and not the called function. So, the array will not be destroyed at return.
Another approach would be to dynamically allocate the array on the heap in the called function and, thus,
solve the issue of the array being present on the popped-off stack frames. For example, consider the
following program:
Here, the function copy() accepts only the old array a and its length n as parameters. The new array b is
allocated dynamically inside copy() using malloc(). It is populated with the values of a and is returned as a
pointer to main(). In this way, we are able to achieve behavior similar to "returning arrays", by returning
pointers to dynamically allocated blocks.
Thus, we can see that both approaches of passing in empty arrays as well as dynamically allocating the new
array in the called function can be used to achieve the required functionality of returning an array from a
function. We prefer either approach based on the constraints and needs of the problem required to be
solved.
Another issue that we talked about was the inability to resize static arrays. We can see this issue gets
resolved in dynamic arrays. Assume we have allocated an array of 10 int values using malloc() (or calloc()).
Now, assuming that we now require to store 5 more int variables in the array, we can simply call realloc()
on the old array with the new size and achieve the required resizing functionality.
Thus, we now have the ability to resize dynamic arrays, which we did not have with static arrays.
Reading Summary:
• How to dynamically allocate and deallocate memory using malloc, calloc, realloc, and free
Recall calloc() is a function that allocates memory and initializes it to zero. It is defined in stdlib.h. The
function takes two arguments: the number of elements and the size of each element.
Implement the function my_calloc() that mimics the behavior of calloc(). You may use malloc() and free() in
your implementation, but you should not use calloc() or realloc(). The prototype of the function is: void
*my_calloc(int num, int size); It should return a pointer to void. The function should return NULL if the
allocation fails.
For example, my_calloc(2,3) would return a block of 6 bytes initialized to all zeroes. my_calloc(8, sizeof(int))
would return a block that can hold 8 int values, initialized to all 0s.
Write a program to read n floating point numbers using malloc and find their average. Read the number of
elements from the user. Use scanf() to read the elements. You should allocate memory for the array using
malloc. Now, calculate the average of the elements and print it. Use free() to free the memory allocated for
the array.
Testcase1
Input:
Output:
Average = 3.4.
More Dynamic Memory Allocation
Topic: Dynamically Allocated Structures and 2-D Arrays
Reading Objective:
In this reading, you will learn about dynamically allocated structures and 2-D arrays. These constructs follow logically from
dynamic memory allocation of primitive types and 1-D arrays.
We have seen that we can allocate variables and arrays of primitive types dynamically using the malloc() function. We can
do the same for structures using exactly the same syntax.
The above code allocates a single structure variable dynamically. The malloc() function returns a void pointer, which we
cast to a pointer to a structure of type struct student.
Similarly, we can allocate an array of structures dynamically using the following syntax:
In order to free the memory allocated for a dynamically-allocated structure, we use the free() function. For example,
The above code frees the memory allocated for the structure variable s and the array s_arr. It works for both isolated
structures and arrays of structures.
Let us put together the above lines of code and view it as a complete program. We write a program to dynamically allocate
an array of 10 students, take the input for the records of the students from the user, and print the details of the student
with the highest CGPA.
Here, we first define the struct student with all of its data members. Then, we dynamically allocate an array of 10 students
using malloc() and store the allocated block’s address in a pointer variable studArray. We then take in input for the records
of the students in the first while loop. After that, we declare a temporary struct student variable temp to store a copy of
the student with the greatest CGPA. Now, we linearly search for the student with the greatest CGPA in our studArray using
a while loop. Whenever we encounter a student with CGPA greater than the student currently stored in temp, we
update temp to now store a copy of the new student. In this way, at the end of the loop, temp now stores a copy of the
required student. Now, we print the roll no. of the required student using a printf() call. In this way, we can use dynamically
allocated arrays of structures to write a diverse range of real-world programs.
2-D Arrays
We have seen that we can allocate memory for 1-D arrays dynamically using the malloc() function. We can do the same for
2-D arrays using exactly the same syntax.
A 2-D array is a 1-D array of 1-D arrays. So, when using pointers, they are a pointer to a pointer. For example, consider the
following code snippet:
The above code block allocates a 2-D array a of 3 rows. Each row is a pointer to an integer. We can allocate memory for
each row using the following syntax:
The above code block allocates memory for each row of the 2-D array. The first row is pointed to by a[0], the second row is
pointed to by a[1], and so on. The above code allocates memory for 2 integers in each row. Figure 1 illustrates the resultant
structure of the above code snippets.
Figure 1. 2D array
Now, we can also create a jagged array wherein each row is of different lengths. This can be done with the following code
snippet in place of the previous allocation in the loop:
Figure 2 illustrates the jagged array created. Note that this kind of jagged array was not possible using purely static
multidimensional arrays.
In order to free the memory allocated for a dynamically allocated 2-D array, we use the free() function. Again, we first need
to free the memory allocated for each row and then free the memory allocated for the array itself. The following code
illustrates this:
Thus, we are now able to use dynamic memory allocation with a variety of data types and constructs, allowing us immense
flexibility in writing complex programs. The arrays thus created can be used, accessed, modified, etc., in the same way as
we covered for static arrays and static arrays of structures. The syntax is exactly the same. The only difference is in their
creation and the memory segment in which they are stored.
Reading Summary:
• How to allocate and free memory for dynamically allocated structures and 2-D arrays
Practice Lab 2 - More Dynamic Memory AllocationAssignment Brief
Practice Lab 2_Question_1
Imagine you are running a pizza shop. Now, pizza is a struct that contains
- base - the base of the pizza. It is an int that represents the index of the base in the global array of bases. For
example, if the base is "Thin", then the index is 0.
- topping - an array of toppings of length NUM_TOPPINGS. Each element is 0 or 1 depending on whether the
topping is present or not. For example, if the toppings are "Tomatoes" and "Olives", then the array is [0, 1,
0, 0, 1]
Every time you receive a new order, you need to instantiate a struct order that contains: -
• The topping price is the sum of the prices of all the toppings
• The total price is the sum of the base price and the topping price
• Create a new order: // allocate memory for a new order, instantiate its members, return pointer to order.
order *new_order(int order_id, char *customer_name, char *customer_phone, pizza *pizzas,int num_pizzas);
• Create a new pizza: // allocate memory for a new pizza, instantiate its members, return pointer to pizza.
• Complete order: // This function should free the memory allocated to the order
• Destroy pizza: // This function should free the memory allocated to the pizza
This flexibility comes at a price—the responsibility to free up the allocated memory when it is no
longer required. So far, memory deallocation has not concerned us because the system has a well-
defined mechanism for deallocating memory for variables defined inside and outside a function.
A function on return frees memory used by its local variables, while global and static variables are
automatically deallocated on program termination. For memory allocated dynamically, we must do
this job ourselves. If we fail to behave responsibly, there can be serious runtime problems resulting
from memory leaks.
When memory is allocated at runtime, its size is usually determined either by user input or an
expression evaluated by the running program. This feature is exploited by a special data structure
called a linked list. Unlike an array, a linked list cannot be defined with a predetermined size—
either at compile time or runtime. When an item is added to the list, memory is created for that
item at that moment and a link (in the form of a pointer) is established between this new item and
its immediate predecessor in the list.
In this chapter, we’ll first examine the library functions that dynamically allocate memory and one
function that frees it. Using these functions, we’ll develop the techniques needed to create, modify
and query a linked list that can be expanded and contracted at will.
Stack
Heap
Data
Program Code
The heap is used for dynamically allocating memory. It grows at the expense of the stack and vice
versa. A block of memory that is freed using the free function is returned to the heap. Failure to
free memory that is no longer required can result in severe heap depletion that may make further
allocation impossible. The memory allocation functions return NULL if they fail to find the
specified amount of memory as a contiguous chunk in the heap. Every time we use these functions,
we must thus check for NULL.
Note: Dynamic memory allocation creates a chunk of contiguous memory. A pointer to this chunk
can be treated as an array except that the latter is a constant pointer unlike a regular pointer which
can be made to point to this block.
We begin our discussions on the individual functions with malloc which is most commonly used
for dynamic memory allocation. malloc accepts the size of the requested memory in bytes as an
argument and returns a generic pointer to the base address of the allocated block. As shown in the
following, this generic pointer is suitably aligned to a pointer variable of a specific data type before
the memory chunk can be accessed:
int *p;
p = malloc(4); No cast needed
malloc allocates a chunk of four bytes whose pointer is implicitly converted to int * on assignment.
The allocated space can now be used to store a four-byte int, but the portable and correct technique
would be to use sizeof(int) as the argument to malloc:
p = malloc(sizeof(int));
malloc doesn’t initialize the allocated block, so each byte has a random value. (This is not the case
with calloc.) However, we can dereference the pointer p to assign a value to this block:
*p = 5; Assigns value to unnamed memory segment
Observe that the allocated block can’t be treated as a variable even though it can be manipulated by
its pointer. You must not lose this pointer because it represents the only mechanism that provides
access to the block.. Further, this pointer is also used by the free function to deallocate the block:
free(p); Deallocates memory allocated by malloc
The pointer returned by malloc is also used by the realloc function for resizing the allocated block,
if considered necessary. Both realloc and free are examined soon.
524 Computer Fundamentals & C Programming
#include <stdio.h>
#include <stdlib.h> /* Needed by malloc, calloc, realloc and free */
int main(void)
{
short *p1;
int *p2;
float *p3;
p1 = malloc(2); /* Non-portable; not recommended */
p2 = malloc(sizeof(int)); /* The right way */
p3 = malloc(sizeof(float)); /* Ditto */
*p1 = 256; *p3 = 123.456;
printf(“*p1 = %hd\n”, *p1);
printf(“*p2 = %d\n”, *p2); /* Prints uninitialized value */
printf(“*p3 = %f\n”, *p3);
exit(0); /* Frees all allocated blocks */
}
When the total amount of dynamic memory required by a program exceeds the amount available
on the heap, a program must recycle memory. At any instant, adequate memory must be available
for the next allocation. For small programs that eventually terminate, this is not an issue as the
allocated memory is automatically returned to the heap on program termination. However, memory
recycling is essential for server programs that run continuously without ever terminating.
16.3.3 Using malloc to Store an Array
Because array elements are stored contiguously in memory and malloc creates a contiguous block,
it is often used to create space for an array. Thus, the following statement creates a memory block
for storing 10 short values:
short *p;
p = malloc(10 * sizeof(short));
Since the name of an array represents a pointer to the first array element, p can be treated as an
array for navigating and performing pointer arithmetic. We can assign values to the “elements” of
this dynamically created “array” using either pointer or array notation:
Pointer Notation Array Notation
*p = 5; p[0] = 5;
*(p + 1) = 10; p[1] = 10;
*(p + 2) = 15; p[2] = 15;
Even though the calloc function has been specifically designed for allocating memory for arrays
and structures, simple arrays are easily handled with malloc. As the next program demonstrates,
the size of the array can be determined at runtime.
Takeaway: The malloc function makes available a contiguous block of memory that
can be used to store any data type including arrays and structures but without automatic
initialization.
Takeaway: malloc doesn’t determine the data type of the dynamically created block. The type
is determined by the user when declaring the pointer to this block.
Caution: It is a common programming error to use the pointer variable assigned by malloc in
a subsequent call to the same function without freeing the previous block.
free returns nothing and ptr must have been previously returned by any of the memory allocation
functions. Thus, the memory allocated by
int *p = malloc(sizeof(int) * 10);
is returned to the heap by a call to free, preferably followed by setting the pointer to NULL:
free(p); Pointer lost
p = NULL; Can’t access the freed memory
free doesn’t need to know the size of the block because it is known to the OS which is ultimately
responsible for memory management. Because of its subsequent assignment to NULL, the original
value of the pointer is now lost, so the freed block can no longer be accessed directly.
For programs performing simple tasks that consume insignificant amounts of memory, we need not
use free because the allocated memory will be automatically deallocated on program termination.
But when heap memory is scarce, a program that calls malloc 100 times (possibly in a loop) must
also call free 100 times. This ensures that, at any instant of time, adequate memory is available
on the heap for allocation.
Takeaway: The pointer returned by malloc is used for three purposes: (i) for performing
read/write operations on the allocated block, (ii) for resizing the block with realloc,
(iii) for deallocating the block with free.
Note: free can’t release named memory blocks used by variables, arrays and structures.
The argument used by free must point to the beginning of a block previously allocated dynamically.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char *my_strcpy2(const char *src);
int main(void){
char stg[80], *p;
printf(“Enter string: “);
scanf(“%[^\n]”, stg);
p = my_strcpy2(stg);
printf(“Copied string: %s\n”, p);
free(p); /* Frees memory created in the function */
return 0;
}
Unlike my_strcpy, the my_strcpy2 function uses a single argument—a pointer to the source string.
The function uses malloc to create a correctly-sized memory block by determining the length of
the string first. The copied string is then written to this block and its pointer is returned by the
function. Note that termination of the function doesn’t free the block which is eventually freed in
main. A memory leak is thus prevented because the pointer to the block could be transmitted to the caller.
Takeaway: A memory block created inside a function remains in existence after the function
terminates. If this block has a local variable pointing to it, this pointer variable must be returned
by the function so it can be used in the caller to free the block.
** 1 2 3 4 5
*
11 22 33 44 55
*
55 66 77 88 99
*
int main(void)
{
short rows, columns, i, j;
short **p; /* Signifies a 2D array p[ ][ ] */
int main(void) {
int i, length, num;
char **p_names; /* Array of pointers for multiple strings */
char temp[80];
fputs(“Number of strings to enter: “, stderr);
scanf(“%d”,&num);
p_names = malloc(num * sizeof(char *)); /* (A) Allocates memory for ... */
/* ... array of char pointers */
for (i = 0; i < num; i++) {
FLUSH_BUFFER
fputs(“Name: “, stderr);
scanf(“%[^\n]”, temp);
length = strlen(temp);
if (p_names[i] == NULL) {
fputs(“Memory allocation failed\n”, stderr);
exit(1);
}
strcpy(p_names[i], temp); /* Copies string to allocated memory */
}
for (i = 0; i < num; i++) {
printf(“%s\n”, p_names[i]); /* Prints each string */
free(p_names[i]); /* Frees memory allocated in (B) */
}
free(p_names); /* Frees memory allocated in (A) */
exit(0);
}
Even though malloc can create space for storing arrays and structures, C supports a separate
function—calloc—for dynamically allocating memory for arrays of any type. Unlike malloc,
calloc needs two arguments:
The number of array elements (num).
The size in bytes of each element (size).
Dynamic Memory Allocation and Linked Lists 533
calloc creates a block of num * size bytes and returns a generic pointer to the beginning of the
block. Unlike malloc, calloc initializes the entire block with zeroes, sparing a programmer the
trouble of using a loop for initialization.
Both of the following allocation statements create a block of memory that can hold an int array
of 10 elements:
int *p;
p = malloc(10 * sizeof(int));
p = calloc(10, sizeof(int)); Pointer to block created by malloc lost!
In either case, the size of the block is typically 40 bytes, but all bytes of the block created by calloc
are initialized to zeroes. If initialization is not required, then you may use either of the two functions,
even though the syntax of calloc is more intuitive when used for storing arrays.
Takeaway: Both malloc and calloc can create space for arrays except that (i) the arguments
of calloc are array-oriented, (ii) calloc initializes the array elements to zero.
Program 16.5 uses calloc to create space for an array of structures containing the performance
parameters of sportsmen. The size of this array is determined at runtime by user input. After the
data is keyed in, the details of the sportsmen are displayed.
Note: Using malloc in tandem with the memset function, you can simulate the effect of calloc.
memset uses three arguments: (i) a pointer to the block, (ii) the initial value, (iii) the number of bytes
to be initialized. The functions have to be used in the following manner to initialize an array of 10 integers
to zero:
p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10); Must include <string.h>
memset assigns the same value to every byte of the block; you can’t use it to set, say, each int array
“element” to 1. That would imply setting every fourth byte to 1. Thus, zero is the only value we can use here.
In spite of allocating memory at runtime, it is still possible for the allocation to be either excessive
or inadequate. You would then need to resize the existing memory block with the realloc function.
This function takes the old pointer (ptr) returned by a previous invocation of malloc or calloc as
the first argument and the desired size (size) as the other argument.
realloc returns a pointer to the beginning of the reallocated block, or NULL on error. The following
sequence that also uses malloc enlarges an existing block of 20 bytes to 100:
Dynamic Memory Allocation and Linked Lists 535
num = i;
if (i > 0) {
fputs(“Printing array elements ...\n”, stderr);
for (i = 0; i < num; i++)
printf(“%d “, p[i]);
}
exit(0);
}