DEV Community

loading...
Cover image for Top 5 advanced C programming concepts for developers
Educative

Top 5 advanced C programming concepts for developers

ryanthelin profile image Ryan Thelin Originally published at educative.io ・11 min read

Due to the adoption of C as the go-to language for self-driving car development, C is making a resurgence in popularity and demand. Many industry veterans learned programming through C but haven't used it in years.

Today, we'll help you refresh your C knowledge or take your learning to the next level with the 5 most important advanced C concepts for developers.

Here’s what we’ll cover today:

Become a certified C programming master

Refresh your C knowledge fast and receive an Advanced C Certificate to amaze your next interviewer.

Advanced Programming Techniques in C

1. Dynamic memory allocation

Definition

In C, there are two types of memory allocation: static and dynamic. Static allocation is the more basic of the two and is allocated to the Stack at execution.

Once allocated, static allocation has a fixed size. Static allocation is used for any global variables, file scope variables, and variables with the static keyword.

Dynamic memory allocation is the more advanced of the two that can shift in size after allocation. This memory is stored in the Heap. Unlike the Stack, Heap memory has no variable size limitation.

Dynamic allocation allocates more memory as it's needed, meaning that you never have more memory allocated than you need.

Dynamic memory allocation also has performance downsides. Cached heap data is not adjacent like cached Stack data. As a result, retrieving heap data is often less efficient than retrieving Stack data.

Dynamic memory allocation also costs a large overhead. Variables cached to the Heap must have an associated pointer of equal size to locate the variable later.

For example, a 4-byte variable would also have an associated 4-byte pointer. This means that Heap data has roughly double the resource cost of Stack data.

While dynamic memory can scale up to allow for more data, it does not automatically deallocate memory when the amount of data is reduced. You must instead manually deallocate memory using the free() function.

You'll quickly find yourself with a memory leak error if you forget to deallocate your memory that can slow or even crash your program!

Uses

Dynamic memory's overhead cost is best used for select variables like scaling data structures. It's best to still use static allocation for the majority of variables to avoid slowing your program.

You should use dynamic memory when:

  • You don't know how much memory a structure will need beforehand
  • You want a data structure that can infinitely scale to match the input
  • You want to ensure that you never over-allocate memory in advance (automatic upward scaling)
  • You're using a linked list or structure object

Implementation

#include <stdio.h>
#include <stdlib.h> 
int main( )
{
    // Declare pointer variables
    int  *i ;
    float  *a ; 
    /* Allocate memory using malloc and store the starting 
    address of allocated memory in pointer variable*/
    i = (int*) malloc (sizeof(int));
    a = (float*) malloc (sizeof(float));
    // Declare employee structure
    struct emp
    {
        char name [20];
        int age;
        float sal;
    };
    // Declare structure pointer
    struct emp *e;
    e = (struct emp *) malloc (sizeof (struct emp));
}
Enter fullscreen mode Exit fullscreen mode

In lines 6,7, and 20, i, a, and e are created on the stack. All three are pointers. The memory chunks they are pointing to are created on the heap.

In lines 10-11, we use the malloc( ) library function to implement dynamic memory allocation. In dynamic memory allocation, both decisions—how much memory to allocate and where to allocate—are made during execution.

The number of bytes that should be allocated is passed to malloc( ) as an argument. malloc( ) allocates that much memory and returns the base address of the memory chunk allocated as a void pointer.

The void pointer returned by malloc( ) is appropriately typecasted (converted) into an integer pointer, float pointer, or struct emp pointer using the (target type) syntax.

2. Debugging with gdb

Definition

Linux is the most commonly used OS for C programming. Linux has a debugging command-line tool called gdb that will help you debug your program. Once installed, you can run your entire program using gdb and it will point out both logical and syntactical errors.

This allows you to:

  • Execute a program, one statement at a time
  • Stop an execution at any given line/function of the program
  • Print the intermediate values
  • Understand the detailed flow of execution

Uses

The most important commands in gdb are breakpoints, step, next, and variable value printing.

Breakpoints pause a program's execution at a designated time to allow you to evaluate the program status and variable values. This tool is used to break up larger sections of code into smaller chunks. You can then run these chunks to determine the general location of the bug.

Step lets you run code one line at a time. This command executes the next line of code then pauses for further instruction. You'll use this once you've narrowed the bug down to a smaller code chunk but not a specific line. As you step through the chunk, you'll eventually find which specific line causes the bug.

Next is similar to step but instead executes the next function rather than the next line. This is best used in tandem with breakpoints to debug function-heavy or branching programs. You can run each modular function and make sure each performs as expected.

Variable value printing used after a breakpoint, step, or next command to print the current value of the variable. Without gdb, you'd have to manually write print commands into your program after each time it interacts with the variable. Instead, gdb allows you to keep your program clear of cluttering print commands while still allowing you to track a variable's value through a program segment.

