Last Updated:

Dynamic memory allocation in C

Any program will require memory allocation to run. If you know in advance how much data will be used in your program, you can schedule memory allocation at compile time. But often you have to work with data, the amount of which cannot be known in advance. In this case, dynamic memory allocation is used.

The role of the operating system in working with memory

The operating system manages physical devices. Virtual memory is allocated to the running application. How this memory is distributed in RAM or the paging file from the application cannot be determined. In the operating system, you can see how much memory the application requested and how much was eventually allocated by the system. In Windows, you can use Task Manager or special programs. On Linux, you can use the top or ps commands.

Modern operating systems use pages as a unit of allocated memory. The minimum size of such a page is 4096 bytes for a 32-bit system. If the operating system detects an attempt by an application to use more memory than was allocated, it will be immediately closed.

To work with dynamic memory allocation, you must understand the structure of the application in the operating system. This structure is true for all applications on Windows or Linux operating systems. This is not stipulated by the language standards, but examples of working in popular systems will help to understand the principles of allocation and use of memory by the application. The virtual memory used by the application is divided into three parts:

  • Segment with data
  • Call stack
  • segment codes.

The part given to the application code is unchanged throughout the program. Typically, this part of the memory is read-only from the application itself. This segment is located after the stack and occupies a fixed space. This part of memory contains instructions for execution and is not used to store variables or their values.

The next part of the memory is allocated to the stack. It is populated with local variables and function calls. The stack grows in the opposite direction from the location of the code towards the data segment. The data is placed in the stack as frames. The frame must store the return address.

Variables created inside functions are also located here. The exception is static variables that fall into a data segment. The value of static variables should be stored all the time the program is running.

A data segment stores global and static variables. Constants are located in a read-only segment, variables in a read/write segment. Uninitialized variables are in the bss segment. Next is a space called heap, which is used when a program directly requests memory allocation.

Static memory allocation

If you know in advance how much space is needed for the data with which the program works, then you can allocate all the necessary amount of memory before starting work. Constants and variables declared as global fall into this memory.

Example:

int id = 150; define a static global variable

int main()
{
printf(“%d”, id + 8); //

}

Global variables are located in a data segment. They are available from any part of the program.

Automatic memory allocation

 

Local variables fall into this category. For local variables, visibility is limited to a specific function or loop. Keeping such variables in memory throughout the program, as happens in the case of global variables, is not rational. Therefore, when a function is called, they fall into the stack, and when it is finished, the stack is automatically cleared.

Example:

int main()
{
int a = 3;
int result = factorial(a);
printf(“%d”, result);

}

int factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n — 1);

}

When you use a new call, data is added to the end of the stack and does not erase information left by previous calls. A description of the technical features of function calling can be found in the calling convention. The specific implementation depends on the platform.

Information about the function, its variables, and return address is added to the end of the stack. Each new call creates its own record. The depth of the stack is limited, which should be considered when writing programs.

Dynamic memory allocation

 

When the amount of data cannot be specified during compilation, special functions are used to add memory while the program is running. This memory ends up in a heap in a segment with data. Unlike static and automatic memory, which is allocated at the time of program startup, dynamic memory can be added during operation.

Dynamic memory allocation must use pointers. The difference between a pointer and other variables is that the pointer value is always an address. Pointers are declared based on the type of data that will be stored at that address.

When using pointers, you must use address pickup and dereferencing operators. Pointers can be constants. A pointer can store the memory address of another pointer. Pointers are one of the most difficult and important topics in the study of C.

Example of working with pointers:

#include <conio.h>
#include <stdio.h>

