Memory Layout In C
To understand memory management in C, you must be familiar with memory segments, storage class specifiers, datatypes, type qualifiers and type modifiers. Let us discuss each topic in detail.
Note: This section is very important for writing an efficient program in C. This article is dedicated to teaching memory management in C. You may have to read it multiple times to understand it fully.
Conversion:
8bits = 1 byte, 1024 bytes = 1KB, 1024 KB = 1MB, 1024MB = 1GB
Memory Segments
The compiler of C divides the main memory into four major segments. Let us understand each segment in brief.
Code Segment: This segment has a fixed size i.e. the size of the code segment neither increases nor decreases. This segment of memory is managed(allocation and deallocation) by the compiler itself. This segment of memory is responsible for storing the executable code so that the CPU can execute it. The code segment stores the program's instructions but not the data that the program operates on.
Data Segment: This segment has a fixed size i.e. the size of the data segment neither increases nor decreases. This segment of memory is managed(allocation and deallocation) by the compiler itself. This segment of memory is responsible for storing the data that are necessary till the whole program is over i.e. static variables and global variables.
Unitialized Data [Block Started by Symbol (BSS)] Segment: The BSS segment is part of a program's data segment where uninitialized static or global variables are stored. The variables stored in this segment are initialized to 0 by the operating system during execution.
Initialized Data Segment: This segment is part of a program's data segment where initialized static or global variables are stored. The variables store the actual value that was initialized.
Stack: This segment has a fixed size i.e. the size of the stack neither increases nor decreases. This segment of memory is managed(allocation and deallocation) by the compiler itself. This segment of memory is responsible for storing the data that are necessary till the function call is over i.e. local variables. Every function call is maintained in the stack. When the function is called, the stack frame containing every data and instructions of that function is pushed/inserted into the stack i.e. Memory is allocated for local variables. After the function is over, the stack frame of that function is popped/removed from the stack i.e. Memory is deallocated for local variables. This is why, the variables declared inside the function are inaccessible from outside of the function.
Stack Overflow: Since the size of a stack is fixed, the space available sometimes is not enough for the function calls. The stack gets full. In such case, there is no more space for another function call and we get a stack overflow. During compilation, the size of the stack frame is calculated and on that basis, the size of the stack is allocated during execution. If there are multiple function calls such as recursive function calls there might be a high chance of stack overflow.Heap: This segment doesn't have a fixed size i.e. the size of the heap can increase or decrease as per our needs. This segment of memory is essential for dynamic memory allocation (DMA). This segment of memory must be managed by the programmers themselves i.e. allocate yourself and deallocate yourself. We can allocate memory dynamically using malloc(), calloc() and realloc() functions. Since the allocated memory could be anywhere, the above-mentioned functions return a memory address of that allocated memory. To store a memory address we use pointers. In short, we can access the heap memory through pointers. The memory allocated using the above functions must be deallocated. We can deallocate dynamically allocated memory using the free() function. The heap is sometimes referred to as free memory. We must be careful while using heap memory because if we forget to release the memory (deallocate), the program will have a memory leakage.
Storage Class Specifier
Storage specifiers are used to determine how and where variables are stored in memory, their scope, and their lifetime within a program.
HOW are the variables accessed in different segments of the memory?
WHERE are the variables stored?
HOW long is the life of stored variables?
auto:
This is a default storage class specifier. Variables declared with the "auto" storage class specifier are allocated memory on the stack. They are typically accessed using stack-based memory allocation and deallocation mechanisms.extern:
Variables declared with "extern" are typically not allocated memory at the point of declaration. Instead, they are associated with global symbols, and their memory is allocated elsewhere (in another translation unit or object file). They are used for sharing data between files. They are accessed using symbol resolution during linking.static:
Variables declared as "static" are allocated memory in a data segment.register:
Variables declared as "register" are often allocated memory in the CPU register.#include <stdio.h> int globalVar; // Global variable with static storage duration static int globalStaticVar = 10; int main() { // Automatic variable (default storage class specifier is "auto") auto int localVarAuto = 5; // Static variable with block scope static int localVarStatic = 7; // Extern variable (declared in another file) extern int globalExternVar; printf("Auto variable: %d\n", localVarAuto); printf("Static variable: %d\n", localVarStatic); printf("Static global variable: %d\n", globalStaticVar); printf("Extern variable: %d\n", globalExternVar); return 0; }
Datatype
Datatypes are used to define the nature of the data and valid operations. The data types can be classified into three groups:
Fundamental/Primitive Datatype: These are the basic building blocks of data representation in C. They are int, char, float, double and void.
int:
- a decimal integer data can be held in a variable of int datatype.
- an integer datatype allocates either 2 bytes(older standards/16-bit compiler) or 4 bytes(newer standards/32-bit compiler) of space/memory.
- Range: 2^16 = 65536 i.e. -32,768 to +32767#include<stdio.h> int main() { int num = 5; printf("\nThe value of num is %d", num); printf("\nMy PC takes %lu bytes to store an integer.", sizeof(int)); return 0; }
char:
- character data can be held in a variable of char data type.
- character datatype allocates one byte[8 bit] of memory.
- Range: 2^8 = 256 i.e. -128 to +127#include<stdio.h> int main() { char ch = 'K'; printf("\nThe value of ch is %c", ch); printf("\nMy PC takes %lu bytes to store a character.", sizeof(char)); return 0; }
float:
- a decimal floating point data can be held in a variable of float data type.
- a float datatype allocates 4 bytes of memory.
- Range: -3.4E+38 to +3.4E+38#include<stdio.h> int main() { float marks = 77.56; printf("\nThe value of marks is %f", marks); printf("\nMy PC takes %lu bytes to store a float.", sizeof(float)); return 0; }
double:
- a decimal floating point data can be held in a variable of double data type.
- a double datatype allocates 8 bytes of memory.
- Range: -1.7E+308 to +1.7E+308#include<stdio.h> int main() { double marks = 78.98; printf("\nThe value of marks is %lf", marks); printf("\nMy PC takes %lu bytes to store a double.", sizeof(double)); return 0; }
void:
- It holds no value.
- They are mostly used in function definition to tell the compiler that it doesn't return a value after execution.
- It is also used to create void pointers that can point to data of any type, especially in dynamic memory allocation.#include<stdio.h> #include<stdlib.h> void display(int*); int main() { int *ptr; ptr = (int*)malloc(sizeof(int)); *ptr = 5; display(ptr); return 0; } void display(int *num) { printf("\nThe value of num is %d", *num); }
Derived/Non-Primitive Datatype: These are created by modifying fundamental data types. They are array, pointer and function.
Array: Represents a collection of elements of the same data type. Since, an array is a collection of similar items, the use of a loop is common.
#include<stdio.h> int main() { // declaration int num[5], i; float marks[3]; // initialization char name[50] = "Kushal Ghimire"; // assignment marks[0] = 44.44; marks[1] = 55.55; marks[2] = 66.66; //input for(i=0; i<5; i++) { printf("\nEnter num[%d]: ", i); scanf("%d", &num[i]); } //output for(i=0; i<5; i++) { printf("\n num[%d] = %d ", i, num[i]); } return 0; }
Pointer: Holds the memory address of another variable.
#include<stdio.h> int main() { // initialization of a variable int num = 5; // declaration of a pointer int *p; // assignment of memory address of a variable to a pointer p = # // Using Variables printf("\nThe value of num is %d", num); printf("\nThe memory address of num is %p", (void*)&num); // Using Pointers printf("\nThe value of num is %d", *p); printf("\nThe memory address of num is %p", (void*)p); return 0; }
Function: Represents a function that can take arguments and return a value.
#include<stdio.h> void square(int); int main() { int returned_value, num = 9; returned_value = square(num); printf("\nNum = %d and Square = %d", num, returned_value); return 0; } int square(int n) { return n*n; }
User-Defined Datatype: These datatypes are created by the programmer using either
struct, union, enum
. If the new defined data type needs a meaningful name, we can define a meaningful name usingtypedef
.Struct: Groups variables of different data types under a single name.
#include<stdio.h> // definition of a user defined datatype with a tagname student struct student { int roll; float marks; char name[100]; }; int main() { // declaration of a user defined datatype struct student s1; // initialization of a user defined datatype s1.roll = 1; s1.marks = 55.79; s1.name "Samir Karki"; printf("Roll = %d Marks = %f Name = %s", s1.roll, s1.marks, s1.name); return 0; }
Union: Similar to structures, but only one member can have a value at a time.
#include<stdio.h> //tagname = student union student { int roll; float marks; char name[100]; }; int main() { // declaration of a user defined datatype union student s1; s1.roll = 1; printf("Roll = %d", s1.roll); s1.marks = 55.79; printf("\nMarks = %f", s1.marks); s1.name "Samir Karki"; printf("\nName = %s", s1.name); return 0; }
Type Definition: When the meaningful name of a user defined datatype is needed we use typedef keyword in c. For example,
#include<stdio.h> struct student { int roll; float marks; char name[100]; }; // Type definition: instead of "struct student", i want to write "student". typedef struct student student; int main() { student s1; s1.roll = 1; s1.marks = 55.79; s1.name "Samir Karki"; printf("Roll = %d Marks = %f Name = %s", s1.roll, s1.marks, s1.name); return 0; }
Enumeration: Defines a set of named integer constants.
#include<stdio.h> enum color {red, blue, green }; int main() { enum color c1; c1 = blue; printf("%d",c1); return 0; }
Type Qualifier
Modify the properties and usage of data types or variables of the data type, often adding constraints or indicating specific behaviors. const and volatile are type qualifiers in ANSI C.
const: Indicates that a variable's value cannot be modified after initialization, ensuring immutability.
#include<stdio.h>
int main()
{
int n = 11;
const int num = 7;
//changing the value of a variable
n = 15;
//changing the value of a constant is not allowed
num = 9; //this line will give an error
printf("\nThe value of num is %d", num);
printf("\nThe value of n is %d", n);
return 0;
}
volatile: The volatile keyword is a type qualifier that is used to indicate that a variable can be changed by external sources or in ways that are not explicitly specified by the program. For example, the value of that variable may be received from a sensor (another hardware). Another example, could be taken of a multithreading concept where multiple threads might be working with the shared variables and such variables should not be optimized by the compiler.
/*
This code is just a demonstration and it won't compile or run if you copied it.
Suppose there is a readSensor() defined somewhere.
*/
#include<stdio.h>
volatile int sensorValue; // Declare a volatile variable
int main()
{
// Code that interacts with hardware or external sources
sensorValue = readSensor(); // Read sensor value from a function
// Use the sensor value without the compiler optimizing it away
printf("Sensor Value: %d\n", sensorValue);
return 0;
}
Type Modifier
Datatype modifiers specify various properties and characteristics of program elements, including storage, type, and other attributes. We can modify the base data type itself usinglong, short, signed, unsigned
in C.
short: If your compiler allocates 4 bytes for a normal integer then it will allocate 2 byes of memory for short int.
long: If your compiler allocates 4 bytes for a normal integer then it will allocate 8 byes of memory for long int.
signed: It is a default modifier. The signed integer can hold positive as well as negative values. Thus, the range is of 2 bytes. 2^16 = 65536 i.e. -32,768 to +32767.
unsigned: The unsigned integer can hold only positive values. Thus, the range is of 4 bytes.
#include<stdio.h>
int main()
{
short unsigned int num = 1;
long unsigned int fact=1;
int negative_num = -1;
printf("\n Short Unsigned Number = %hu \t Size = %lu byte", num, sizeof(num));
printf("\n Long Unsigned Number = %lu \t Size = %lu bytes", fact, sizeof(fact));
printf("\n Negative Signed Number = %d \t Size = %lu bytes", negative_num, sizeof(negative_num));
return 0;
}
In short, type qualifiers modify the behavior of variables of a specific data type, while type modifiers alter the base data type itself to create variations of that type.