Implementation

Install:

apt-get -y install gdb
Enter fullscreen mode Exit fullscreen mode

Compile the program using gdb compiler:

gcc main.c -g
Enter fullscreen mode Exit fullscreen mode

Enter gdb mode:

gdb a.out
Enter fullscreen mode Exit fullscreen mode

See source code:

list
Enter fullscreen mode Exit fullscreen mode

Create a breakpoint:

gdb) break 6
gdb) run
Enter fullscreen mode Exit fullscreen mode

Step one line:

gdb) step
Enter fullscreen mode Exit fullscreen mode

Step one function:

gdb) next
Enter fullscreen mode Exit fullscreen mode

Check variable's current value:

print {variable name}
Enter fullscreen mode Exit fullscreen mode

Pressing enter while in gdb renters the previous command. Use this to quickly step through code blocks without retyping the step or next commands each time.

3. Function pointers

Definition

Function pointers are another way to call a created function. The standard function call is with the functions name and parentheses, function1(). Function pointers allow you to call a function with the function's memory location, (*f)().

To do this, you must first store the location of the desired function in a pointer variable. Once you do this, you can use the function pointer in place anywhere you'd use the standard function call.

Uses

Function pointers allow you to pass functions as arguments to other functions or data structures. You can therefore avoid the use of temporary variables and instead simply pass the function's result directly to the next function.

Function pointers also prevent repeated code for different data types. For example, normally to create a quicksort program for different data types you'd need to write type-specific sort functions like quicksort_integer or quicksort_char.

With function pointers, you could instead create a function that takes pointers as arguments and sort by any field that you have a comparator for.

Finally, function pointers allow you to create structs populated with functions rather than variables. For example, you could create an array of functions by storing each function pointer as an element.

Use function pointers when you want to plug one function into another or when you want to ease type restrictions when using data structures.

Implementation

# include <stdio.h>
void dbl ( int * ) ;
void tple ( int * ) ;
void qdpl ( int * ) ;
int main( )
{
    int  num = 2, i ;
    void ( *p[ ] )( int * ) = { dbl, tple, qdpl } ;
    for ( i = 0 ; i < 3 ; i++ )
    {
        p[ i ]( &num ) ;
        printf ( "%d\n", num ) ;
    }
    return 0 ;
}

void dbl ( int *n )  
{
    *n = *n * *n ;
}

void tple ( int *n )  
{
    *n = *n * *n * *n ;
}

void qdpl ( int *n )  
{
    *n = *n * *n * *n * *n ;
}
Enter fullscreen mode Exit fullscreen mode

In this program, we have defined three functions. The dble, tple, and qdpl functions receive the address of an int, doubles/triples/quadruples the int value, respectively, and returns nothing.

In the main function, we store the addresses of these functions in an array and, then, call them one by one by iterating through an array.

In line 8, we create an array of a function pointer and store the addresses of dble, tple, and qdpl in an array.

In line 11, we call the dbl, tple, and qdpl functions by their addresses by iterating through an array.

Keep learning about Advanced C

Refresh your C skills quickly, without scrubbing through tutorial videos. Educative's courses are skimmable and feature interactive coding exercises designed specifically for developers like you.

Advanced Programming Techniques in C

4. Recursion in C

Definition

Recursion is when a function contains a call to itself. Recursive programs will often contain commands and operations above the recursive call that are repeated in each recursive iteration.

In C, you can recursively call both user functions and main(). Recursion can replace traditional loops in many circumstances. Like loops, recursive programs can loop infinitely if programmed without an exit condition.

Typical recursive calls either come within an if segment or an else segment.

For if type recursive programs, the recursive call is held in the if segment. The program will continue to iterate until the if becomes false. Then, the program will progress to the else segment that contains the return statement.

For example:

