Tales of System Programming - 2

Baby steps....

Hello there!

Hope you're doing well. Welcome to the second part of the System Programming write-up. Here is the PART 1

Let's continue with Dynamic Memory Management.

When memory is allocated, additional information is stored as part of a data structure maintained by the heap manager. This information includes, but not limited to, block's size. And it is placed immediately adjacent to the allocated block. If the application writes outside of this block of memory, then the data structure can be corrupted.

Memory Leaks

Occurs when allocated memory is never used again but is not freed. This can happen when the Memory's address is lost or the free function is never invoked though it should be. The latter case also called hidden leak sometimes. A problem with memory leaks is that the memory cannot be reclaimed and used later.The amount of memory available to the heap manager is decreased. If memory is repeatedly allocated and then lost, then the program may terminate when more memory is needed but malloc cannot allocate it because it ran out of memory. In extreme cases,the operating system may crash. Here is a small example that can illustrate this scenario:

char *ptr;
while(1) {   //This is infinite loop
     ptr = (char*)malloc(1000000);
     printf("Allocating\n");
}

The variable ptr is assigned memory from the heap. However, this memory is not freed before another block of memory is assigned to it. Eventually, the application will run out of memory and terminate abnormally. Inefficient memory utilization!

There are also other issues like Losing the address, hidden memory leaks possible. This is what separates System Programmers from the herd of general application programmers. Your code must be efficient so as to avoid, at least basic bugs. There are no excuses!

The Heap and System Memory

The heap typically uses OS functions to manage its memory. The heap’s size may be fixed when the program is created, or it may be allowed to grow. However,the heap manager does not necessarily return memory to the operating system when the free function is called. The deallocated memory is simply made available for subsequent use by the application. Thus, when a program allocates and then frees up memory, the deallocation of memory is not normally reflected in the application’s memory usage as seen from the OS perspective.

Now let's understand the concept of Dangling Pointers, one of the most famous and important issue in the system programming community.

If a pointer still references the original memory after it has been freed, it is called a dangling pointer. The pointer does not point to a valid object. This is sometimes referred to as a premature free. The use of dangling pointers can result in a number of different types of problems,including:

  • Unpredictable behavior if the memory is accessed

  • Segmentation faults when the memory is no longer accessible

  • Potential security risks

These types of problems can result when:

  • Memory is accessed after it has been freed

  • A pointer is returned to an automatic variable in a previous function call

Let's understand this issue with an example. A subtle problem can occur when using block statements, as shown below. Here pi is assigned the address of tmp. The variable pi may be a global variable or a local variable. However,when tmp’s enclosing block is popped off of the program stack, the address is no longer valid.

int *ptr;
ptr = (int*)malloc(sizeof(int));
...
{
    int tmp = 10;
    ptr = &tmp;
}
//Now ptr is a dangling pointer
foo();

Most compilers will treat a block statement as a stack frame. The variable tmp was allocated on the stack frame and subsequently popped off the stack when the block statement was exited. The pointer ptr is now left pointing to a region of memory that may eventually be overridden by a different activation record, such as the function foo.

This all for this part. But before concluding, Will walk through one of the concept which I forgot in the earlier and one of the reader asked me to cover this concept.

Constants and Pointers

Using the const keyword with pointers is a rich and powerful aspect of C. It provides different types of protections for different problem sets.

Pointers to a constant

A pointer can be defined to point to a constant. This means the pointer cannot be used to modify the value it is referencing. Let's dive into one example to understand this concept clearly.

int n = 5;
const int m = 10;
int *ptr;    //Pointer to an integer
const int *constPtr;  // Pointer to a constant integer

ptr = &n;
constPtr = &m;

Here, we declared an integer and an integer constant. And, a pointer to integer and a pointer to an integer constant are declared and then initialized to respective integers. Let's explore different possible cases:

-> Dereferencing a constant pointer is fine if we are simply reading the integer’s value. Reading is a perfectly legitimate and necessary capability.

printf("%d\n", *constPtr);    //valid

We cannot dereference a constant pointer to change what the pointer references, but we can change the pointer. The pointer value is not constant. The pointer can be changed to reference another constant integer or a simple integer.

constPtr = &n;   //legal

We can dereference constPtr to read it. However, we cannot dereference it to modify it.

*constPtr = 30;   // '*constPtr*' : you cannot assign to a variable that is const

We can still modify num using its name. We just can’t use constPtr to modify it.

So, let me summarize this whole chaos(Actually it's not):

-> constPtr can be assigned to point to different constant integers.

-> constPtr can be assigned to point to different nonconstant integers.

-> constPtr can be dereferenced for reading purposes.

But

constPtr cannot be dereferenced to change what it points to.

Hmm. Now I got it. Congrats then!

The order of the type and the const keyword is not important. The following are equivalent:

const int *constPtr
int const *constPtr  //both are same

Constant pointers to nonconstants

We can also declare a constant pointer to a nonconstant. When we do this, it means that while the pointer cannot be changed, the data pointed to can be modified.

int number;
int *const ptr = &number;

With this declaration:

-> ptr must be initialized to a nonconstant variable.

-> ptr cannot be modified.

-> The data pointed to by ptr can be modified.

*ptr = 25; // We can dereference ptr and assign a new value

However,

const int k = 590;
int *const ptr = &k;

Warning: initialization discards qualifiers from pointer target type

If ptr referenced the const k, the constant could be modified. This is not desirable. We generally prefer constants to remain constant. Once an address has been assigned to ptr, we cannot assign a new value to ptr as shown below:

int num;
int age;
int *const ptr = #
ptr = &age;

// 'ptr' : you cannot assign to a variable that is const

There are also two other concepts in this Pointers and Constants.

  • Constant pointers to constants

  • Pointer to (constant pointer to constant)

Not so frequent but you can refer these concepts later.

That's it for today. Thanks for staying with me till now. Hope you are enjoying reading and learning something that you really think important.

Any kind of suggestions, feedback, or which part you liked the most, let me know. It will also helps me to write in a better way in the coming articles.

I'll come up with the next part soon. Till then, Good bye!

Keep Learning....