void main() {
int A = 100;
int*p;

//Get the address of variable A
p = &A;

//Display the address of variable A
printf("%p\n", p);

Displaying the contents of variable A
printf("%d\n", *p);

//Change the content of variable A
*p = 200;

printf(%d\n'A);
printf(%d", *p);

getch();
}

Memory allocation occurs with the help of system calls to the operating system.

Functions for language C memory allocation

You can use several functions to query for additional memory. Functions for working with memory are located in the stdlib library. If your program plans to call these functions, you should declare this library. Otherwise, you may encounter errors or warnings at compile time or run the program.

malloc

The first of the functions for dynamic memory allocation in the stdlib library.

Syntax:

void *malloc(size_t size)

The function takes the number of bytes to allocate. Returns a pointer to allocated memory, or null if something goes wrong.

Example of use:

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

void main() {
const int maxNumber = 100;
int *p = NULL;
unsigned i, size;

do {
printf(«Enter number from 0 to %d: «, maxNumber);
scanf(«%d», &size);
if (size < maxNumber) {
break;
}
} while (1);

p = (int*) malloc(size * sizeof(int));

for (i = 0; i < size; i++) {
p[i] = i*i;

}

for (i = 0; i < size; i++) {
printf(«%d «, p[i]);

}

_getch();
free(p);

}

calloc

The difference from the previous function is that the allocated memory is initialized with zeros. This is useful when creating arrays.

Syntax:

void *calloc(size_t nitems, size_t size)

The first parameter is the number of elements in the array, followed by the size of the individual element in bytes. The function returns a pointer to a block of memory or a null pointer if it fails.

Example of use:

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

int main () {
int i, n;
int *a;

printf(«Number of elements to be entered:»);
scanf(«%d»,&n);

a = (int*)calloc(n, sizeof(int));
printf(«Enter %d numbers:\n»,n);
for( i=0 ; i < n ; i++ ) {
scanf(«%d»,&a[i]);

}

printf(«The numbers entered are: «);
for( i=0 ; i < n ; i++ ) {
printf(«%d «,a[i]);
}
free( a );

return(0);
}

The result of the function will depend on user input:

Number of elements to be entered:3
Enter 3 numbers:
22
55
14
The numbers entered are: 22 55 14

realloc

Use this feature to change the amount of memory previously allocated. It is applied after malloc or calloc functions.

Syntax:

void *realloc(void *ptr, size_t size)

A pointer is passed to the function to the amount of memory that you want to change and a new value for that memory block. The result of the function will be returned to the pointer, if the memory was allocated successfully, then there will be a memory address, otherwise the pointer will be equal to the null value.

In the example below, the memory is allocated for the first time by the malloc function, and then the size of the selection is changed by the realloc function:

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

int main () {
char *str;

/* Initial memory allocation */
str = (char *) malloc(15);
strcpy(str, «tutorialspoint»);
printf(«String = %s, Address = %u\n», str, str);

/* Reallocating memory */
str = (char *) realloc(str, 25);
strcat(str, «.com»);
printf(«String = %s, Address = %u\n», str, str);

free(str);

return(0);
}

The result of the output to the console will be:

String = tutorialspoint, Address = 355090448
String = tutorialspoint.com, Address = 355090448

free

 

This feature should be used at the end of any program with dynamic memory allocation. In all of the examples above, it was invoked before the shutdown of a function that used dynamic memory allocation.

Syntax:

void free(void *ptr)

The function takes a pointer to the memory to be released. It doesn't return anything. If the passed parameter was NULL, nothing will happen.

Note for c++

 

C++ supports full compatibility with C. These functions will work, but it is recommended that you use your own C++ methods to work with memory. The new method is designed to allocate memory, and the delete method is designed to release it.

Memory leak

 

There are no garbage collectors in C that may be familiar to people who have used Python or Java. The programmer is responsible for all memory requested by the program. After each request for memory in the program, a command should go to free this memory.

If you use memory allocation without releasing it later, a memory leak occurs. When the program runs for a long time, the amount of requested memory increases, and since there is no return, there are problems associated with the limb of the RAM.

Examples of a program with incorrect memory handling:

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

void swap_arrays(int *A, int *B, size_t N)
{
int * tmp = (int *) malloc(sizeof(int)*N); //temporary array
for(size_t i = 0; i < N; i++)
tmp[i] = A[i];
for(size_t i = 0; i < N; i++)
A[i] = B[i];
for(size_t i = 0; i < N; i++)
B[i] = tmp[i];
//when exiting the function, we forgot to free the memory of the temporary array
}

int main()
{
int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int B[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
swap_arrays(A, B, 10); //the swap_arrays() function has a memory leak

int*p;
for(int i = 0; i < 10; i++) {
p = (int *)malloc(sizeof(int)); //allocation of memory in a loop 10 times
*p = 0;
}
free(p); //and the release outside the loop is one-time. A leak!

return 0;
}

Segmentation error

Trying to use more memory than allocated, or accessing someone else's memory, is called a segmentation error. When using dynamic memory allocation, you should be attentive to addresses and check what values the malloc, calloc, and realloc functions return.

Examples of this error:

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

void foo(int *pointer)
{
*pointer=0; //potential Segmentation fault
}

int main()
{
int*p;
intx;
*NULL = 10; //very obvious Segmentation fault
*p = 10; //fairly obvious Segmentation fault
foo(NULL); //hidden Segmentation fault
scanf("%d", x); //hidden and very popular with C newbies Segmentation fault

return 0;
}

Which way to allocate memory to use

Each way you work with memory has its own advantages and disadvantages. Memory usage is determined by what data the program has to work with.

Advantages of static memory allocation

For arrays whose size is known before the program starts and does not change during its operation, it is better to use static memory allocation. The program code in this case will be simpler, therefore, the number of errors that a programmer can make is reduced.

Advantages of Dynamic Memory Allocation

If you use dynamic memory allocation, you will not encounter stack overflow errors because of the large volume of local variables. You can allocate and release memory using it more rationally. Dynamic memory allocation does not require information about the amount of data before running the program. Memory can be redistributed during the operation of the program.