This is 5th blog in my ongoing CS50 Series. Readers are suggested to go through 0th, 1st ,2nd & 3rd article for a better understanding of subject matter. In this blog, we will learn about memory management in C.
In the last blog, we studied different searching & sorting algorithms. In this blog, we will be studying
- Hexadecimal Numbers
- Memory Layout
- Dynamic Memory Allocation
- Call Stacks
- File Pointers
In our daily life, we use Decimal System (base 10) having 10 digits (0 to 9) to represent the data where each digit has place values like 1, 10, 100 and so on. The computer use Binary System (base 10) having 2digits (0 &1) to represent the data where each digit has place values like 1, 2, 4 and so on. Similarly we there is Hexadecimal System (base 16) having 16 digits (0 to F), each digit has a place value of 1, 16, 256 and so on.
Each hexadecimal number corresponds to a unique arrangement of 4 binary digits or bits and hence we can express very long, complex binary numbers in a very concise way without losing any data. We use the prefix “0x” before any hexadecimal to represent that it is a hexadecimal number. Each byte in the computer memory is identified by a unique hexadecimal address.
The pointer, as its name suggests, stores the address of another variable i.e. points toward the other variable. The pointer is nothing more than an address.
For any pointer,
• Its value is a memory address
• Its datatype describes the data located at that memory address.
There are two operators which have been used in the pointer syntax & example above
- Extraction Operator (&)
The extraction operator(&) extracts the address of a variable. We use the extraction operator to initialize the pointer. If var is variable of int type then &var is a pointer to the int and its value is the address of variable var.
- Dereference Operator (*)
The dereference operator(*) it goes to the reference and accesses data at that memory location allowing you to manipulate it at will. We use the dereferencing operator to declare or create the pointer. If ptr is a pointer of int type then *ptr is the data or value of a variable (here int) whose address is stored inside ptr.
Remember the following things about pointers.
- The pointer must have the same data type as that of data at the memory address which is stored in a pointer.
- The size of a pointer to any data type is 4 byte for a 32-bit system & 8 bytes for a 64-bit system.
- The pointer should be initialized with NULL if you don't initialize it with meaningful value. Such pointer is called Null Pointer which points toward nothing.
- Pointers allow us to pass the data by reference. means it allows us to pass an actual variable itself. Thus change that is made in other function will impact the actual data in the passed variable
The computer memory consists of millions and billions of bytes and during a program when we request the memory to the computer, the requested memory is allocated to you from these bytes. The computer (specifically OS) doesn't allocate the memory arbitrarily but follows a certain methodology. The computer uses different sections of memory for different types of data. If we imagine computer memory as a rectangle with three sections then the data will be stored as follows
- Upper Section
The compiled machine code is stored in the uppermost section of memory. Below machine code, global variables are stored.
- Middle Section(Heap)
Heap section is a chunk of memory from which memory is allocated for dynamic memory allocation. When we function like malloc(), they are allocated memory in the heap section. The more memory we use, the computer will go on allocating memory on the heap section towards the lower side.
- Bottom Section(Stack)
The bottom-most part of memory below the heap section is called Stack. Whenever you called any function in a program, the local variables and arguments of that function get memory from the stack section. The more memory we use, the computer will go on allocating memory on the stack section towards the upper side.
Dynamic Memory Allocation
Till this time in CS50, we were allocating memory statically where we would specify how much memory we need in a program. What if we don't know the amount of memory we need? This problem is solved by using Dynamic Memory Allocation.
We can use pointers to get access to a block of dynamically allocated memory at runtime. Dynamically allocated memory comes from the heap section whereas statically allocated memory comes from the stack section.
We can get dynamically allocated memory by making a call to malloc() function, passing as it parameter the number of bytes required. If it can obtain memory for you, it will return a pointer to the obtained memory. if it can’t obtain memory for you, it will return NULL pointer. Hence it is important to check whether pointer returned by malloc() is NULL or not and if t is then you need to end the program. If you try to dereference the NULL pointer then you will suffer a segmentation error.
The problem with dynamically allocated memory is that the memory you are allocated is not automatically returned to the system for further use when a function in which it is created finishes execution. Thus it results in memory leak which can compromise the performance of your system. Hence it is standard practice to free the dynamically allocated memory you have used using the free() function.
Three Golden Rules of Dynamic Memory Allocation:
- Every block of memory that you malloc() must subsequently be free()d.
- The only memory that you malloc() should be free()d.
- Do not free() a block of memory more than once.
You can understand the Dynamic Memory Allocation from the following visualization.
When you call a function, the system set aside a chunk of memory on the stack section for that function to do its necessary work. Such a chunk of memory is called Stack frames or function frames.
- In the case of nested functions, more than one functions stack frames can exist in memory at a given time, although only one stack frame can be active at a given time.
- These frames are arranged in a stack. The frame for most recently called function is always on the top of the stack.
- When a new function is called, a new frame is pushed on the top of the stack and becomes the active frame.
- When a function finishes it’s work, its frame is popped off of the stack and the frame immediately below it becomes new active function on the top of the stack and picks up immediately from where it left off.
You can understand the above process from the following visualization.
Up to this point in C, we don't have any means by which we can store any persistent data, i.e. the information that exists even after the program has stopped running. Fortunately, C provides you with this ability using FILE. It is a data structure that represents a file. We will be working with FILE*, a pointer to files. All of the file manipulation functions are defined in header file stdio.h and all of them accept FILE* as their parameter except fopen() function which is used to get file pointer in the first place. Following are some of the most common file input/output (I/O) functions.
You should take care of following things while using these functions
- fopen(): Always check it’s return value to making sure that you don't get back NULL.
- fgetc() / fread(): The operation of file pointer passed in as parameter must be “r” for read, or you will suffer an error.
- fputc() / fwrite(): The operation of file pointer passed in as parameter must be “w” for write or “a” for append, or you will suffer an error
I hope this blog was helpful to you. This series of blogs on CS50 will continue. Thank you very much for your patient reading. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above. You can connect with me on LinkedIn or by visiting my website on Medium. Happy Learning.