Lesson 12 - Functions in the C language
In the previous exercise, Solved tasks for C lessons 10-11, we've practiced our knowledge from previous lessons.
Lesson highlights
Are you looking for a quick reference on declaring C functions instead of a thorough-full lesson? Here it is:
Declaring functions:
{C_IMPORTS}
void greet(void)
{
printf("Hi, welcome! \n");
}
int main()
{
greet(); // calling the function
greet(); // calling it again
return 0;
}
A function taking parameters and returning a value:
{C_IMPORTS}
int rectangle_area(int width, int height)
{
int result = width * height;
return result;
}
int main()
{
printf("The area of the rectangle is %d cm^2", rectangle_area(10, 20));
return 0;
}
Recursion when a function is calling itself:
{C_IMPORTS}
int factorial(int x)
{
if (x == 1)
return 1;
return x * factorial(x - 1);
}
int main()
{
printf("%d", factorial(10));
return 0;
}
Would you like to learn more? A complete lesson on this topic follows.
Today's C lesson is about a very important topic - functions. We already know
that we write code into the main()
function. This was all well and
good for our small educational programs which usually could only do a single
thing. However, consider writing a program which is thousands of lines long. I'm
sure you agree it'd be very hard to work with such a "noodle" of code if it was
all in a single file and a single function. Even more so, if we wanted to
perform the same command sequences at multiple places in our program. In this
case, we'd have to copy it all over again or to jump from one program place to
another. Both options are very confusing.
Functional decomposition
We sometimes refer to splitting a program into multiple functions as a functional decomposition. Don't let the term intimidate you, we'll just think about what our application needs to do and then create a function in our source code for each of those tasks. In real-life applications, we usually create auxiliary functions as well. For example, a function for printing the application menu, or splitting a complex function to various simpler functions to keep the program readable.
Functions are sometimes called subroutines or subprograms. If a function
doesn't return a value (more on this further along), it may be referred to as a
procedure in some programming languages. Functions for larger applications,
where there is a lot of them, are gathered into multiple modules/libraries. You
know these very well already, e.g. from writing
#include <stdio.h>
which loads the library (module) for
working with standard input/output (with the console). Similarly, mathematical
functions are gathered in the math.h
system library. We'll also
learn to create said modules/libraries later on.
Creating functions
A function is a logical block of code which we write once and then call it
multiple times without needing to write it all over again. We'll declare
functions in the global scope, somewhere above the main()
function.
They'll look similar to main()
. Let's add a function to our source
code which will write "Hi, welcome!"
.
We'll show the entire source code just to be illustrative:
#include <stdio.h> #include <stdlib.h> void greet(void) { printf("Hi, welcome!\n"); } int main(int argc, char** argv) { return (EXIT_SUCCESS); }
The first void
in the function definition specifies that the
function doesn't return any value. The second void
is there for a
similar reason. It indicates that the function doesn't have any input
parameters. Now, we have to call the function to execute it. Of course, we'd
only be able to do so after we declare it, otherwise, the compiler wouldn't
recognize the function (that's why we declared our function before
main()
). Add the following line to main()
:
#include <stdio.h>
#include <stdlib.h>
void greet(void)
{
printf("Hi, welcome here!\n");
}
int main(int argc, char** argv)
{
greet(); // calling the function
return 0;
}
The result:
Console application
Hi, welcome!
Functions with parameters
A function can have any number of input parameters (they're sometimes called
arguments) which we write into the parentheses in its definition. We influence a
function's behavior by parameters. Consider a situation we want to greet our
users by their names. So let's extend our function of a name
parameter and specify it later with a concrete value when calling the
function:
void greet(char name[]) { printf("Hi %s, welcome here!\n", name); }
Now, we'll modify the calling of the function in main()
like
this:
#include <stdio.h>
#include <stdlib.h>
void greet(char name[])
{
printf("Hi %s, welcome!\n", name);
}
int main(int argc, char** argv)
{
greet("Carl"); // calling the function
return 0;
}
If we wanted to greet multiple people, we wouldn't have to write
printf("Hi ...
for each of them. Instead, we'll simply call our
function:
#include <stdio.h>
#include <stdlib.h>
void greet(char name[])
{
printf("Hi %s, welcome!\n", name);
}
int main(int argc, char** argv)
{
greet("Carl");
greet("David");
greet("Mary");
return 0;
}
The result:
Console application
Hi Carl, welcome!
Hi David, welcome!
Hi Mary, welcome!
The function's return value
A function can also return a value. Let's leave our greeting example and
create a function for computing the area of a rectangle. Furthermore, we'll make
it so we're able to use the result in another calculations. Therefore, we won't
write the result but return it as the return
value. Every function
can return 1 value using the return
command which will also
terminate the function, so any other code after it won't be executed. We specify
the data type of the return value before the function definition. Add the
following function to your program:
int rectangle_area(int width, int height) { int result = width * height; return result; }
In real-world applications, our function would probably compute something
more complex, so it'd actually be worthwhile to implement. However, as an
example, a simple rectangle will serve just fine. We name functions using
lowercase letters, whole words and using under_scores instead of spaces.
Although the C language is full of abbreviations, you should avoid them at all
costs. For example, birth_date()
is much clearer than
bird()
which makes it hard to tell what it even does at the first
sight.
If we wanted to print the area of a rectangle now, we'd simply call our
function straight in the printf()
function. First, the rectangle's
area will be computed. Then, this value will be returned and passed as an input
parameter to printf()
which will print it. Let's try it out by
entering 10
and 20
cm as width and height:
#include <stdio.h>
int rectangle_area(int width, int height)
{
int result = width * height;
return result;
}
int main(void)
{
printf("The area of the rectangle is: %d cm^2", rectangle_area(10, 20));
return 0;
}
Console application
The area of the rectangle is: 200 cm^2
If you find it confusing, feel free to use an auxiliary variable:
#include <stdio.h>
int rectangle_area(int width, int height)
{
int result = width * height;
return result;
}
int main(void)
{
int area = rectangle_area(10, 20);
printf("The area of the rectangle is: %d cm^2", area);
return 0;
}
However, we didn't decide to return the result as the function's return value to simply print it. Let's take advantage of this and compute the sum of the areas of two rectangles:
#include <stdio.h>
int rectangle_area(int width, int height)
{
int result = width * height;
return result;
}
int main(void)
{
int total_area = rectangle_area(10, 20) + rectangle_area(20, 40);
printf("The sum of the areas of the rectangles is: %d cm^2", total_area);
return 0;
}
The result:
Console application
The sum of the areas of the rectangles is: 1000 cm^2
Regarding previous exercises we did earlier on in the course: you may try to modify some of them and split them up into functions. According to good software design practices, a source code should always be split up into functions (and ideally into libraries/modules, more on this later on) to keep it clear. We omitted this at the beginning to keep things simple, but now, please, keep this in mind
The main advantage of using functions is clarity and keeping code shorter (we can write something once and call it a hundred times from multiple places in our program). If we decide to modify the function, we would only have to do it at one place and this change will affect all of the function calls immediately which decreases the possibility of making an error, significantly. In the greeting example, we could simply change the greeting text once in the function and it'd affect all three of the calls. If we didn't have the code in a function, we'd have to modify 3 sentences and it's very likely we'd make a typo somewhere.
Recursion
To sum it all up, let's take a little peek into an advanced topic - recursion. A recursive function is a function which calls itself in its body. Such a function needs some information to determine when it should end. Otherwise, it'd call itself, then it'd call itself again and this would end with the program terminating due to insufficient memory. Recursion is used very often in various algorithms.
In functional programming languages, recursion is used instead of loops. Take
for example, a for
loop which sums up the numbers from
1
to 10
. We could achieve the same result with
recursion as well. The function would call itself over and over with a number
which increases by one 1 or it would terminate itself (depending on what the
current number is).
int loop(int current_index, int final_index, int sum) { if (current_index == final_index) return sum; return loop(current_index + 1, final_index, sum + current_index); }
We'd call the function like this:
#include <stdio.h>
int loop(int current_index, int final_index, int sum)
{
if (current_index == final_index)
return sum;
return loop(current_index + 1, final_index, sum + current_index);
}
int main(void)
{
printf("%d", loop(0, 10, 0)); // beginning of the recursion
return 0;
}
We could do the same using a for
loop:
{C_CONSOLE}
int sum = 0;
int a;
for (a = 0; a < 10; a++)
sum += a;
printf("%d", a);
{/C_CONSOLE}
As you can see, reading a code using recursion is not as easy as reading a code using loops. But that's not all. Using recursion creates additional memory requirements since parameters and return values have to be passed over and over again. Generally speaking, most programs which use recursion can be rewritten to not do so. Let's create a sample program that computes a factorial. We'll make versions with and without recursion.
int factorial(int x) { if (x == 1) return 1 return x * factorial(x - 1); }
We'd call the function like this:
#include <stdio.h>
int factorial(int x)
{
if (x == 1)
return 1;
return x * factorial(x - 1);
}
int main(void)
{
printf("%d", factorial(10));
return 0;
}
Here's the alternative using loops:
{C_CONSOLE}
int result = 1;
int x = 10;
int i;
for (i = 2; i <= x; i++)
result *= i;
printf("%d", result);
{/C_CONSOLE}
You may run into recursion in existing source codes or at job interviews. However, it's probably better to avoid recursion, at least for now. Recursion is also able to waste the entire call stack quickly and terminate the program. Furthermore, it's difficult to understand. If you're confused by it, you're going to get more acquainted with it in the algorithms course where there's enough space to explain it in further detail.
In the next lesson, Solved tasks for C lesson 12, we'll introduce one of the basic aspects of the C language - structures.
In the following exercise, Solved tasks for C lesson 12, we're gonna practice our knowledge from previous lessons.