Avoiding potential pitfalls while programming in C

C is one of the older, simpler and widely used programming language. While programming in C is simple enough, a piece of code which is not written properly without proper programming language conventions could lead to non-functional program behavior. In embedded and systems programming, even a badly written function in C could cause the process, which calls this buggy function, to crash and render the entire system unusable. Moreover, programs written without proper error checks could lead to unexpected program functionality which may appear in form of software bugs in the test cycles. Hence it is vitally important that we follow good programming language coding conventions. Following are some C programming language coding conventions that prove useful to developers:-

1. Checking pointers for NULL value before de-referencing them
This is of course the most basic type of error checking that must be followed. Often, developers ignore checking for NULL values for a pointer before de-referencing them or accessing some value pointed to by that pointer. De-referencing a NULL pointer could lead to unexpected behavior in the program like crashing of the program. In the code snippet below, the C program would crash at line number three with signal 11 (segmentation fault) , if the pointer "ptr" is NULL in line number one.
1:  void function1 (struct node* ptr)  
2:  {  
3:     printf("data = %d", ptr->data);  
4:     .  
5:     .  
6:  }  
We should always check the sanity of the pointer before de-referencing the pointer. The function in the above code snippet could be changed in the following manner. In the code snippet, we simply return from the function, if "ptr" passed in the function is null. You could choose to return error values from the function if the return type of the function is not void.
1:  void function1 (struct node* ptr)  
2:  {  
3:     if (!ptr) {  
4:       return;  
5:     }  
6:    
7:     printf("data = %d", ptr->data);  
8:     .  
9:     .  
10:  }  
Often the de-referencing of a NULL pointer, can be exposed in static analysis tools like klocwork.

2. Checking for boundary values for parameters passed into the function
Often, we sometime do not check for range of valid values for a variable passed into a function. For example, if in a function we pass the index which is used to access an element of an array. If we do not check for the range of values for the index, then we may end up accessing some memory location which may not be in the memory space of your program. Consider, the following code snippet in which the pointer "ptr" points to an array which has 100 integers.
1:  void function2 (int* ptr, int index)  
2:  {  
3:     if (!ptr) {  
4:       return;  
5:     }  
6:    
7:     printf("data at index: %d = %d", index, ptr[index]);  
8:     .  
9:     .  
10:  }  
If the index is either less than 0 or greater than or equal to 100, then the program could potentially access some memory location which may be outside of the memory allocated for this program. If "index" is not within range, then the program could crash with signal 11 (segmentation fault) at line seven. The "function2()" could be modified in the following manner to check for the valid range of "index". If the value of "index" is outside of the valid range, then we could return from "function2()" even before de-referencing the element in the array pointed to by "ptr". Line 7-9 realize the validity check on "index" to make sure that the "printf" statement in line 11 gets executed only if "index" is within the valid range. You could choose to return error value from the function if the return type of the function is not void.
1:  void function2 (int* ptr, int index)  
2:  {  
3:     if (!ptr) {  
4:       return;  
5:     }  
6:    
7:     if ((index < 0) || ((index >= 100)) {  
8:       return;  
9:     }  
10:    
11:     printf("data at index: %d = %d", index, ptr[index]);  
12:     .  
13:     .  
14:  }  

3. Testing the return values of the library functions and other user defined functions
A lot of times, programmers overlook to test to return values of library functions or other user defined functions. Not testing the return values of the functions often leads to unexpected behaviors in the program. These unexpected behaviors are termed as software bugs. Gracefully handling of return values of a library and user defined functions makes the code more robust to failures. Consider "function3()" below, which allocates some memory using the popular library function "malloc()".
1:  void function3 ()  
2:  {  
3:    int* ptr = malloc(sizeof(int) * 100);  
4:    int i;  
5:      
6:    for (i = 0; i < 100; ++i) {  
7:      printf("data at index %d = %d", i, ptr[i]);  
8:    }  
9:    .  
10:    .  
11:  }  
However, the above function does not check for the return value of the library function "malloc()". In the event if the memory allocation fails, then "malloc()" returns NULL. In such a scenario, we are trying to de-reference a NULL pointer in line 7. The above function could potentially cause the program to crash if the "malloc()" at line 3 fails to allocate memory.

"function3()" could be modified so that we check for the return value of "malloc()" before proceeding to work with the memory allocated by it. We could check if the pointer returned by malloc() is a NULL pointer or not. If the pointer returned by malloc() is NULL, then we could simply return from this function. "function3()" could hence be modified in the following way with the NULL check on the pointer variable "ptr" in lines 6-8. You could choose to return error value from the function if the return type of the function is not void.
1:  void function3 ()  
2:  {  
3:    int* ptr = malloc(sizeof(int) * 100);  
4:    int i;  
5:      
6:    if (!ptr) {  
7:      return;  
8:    }  
9:    
10:    for (i = 0; i < 100; ++i) {  
11:      printf("data at index %d = %d", i, ptr[i]);  
12:    }  
13:    .  
14:    .  
15:  }  


Comments

Popular Posts