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:
- 1. Dynamic memory allocation
- 2. Debugging with gdb
- 3. Function pointers
- 4. Recursion in C
- 5. Typecasting and typedef in C
- What to learn next
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));
}
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
Compile the program using gdb compiler:
gcc main.c -g
Enter gdb mode:
gdb a.out
See source code:
list
Create a breakpoint:
gdb) break 6
gdb) run
Step one line:
gdb) step
Step one function:
gdb) next
Check variable's current value:
print {variable name}
Pressing
enter
while in gdb renters the previous command. Use this to quickly step through code blocks without retyping thestep
ornext
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 ;
}
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;
}
}
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
}
}
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 ) ;
}
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 ) ;
}
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 ) ;
}
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 from
int
→double
but you'll lose information if you convert fromdouble
→int
.
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;
}
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 (string
→ color
).
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 ) ;
}
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 ) ;
}
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;
}
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!
Top comments (2)
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).
Some comments may only be visible to logged-in visitors. Sign in to view all comments.