A quick note: NULL

One thing we should quickly visit is the idea of NULL. All memory contains A VALUE of some kind, right? Well for basic data types like integers and booleans, it isn’t too detrimental to hold on to odd values when they aren’t initialized. But for pointers, it’d be nice if we could have some sort of default value. Something that says… “I’m not pointing at ANYTHING right now, so don’t use me!“.

Turns out there is a value we can use like that, and it’s called NULL:

//This pointer doesn't point at a valid memory address, on purpose
int* bob = NULL;

We can sometimes use NULL in our if statements to check whether a pointer is valid or not. We’ll get into this more as we talk about dynamic memory.

Warning: Don't confuse NULL with other things!

NULL is not the same as the nul-terminator for strings. It also is not the same as nullptr, which is the convention for assigning a null pointer in C++ (but not in C).

Dynamic Memory

When we create a variable or array in our code, we never think about where that memory exists! This hasn’t been something we’ve needed to worry about. We just… make stuff, and it exists in memory. But WHICH memory?

Turns out our variables exist in a memory space called the stack. It’s basically a big hunk of memory that is used when we create variables or call functions. But it’s not usually very big. There are ways to increase the stack size, but it generally starts off small, at maybe only 1MB on Windows machines.

So what if we want a HUGE amount of memory? It doesn’t make much sense to put it on the stack, since it isn’t very large. Also, what if we have some temporary operation that uses some memory, and then we don’t that memory later? Is there anything we can do?

It’d be convenient if we could ask the operating system itself for memory! And there’s a nifty mechanism in C that allows us to do just that!

Calling malloc

#include <stdlib.h>
 
//allocate a block of memory
void* malloc(size_t size);
 
//deallocate a block of memory
void free(void* pointer);

Basically: we ask malloc to send us back an address where a bunch of free memory is located. Specifically, we want a chunk of memory that is size bytes large. So malloc will go ask the operating system to provide that memory.

You can see that malloc returns a void* (called a void pointer). It’s just a generic address without any associated type. We can’t really do much with it on its own… but we can assign that address to a pointer variable that DOES have a type:

int* millionInts = (int*) malloc(sizeof(int) * 1000000)

We’re casting the return value of malloc to an int*. It’s good practice to do this even though it isn’t strictly required in some newer compilers. It is definitely required in C++ (meaning CS170) so just do the cast!

This memory is located in a structure called the heap. Your program keeps track of its heap size, and uses malloc to ask the operating system for more heap memory. We can allocate a LOT of heap memory to fit our needs, and then free it again when we no longer need it:

free(millionInts);

OK, so we have a big chunk of memory now… how do we use it? Well, same as we would any other chunk of memory!

millionInts[2] = 57;
 
(*(millionInts + 2)) = 57;

We can use array syntax or pointer syntax to access the chunk of memory (which we are choosing to interpret as integer data) and set/retrieve values from it. Keep in mind we face the same challenges here as with normal arrays/pointers: if you try to access something outside the bounds of your allocated memory, you will certainly get undefined behavior.

Warning

Just like uninitialized stack memory, failing to initialize your heap memory and then using it will result in weird behavior. Unless you immediately plan to fill up ALL of the memory you’ve allocated for yourself, it’s a good idea to set it all to 0 or some default value.

Calloc

There is also a variation of malloc, a different function called calloc. This function works identically to malloc, except that it zeros out the memory (meaning, all the bits in the allocated memory are set to 0):

int* millionInts = (int*) calloc(1000000, sizeof(int));

There is one odd quirk about calloc though: the function call looks a bit different because it takes a number and a size, rather than just a size. This saves us having to do a multiplication in our call.

Calloc is convenient if the needed memory must be zeroed out before use. However, if you plan to immediately fill all of the dynamically allocated memory with useful values, it would be more efficient to call malloc instead.

Failure and NULL

It is possible for the malloc function to fail, meaning that the operating system can’t grant us the amount of memory we’re requesting. If it fails, malloc returns NULL. This could happen for a number of reasons, but usually it means you’re doing something pretty far-out. Remember to check the return value of malloc before using it!

float* myTemperatures = malloc(sizeof(float) * 5000);
if(myTemperatures == NULL)
{
	//handle this weird error!
}

It’s also interesting to note that the examples above all use the sizeof operator. Why is that? In short: don’t assume the sizes of your data types! Because float, int, etc could be different sizes on different platforms, you want to use the size returned by sizeof and not assume you know the type’s size. This is especially important when using structs, which aren’t guaranteed to have their data members residing sequentially in memory (which, again, we haven’t talked about… but we will!).

Calling free

When we’re done using our malloc'd memory, we should free it:

Pokemon* myBoxes = malloc(sizeof(Pokemon) * 3000);
if(myBoxes == NULL)
{
	//handle this weird error!
}
 
//do Pokemon related stuff
 
free(myBoxes);

This allows the OS to reclaim that memory for other programs that need it. We may need it again ourselves in the future!

Pro tips about freeing memory:

  • Don’t call free on a pointer twice! Free it ONCE!
  • Match up your malloc/free calls; if you call free on a pointer that doesn’t point at the BEGINNING of memory allocated via malloc, your whole program is now undefined.
  • Forgetting to free memory results in memory leaks. Eventually your program will not be able to ask for more memory and it will crash in a weird, painful way!
  • Don’t try to access free'd memory! Leave it alone!
  • Don’t EVER lose track of your memory as shown below, or it will be impossible to free it yourself:
 
int x = 5;
 
int* myIntegers = (int*) malloc(sizeof(int) * 500);
 
//OH SNAP! No way to free that memory any more!
myIntegers = &x;
 

The operating system will relinquish our program’s hold on memory when the program exits. But it is certainly always good practice to free memory you’ve allocated, even if you’re certain that you’ll need it until your program terminates.