Lecture 7: Stack and Heap
We will now delve deeper into memory and the subdivisions of memory.
Stack
- Section in memory where things like local variables and parameters live.
- Grows downward and shrinks upward. Kind of works like the stack data structure in that aspect.
- Each function call has its own stack frame. Once the function is done, the stack frame is deleted and the stack shrinks upward.
- See diagrams in the slides.
- When stack frame is done, stack does not delete memory it took up since stack is fixed size. Instead, it denotes that that particular spot in the stack is free to use.
- Stack is set to MB (static).
- Stack overflow is when you use up all of the memory in the stack at once.
Limitations
- Say we create an array in a function. When we return the array, like with parameter passing, it is automatically casted to a pointer. However, once the function is done (i.e. returns something), the stack frame goes away and we can no longer access the value being pointed to.
- Problem: local variables go away when a function finishes. These characters will thus no longer exist, and the address will be for unknown memory!
- This is a problem! We need a way to have memory that doesn't get cleaned up when a function exits!
- To solve this, we introduce the heap and dynamic memory!
Heap
To solve the limitations of the stack, we introduce a new section of memory called the heap which allows us to dynamically allocate memory manually.
- Unlike the stack, you are responsible for how you manage (allocate and free) memory on the heap.
- Memory only goes away once you manually delete it.
- Heap grows upward.
Heap Allocation
malloc
The main command/function to allocate memory on the heap is malloc
:
void *malloc(size_t size);
- Size is how many bytes you wish to allocate.
- Remember this is byte-wise (not element-wise!) to make sure to use
sizeof
.
- Remember this is byte-wise (not element-wise!) to make sure to use
- The function
malloc
goes to the heap, finds of block of contiguous memory returns of the requested size, and returns a pointer to the address which is the location of the start of the segment of memory block.
- Different than the stack since we can only interface with the heap via pointers (not even things like arrays).
- Fortunately,
malloc
does not care what you do with the memory (i.e. the type of the data that is stored). As such, the return type ofmalloc
is a generic pointer).
- Returns
NULL
when there is not enough memory given the requested size.- This is dangerous because we may encounter things like memory leaks and errors which can alter the functionality of the program. Thus, we use the
assert()
command which we needs to be placed after every heap allocation.
- This is dangerous because we may encounter things like memory leaks and errors which can alter the functionality of the program. Thus, we use the
assert
Important:
Example:
int *arr = malloc(sizeof(int) * len);
assert(arr != NULL); // if = NULL, terminate immediately
- Note: since the heap is dynamically allocated, there may be previously used/garbage values at the spot of the current allocation. It is your job to zero this memory out before using it if zeroing out is required.
calloc
To help with this there is also another command to allocate memory called calloc
:
void *calloc(size_t nmemb, size_t size);
- Same thing as
malloc
except it zeroes out the memory for us.- Also has different signature. You need to specify the number of elements you wish to allocate and the size of the element. The number of bytes allocated is then these two multiplied together.
- The reasoning and origin behind this was for allocating arrays, but it is now useful for all other things.
- Also has different signature. You need to specify the number of elements you wish to allocate and the size of the element. The number of bytes allocated is then these two multiplied together.
The following uses of malloc
and calloc
are equivalent:
// allocate and zero 20 ints
int *scores = calloc(20, sizeof(int));
// alternate (but slower, more lines of code)
int *scores = malloc(20 * sizeof(int));
for (int i = 0; i < 20; i++) scores[i] = 0; // zero out the bytes
- Because
calloc
automatically zeroes things out for you, it is a more computationally expensive operation. Thus, you should only use it when necessary. Indeed, you actually won't need to use it that often since you will probably overwrite the values already there anyway.
strdup
Indeed, there is another command for allocation called strdup
. This is useful for strings. Instead of having to copy the characters into the dynamically allocated array, C has a nice function:
char *strdup(char *s); // function signature, must take in char *
Freeing Memory
We must clean up after ourselves since we are dynamically allocating memory (there is no auto-delete memory like in the stack).
Example:
char *bytes = malloc(4);
free(bytes);
Some notes:
- Cannot free part of the allocation, you must free the entire thing.
- Even if you have multiple pointers to the same block of memory, each memory block should only be freed once.
- You may need to free memory allocated by other functions if that function expects the caller to handle memory cleanup (i.e.
strdup
).- You may need to write functions that should inform the caller they must free the memory.
Memory leak
- Might run out of memory if you never free.
- Rarely cause crashes. As such, we recommend not to worry about freeing memory until your program is written. Then, go back and free memory as appropriate.
- Valgrind useful for finding memory leaks.
Realloc
We can use realloc
to update the size of a dynamically allocated array.