Take a deep breath — this is the last theory-heavy chapter you'll be reading for quite a while. 😄
So, what are we diving into today? We're going to learn how to create a variable manually using dynamic memory allocation.
When you declare a variable, what you're really doing is asking for a chunk of memory to be allocated:
int myNumber = 0;
When your program hits a line like that, here's what's actually going on behind the scenes:
- Your program asks the operating system (Windows, Linux, macOS...) for permission to use a bit of memory.
- The OS replies by telling your program where it can store that variable — it gives it the address it's set aside.
- When the function ends, the variable is automatically removed from memory. Your program basically tells the OS: "I no longer need the memory you gave me at that address — thanks!" (Note: whether or not the program actually says "thanks" is unclear, but it really should — after all, the OS is the one in charge of memory! 😄)
Up to now, all of that has happened automatically. When you declare a variable, the OS is quietly doing the work in the background. But how about doing it manually? Not just for the thrill of complexity (though that is tempting), but because sometimes... you have to.
In this chapter, we're going to:
- Take another look at how memory works (yep, again!) and see how much space different variable types take up.
- Then we'll dive into the main topic: how to request memory manually from the operating system. That's what we call dynamic memory allocation.
- Finally, we'll explore why dynamic memory allocation is useful — like when you want to create an array whose size isn't known until the program is running.
The Size of Variables
Depending on the type of variable you want to create (char, int, double, float, etc.), you'll need more or less memory.
For example, to store a number between -128 and 127 (a char), all you need is one byte of memory (super tiny, right? 😄). An int, on the other hand, typically takes up 4 bytes. A double usually takes 8 bytes.
But here's the catch: that's not always the case. It actually depends on your computer. Maybe on your system an int takes 8 bytes — who knows? Our goal here is to check how much memory each type uses on your machine.
There's a super easy way to find out: the sizeof()
operator. Despite how it looks, this isn't a function, but a built-in feature of the C language. You just put the type you want to check in parentheses.
So to check the size of an int, you write:
sizeof(int)
When the code is compiled, this will be replaced with a number: the number of bytes an int occupies in memory. On my system, sizeof(int)
equals 4, which means an int takes up 4 bytes. Yours is probably the same, but don't take it for granted — try it and see! 😉
You can print the results with a few printfs like this:
printf("char : %d bytes\n", sizeof(char)); printf("int : %d bytes\n", sizeof(int)); printf("long : %d bytes\n", sizeof(long)); printf("double : %d bytes\n", sizeof(double));
On my system, this prints:
char : 1 bytes int : 4 bytes long : 4 bytes double : 8 bytes
I didn't include every type we've seen so far — I'll let you experiment on your own with the rest. 😉 You'll notice that long and int take up the same amount of memory. So in this case, using a long is exactly the same as using an int — both use 4 bytes.
In fact, the type long is just shorthand for long int, which in this case is equivalent to just int. So yeah... lots of different names for basically the same thing. 😉 Back in the day, when computers didn't have much memory, having many different types was useful — we wanted to use the most efficient type possible to save space.
Nowadays, with our huge amounts of memory, that's not really a concern anymore.
Yes! sizeof
works on structs too!
typedef struct Coordinates Coordinates; struct Coordinates { int x; int y; };int main(int argc, char *argv[]) { printf("Coordinates : %d bytes\n", sizeof(Coordinates)); return 0; }
Result:
Coordinates : 8 bytes
The more members a structure contains, the more memory it takes. Makes total sense, right? 😄
A New Way of Visualizing Memory
Up until now, my memory diagrams have been a bit vague. But now that we know the actual sizes of different variable types, we can finally make them precise and accurate (about time, right? 😄)
If we declare a variable of type int:
int number = 14;
...and if sizeof(int)
is 4 bytes on our machine, then that variable will occupy 4 bytes in memory!
Let's say the variable number is stored starting at address 2500. Our memory layout would look like this:
Address | Value |
---|---|
2499 | ... |
2500 | 14 |
2501 | 14 |
2502 | 14 |
2503 | 14 |
2504 | ... |
Here, you can clearly see that our variable number (of type int, holding the value 14) uses 4 bytes in memory. It starts at address 2500 (that's its address) and ends at 2503. The next variable can only be stored starting at address 2504!
If we'd done the same with a char, we'd have only used 1 byte, and so on.
Now imagine an array of int! Each element in the array takes up 4 bytes. If our array has 100 elements:
int array[100];
Then the array uses: 4 * 100 = 400 bytes of memory. 🙂
Absolutely! That memory is reserved — no other program can use it (except yours). As soon as a variable is declared, it immediately claims space in memory.
Now let's say we create an array of type Coordinates:
Coordinates array[100];
How much space does that take? (Come on, this one's easy! 😄)
8 * 100 = 800 bytes of memory.
It's super important to get comfortable with these little memory calculations — they're going to be very useful in the rest of the chapter.
Dynamic Memory Allocation
Now we're getting into the heart of the matter. What was the goal of this chapter again? :-°
Ah yes: learning how to request memory manually.
We're going to need to include the <stdlib.h> library (if you've been following my advice, you should already be including it in all your programs anyway 😉).
This library contains two functions we'll need:
- malloc ("Memory ALLOCation"): requests permission from the operating system to use memory.
- free: tells the OS that we no longer need the memory we previously requested. The memory is freed and can now be used by another program if needed.
Whenever you manually allocate memory (which we're about to learn how to do), you should always follow these three steps:
- Call malloc to request memory
- Check the value returned by malloc to see if the OS actually managed to allocate it
- Once you're done with the memory, free it using free. If you don't, your program could end up hogging more and more memory — this is what we call a memory leak.
Hey, don't those three steps sound familiar? Like... file handling, maybe? Yup, exactly. :D
The principle is the same as with files: you allocate, you check if it worked, you use the memory, and you free it when you're done.
Let's dive into how malloc works.
malloc: Requesting Memory
The prototype for the malloc function is actually kind of funny:
void* malloc(size_t numberOfBytesNeeded);
The function takes one argument: the number of bytes to reserve. So if you want enough space for an int, you just write sizeof(int).
But the real kicker is what malloc returns: it gives you a... void* :-°
If you remember the chapter on functions, I told you that void means "empty" and is used to indicate that a function returns nothing.
These programmers really do have a sense of humor.
Actually, the function returns a pointer to the block of memory the OS has reserved for you. So if it found you a spot at address 2500, then malloc returns a pointer containing 2500.
The thing is, malloc doesn't know what kind of variable you're trying to create. It only knows how much memory you're asking for. If you ask for 4 bytes, that could be an int, or a long, or who knows what!
Since malloc has no idea what type it should return, it gives you a void*. That's a universal pointer — it can point to anything. It's like the Swiss Army knife of pointers.
Now, let's get our hands dirty. If I want to manually (ahem...) create an int variable in memory, I'll use malloc and tell it I need sizeof(int) bytes. I'll store the result in an int* pointer.
int* allocatedMemory = NULL; // Creating an int pointerallocatedMemory = malloc(sizeof(int)); // malloc writes the reserved address into our pointer
At the end of this code, allocatedMemory will hold an address given by the OS — say, 2500, if we go by our earlier memory diagrams.
Checking the Pointer
So malloc has written into allocatedMemory the address of the memory it reserved for you.
Two possibilities here:
- If the allocation succeeded, your pointer contains a valid address.
- If it failed, your pointer will hold the address NULL.
It's pretty rare for an allocation to fail, but it can happen. Imagine asking for 34 GB of RAM... not super likely the OS is gonna give you a thumbs-up on that one :/
Still, it's good practice to check if the allocation worked. If it didn't, that means there's no memory left — a pretty serious situation. In such a case, it's best to stop the program immediately, since it can't continue properly anyway.
We'll use a standard function we haven't seen yet: exit()
. It stops the program cold.
It takes one argument: the return value for the program (just like return in main()).
int main(int argc, char *argv[]) { int* allocatedMemory = NULL;allocatedMemory = malloc(sizeof(int)); if (allocatedMemory == NULL) // Allocation failed { exit(0); // Quit the program immediately }
// Program continues as normal
return 0; }
If the pointer isn't NULL, the program can carry on. Otherwise, you should display an error message or quit entirely — there's no use going on if you're out of memory.
free: Releasing Memory
Just like we use fclose to close a file we're done with, we use free to release memory we no longer need.
void free(void* pointer);
free only needs the memory address you want to release. So in our case, we'll pass it our pointer — allocatedMemory.
Here's the full, final version of our memory management routine. Doesn't it look awfully familiar? Yep — just like the file-handling chapter:
int main(int argc, char *argv[]) { int* allocatedMemory = NULL;allocatedMemory = malloc(sizeof(int)); if (allocatedMemory == NULL) // Check if memory was allocated { exit(0); // Error: halt everything! }
// You can use the memory here
free(allocatedMemory); // Done using memory? Free it!
return 0; }
And there you have it — manual memory management in C, with all the power (and responsibility) it brings 😄
An Example of Manual Allocation
Let's go back to something we learned ages ago: asking the user for their age and displaying it back to them.
The only difference this time is that the variable will be allocated manually (also called dynamically), instead of automatically like before. So yeah, the code's a bit more complex now. But take the time to understand it — it's important!
int main(int argc, char *argv[]) { int* allocatedMemory = NULL;allocatedMemory = malloc(sizeof(int)); // Allocating memory if (allocatedMemory == NULL) { exit(0); }
// Using the allocated memory printf("How old are you? "); scanf("%d", allocatedMemory); printf("You are %d years old\n", *allocatedMemory);
free(allocatedMemory); // Freeing memory
return 0; }
Output:
How old are you? 14 You are 14 years old
Heads up: since allocatedMemory is a pointer, you don't use it quite like a regular variable. To get the value, you have to use an asterisk: *allocatedMemory
(check out the printf). But to pass the address (like in scanf), just write the name of the pointer: allocatedMemory
.
Back to our code: we've dynamically allocated a variable of type int.
In the end, this code is functionally identical to doing it the "automatic" way, which you already know well:
int main(int argc, char *argv[]) { int myVariable = 0; // Memory allocation (automatic)// Using the memory printf("How old are you? "); scanf("%d", &myVariable); printf("You are %d years old\n", myVariable);
return 0; } // Memory is freed automatically at the end of the function
Output:
How old are you? 14 You are 14 years old
To sum up: there are two ways to create a variable, i.e., to allocate memory.
- Automatically: That's the method you've been using so far.
- Manually (aka dynamically): That's the one I'm teaching you in this chapter.
Okay, yes — it's a bit more complex. But useless? Definitely not! In some situations, manual allocation is necessary, as we're about to see. :)
Dynamically Allocating an Array
So far, we've only used dynamic allocation to create a tiny variable. But let's be honest — that's not really what dynamic allocation is for ;) The automatic method is simpler and usually better for single variables.
Most often, it's when we want to create an array — but we don't know the array's size until the program is running.
Let's say you're writing a program that stores the ages of all the user's friends in an array. You might think to do this:
int friendsAges[15];
But who says the user has 15 friends? Maybe they have more than that!
When you write the source code, you don't know how big the array should be. You'll only know once the program is running and you ask the user how many friends they have.
That's where dynamic allocation comes in: you ask the user how many friends they have, and then you create an array with exactly that number of elements — no more, no less. :) If they have 15 friends, you make an array of 15 int; if they have 28, then it's 28 int, and so on.
As I've already told you, it's not allowed in C to create an array whose size is defined by a variable like this:
int friends[nFriends];
The beauty of dynamic allocation is that it does let us create an array sized exactly to the value of a variable — and in a way that works everywhere.
We'll ask malloc to reserve nFriends * sizeof(int) bytes in memory:
friends = malloc(nFriends * sizeof(int));
That creates an int array with a size matching the exact number of friends!
Here's the plan of action:
- Ask the user how many friends they have.
- Use malloc to create an int array with that many elements.
- Ask for each friend's age and store them in the array.
- Display all the ages to prove we've stored them properly.
- Once we're done, we release the memory using free.
int main(int argc, char *argv[]) { int nFriends = 0, i = 0; int* friendsAges = NULL; // This pointer will become an array after malloc// Ask the user how many friends they have printf("How many friends do you have? "); scanf("%d", &nFriends);
if (nFriends > 0) // Gotta have at least one friend (otherwise, I feel a bit bad for them :p) { friendsAges = malloc(nFriends * sizeof(int)); // Allocate memory for the array if (friendsAges == NULL) // Check if allocation succeeded { exit(0); // Abort! }
// Ask for each friend's age for (i = 0 ; i < nFriends ; i++) { printf("How old is friend number %d? ", i + 1); scanf("%d", &friendsAges[i]); }
// Display all the stored ages printf("\n\nYour friends are the following ages:\n"); for (i = 0 ; i < nFriends ; i++) { printf("%d years old\n", friendsAges[i]); }
// We're done with the array — free the memory free(friendsAges); }
return 0; }
Output:
How many friends do you have? 5 How old is friend number 1? 14 How old is friend number 2? 15 How old is friend number 3? 20 How old is friend number 4? 27 How old is friend number 5? 90Your friends are the following ages: 14 years old 15 years old 20 years old 27 years old 90 years old
Okay, yes — this program is pretty pointless. It just asks for some ages and prints them out. I picked this example because it's simple (well... if you've wrapped your head around malloc).
Don't worry — later in the course, we'll use malloc for much cooler stuff than just tracking how old your friends are. 😄
Enjoyed this C / C++ course?
If you liked this lesson, you can find the book "Learn C Programing for Beginners" from the same authors, available on SiteRaw, in bookstores and in online libraries in either digital or paperback format. You will find a complete C / C++ workshop with many exclusive bonus chapters.