int example()
{
  if (n != 0){ 
    // do something
    // something more
    example(..); //recursive call
  }
  else 
  {
    return 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the else type programs, the recursive call is held in the else segment along with operations that affect the program conditions. Any else type program iterates until the if statement becomes true.

The difference between these program types is if we're progressing away from a starting state or toward a target state.

For example:

int example ()
{
  if (n == 0){
      return 1; 
  }
  else 
  {
    // do something
    // something more
    example(..); //recursive call
  }
}
Enter fullscreen mode Exit fullscreen mode

Uses

Recursion is used to reorient problems to focus on the desired condition rather than the number of iterations to achieve it.

With loops, you must program the number of iterations the program will take. This means you have to know how many iterations are needed for a given input or make a subcomponent that can determine the number of iterations.

However, you're ultimately concerned with achieving a certain goal and the iterations are your tool to do so, not visa versa.

Recursion allows you to design programs that foreground that goal-based thinking. Recursive programs repeat however many times it takes to achieve their goal, whether that be to deviate from a starting state or to reach a target state.

You shouldn't use recursion for everything because it uses more resources than iterative loop solutions. In general, recursion is best used for solutions that:

  • Can be solved using answers from many smaller sub-problems
  • Require repeating of the same steps multiple times
  • Pursue certain program states.

Implementation

This C program finds the factorial of a passed integer argument. This is an else type recursive program.

#include <stdio.h>
int refact ( int ) ;

int main( )
{
    int  num, fact ;
    num = 4;

    fact = refact ( num ) ;
    printf ( "Factorial value = %d\n", fact ) ;

    return 0 ;
}

int  refact ( int  n )
{
    int  p ;

    if ( n == 0 )   
        return ( 1 ) ; 
    else
        p = n * refact ( n - 1 ) ; //recursive call

    return ( p ) ;
}
Enter fullscreen mode Exit fullscreen mode

5. Typecasting and typedef in C

Definition

Typecasting is a special type of operation in C that one data type to another. Typecasting can be done either implicitly or explicitly.

Implicit typecasting is where the compiler automatically converts data. Implicit typecasting is easier to implement but often loses information during the conversion process.

// implicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( "%d\n", c ) ;
    // Store result of integer division in variable of float type
    d = a / b ;
    printf ( "%f\n", d ) ;
}

Enter fullscreen mode Exit fullscreen mode

Here, d should have the value of 2.500000 but it's actually 2.000000. The decimal value was truncated during automatic conversion as both a and b are integers and therefore return an integer value from their division.

Explicit typecasting solves this problem. With this form of typecasting, you instead manually convert the data and therefore can select an earlier conversion point.

// explicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( "%d\n", c ) ;
    // Store result of float division in variable of float type
    d = ( float ) a / b ;
    printf ( "%f\n", d ) ;
}
Enter fullscreen mode Exit fullscreen mode

On line 12, d = ( float ) a / b, a is typecast forcibly into a float before performing the division operation. The operation, therefore, maintains all decimal value from the operation. As you can see, the value of d is the 2.500000 when explicitly cast.

Only convert from less specific data types to more specific data types to avoid data loss. For example, you'll keep all information if you convert fromintdouble but you'll lose information if you convert from doubleint.

Uses

You can use typecasting to create reusable code. If you convert from lesser to greater data, you can use the same values as arguments for multiple functions even if they call for different data types.

You can also take the output from the first of a function sequence and cast it to a new type to fit the needs of whatever next function it hits. Typecasting allows you a greater degree of freedom when using the strict data types in C.

You can also typecasting to assign new names to existing data types or user-defined structs with the typedef keyword. This does not un-assign the original name but simply adds another way to refer to that same type.

For example:

#include<stdio.h>

int main() {
    // Declare variables
    unsigned long int i, j;
    // Give new name to unsigned long int
    typedef unsigned long int ULI;
    // Declare variables of type ULI
    ULI k, l;
}

Enter fullscreen mode Exit fullscreen mode

Here we assign the name ULI to the unsigned long int type. We can then declare variables of type ULI or unsigned long int and achieve the same effect.

This can help readability as you could assign names to data types to shorten declarations or more accurately represent what values they'll hold (stringcolor).

Implementation

Implicit typecasting:

// implicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( "%d\n", c ) ;
    // Store result of integer division in variable of float type
    d = a / b ;
    printf ( "%f\n", d ) ;
}
Enter fullscreen mode Exit fullscreen mode

Explicit typecasting:

# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( "%d\n", c ) ;
    // Store result of float division in variable of float type
    d = ( float ) a / b ;
    printf ( "%f\n", d ) ;
}
Enter fullscreen mode Exit fullscreen mode

Typedef for user struct:

#include<stdio.h>

int main() {
    // Declare structure
    struct a
    {
        char *p;
    };
    // Give new name to struct a
    typedef struct a FILE;
    FILE *fs , *ft;
}
Enter fullscreen mode Exit fullscreen mode

What to learn next

Congratulations on completing your first steps into advanced C programming! We covered the 5 most important concepts to improve your C programming skills.

However, these are just the surface of what this long developed language has to offer. Some good next topics to study are:

  • Creating libraries
  • C with Linux/Unix
  • Bits and bitwise operators
  • Variable argument lists
  • Structures

Educative's new course Advanced Programming Techniques in C will teach you all these topics and more with interactive exercises and expert tips. By the end of the course, you'll have mastered this classic programming language, completed dozens of practice projects, and even have a certificate to use for a resume or portfolio.

Happy learning!

Continue reading

Discussion (2)

pic
Editor guide
Collapse
ac000 profile image
Andrew Clayton

P.S.A

Don't cast the return value of malloc(3) et al in C (it's not needed and can actually hide bugs).

Collapse
mmi profile image
Georg Nikodym

With respect, I was unable to read past the dynamic memory section because it had so many problems.

Readers, caveat emptor.