Introduction
Functions are independent blocks of code, which may be called any time. By having a independent block of code, it is possible for you to reuse it just by calling instead of rewriting your code any time you need the same functionality. Also, any time you need to update the same logic, you only have to update the code in one place, then call it anywhere.
Functions in C have some unique properties, altough, like in other languages, are a method to reuse code and manipulate the stack. Lets take a look at these properties.
Function declaration
In general, functions will contain the folowing declaration:
return_type function_name(parameters) {
function body;
}
Lets look a simple real example:
int main()
{
printf( "Sum function result: %d", sum(13, 16) );
return;
}
int sum(int arg1, int arg2) {
int add = arg1 + arg2;
return add;
}
If you do not declare a return type, the compiler assumes you are returning an integer.
Function prototype
There are 2 ways to give information to the compiler about functions. The first is declare it earlier in the same source file.
The second is trough a function prototype. A prototype will tell the compiler the number of arguments, the type of each argument, the function name and its return type. However, unlike the declaration, prototypes will not contain any body or code and will end with a semicolon:
return_type function_name(parameters);
int f(double arg1, char arg2);
Our second prototype example above tells the compiler to store information about a function name f, which has 2 parameters, one of type double, another with type char, and a return of type int.
I tend to think a function prototype is somewhat like an interface (if you come from java or c#, you probably know well how to work with an interface). An interface in short, will show you that you can work with such structure, but will hide details about how exactly that structure really works(dont worry too much about breaking your head with interfaces now, i plan to create some c# tutorials and cover in deep detail how to work with them, for now that concept will be more than enough since working with prototypes is a much simpler task). That simpler "interface" will give the compiler details about the function implementation.
If you do not apply a function prototype, the parameters and return value will be checked by the linker, not the compiler. Linking is a process of combining various pieces of code, like your #
includes, functions and files into a single file which can be loaded in memory as the last step(linking tends to be the last process of compilation, entering in action after preprocessing, assembling and compiling).
Return type
The datatype the function will return will be the first thing to be declared in a function. Altough optional, it is a good practice to declare them, otherwise the compiler will choose a default return of an integer.
char function() {
}
in this example we are returning a datatype char.
If you declare the function with a void return, it means it doesnt have to return anything to the caller, it can only execute the block of code called. You should be careful declaring returning compatible return values taking the risk to truncate the value or getting incorrect results.
Function arguments
Functions can receive parameters from the caller. These arguments can be a simple literal raw value or a variable you have declared. In any case, the function will work in a local area called stack, created everytime the code calls the function.
function(5, 120); //calling a function with arguments 5 and 120
function(parameter1, parameter2) { //defining a function with 2 parameters
}
We have been working with the simple main() function from the beginning and the general rules, like this, also apply to this important function which is the entry point in our C programs.
Function main() can also receive arguments:
int main(int argc, char *argv[]) {
}
-argc: amount of parameters you pass
-argv: an array containing the parameters you pass. The first element of this array is the program name
int main(int argc, char *argv[])
{
printf("argc: %d\n", argc);
printf("argv: %s\n", *argv);
return;
}
Altough by default argc is 1 and argv is program name, when executing trough visual studio or another IDE or compiler of your choice, you are not limited to those, being able to adjust while launching your program trough command line or even supplying default arguments in your IDE configurations.
Function definition parameters are like declared local variables. That means, after declaration, you dont have to declare its datatype, and actually the compiler will understand it as a redefinition:
f(int x, int y) {
int y = 15; \\wrong
x = 25; \\right
}
As said, the above code must be assigned only as y=15;x=25; Redeclare the datatype would be a wrong practice, since it has been defined in function definition.
Arguments can be passed by value or by reference to functions. When we pass arguments by value, a local copy is created in the memory area designed to work with the respective function(the stack) and a new scope is created, so the original arguments wont be altered. The copy of the arguments have local scope and will cease to exist when the function ends. When you pass by reference, even tough a local scope is created to your function, the arguments passed will actually retain any changes you make to them, even when the function ends, so the variables in the caller code will have to work with the new values you have set in the callee code. Lets take a look:
int main()
{
int x = 5;
int y = 7;
char numbers[2];
numbers[0] = 5;
numbers[1] = 7;
change_by_value(x, y);
change_by_reference(numbers);
printf("x: %d\n", x);
printf("y: %d\n", y);
printf("number 0: %d\n", numbers[0]);
printf("number 1: %d\n", numbers[1]);
return;
}
change_by_value(int x, int y) {
int temp = x;
x = y;
y = temp;
}
change_by_reference(char n[]) {
char temp = n[0];
n[0] = n[1];
n[1] = temp;
}
Note how x and y have been passed, exchanged values with each other inside the function, and when asked to print its values right below the function, they keep its original values. Then look what happens when you pass by reference as an array. When you exchange the values of the first and second elements of the array inside the function, and ask to print these below the function, those values will not be the original values, but the values you have set in your function.
Observation: in the change_by_reference we have passed our argument as an array. The selected notation is well suited when you dont know, or dont want to worry about the array size. In this context, you also have the option of passing your array as a pointer:
change_by_reference(char *n) {
}
A third option is the situtation where you already know the size of your array, passing it as an argument:
change_by_reference(char n[2]) {
}
Like we said, arguments are passed by value(so the value which will be worked inside the function is a copy) and if you want to change the original value you have to pass by value(using arrays or pointers). If you want to work and modify the original value inside the function, you also have the option of declaring global variables. However, when you change the value of a global variable, this will be the value of this variable trough all the scope of your program. Lets declare a global variable, printf its value insde the function, then change its value inside the function, then print its value again outside the function, after the function being called:
int global_variable = 22;
int main()
{
function();
printf("global variable outside function after change: %d \n", global_variable);
return;
}
function() {
printf("global variable inside function before change: %d \n",global_variable );
global_variable = 17;
}
Now any modification you do to this global value in any piece of the code, will be reflected trough all the code. Do not abuse this technique, since it will occupy memory as long as your code is alive, and also become harder to debug the more you change its values and complex your code becomes and grow.
Variable arguments
In some cases, you might want to define a function with no defined amount of parameters. We have been working with one of these functions since the beginning, our printf() function:
printf("%d, %d, %d", arg1, arg2, arg3);
To declare such a function, you must have at least one (or more) defined parameter, followed by ellipsis(...):
f(int arg, ...) {
}
Your function now contain a variable amount of arguments, stored in an array like structure, which is represented by the ellipsis(...). To manipulate this structure, you have now to include the "stdarg.h" library, which comes with 4 defined commands for this task:
Lets look at how to work with these 4 commands and the arguments they must receive in parenthesis:
Its easy to iterate trough each elemet of a function with variable arguments:
#
include <stdio.h>
#
include <stdarg.h>
int main()
{
vararg(5, 4, 12, 6, 10, 7);
return;
}
vararg(int narg, ...) {
va_list list;
va_start(list, narg);
for (int e = 0; e < narg; e++) {
printf("element %d is %d \n", e, va_arg(list, int));
}
va_end(list);
}
Just take note such functions have limitations. The first you might have noted is, you cant start the list without a first defined argument passed to your function, in this case, the int narg parameter is the defined. The macros cannot actually determine how many arguments are there neither what datatype they belong to. Thats why this task is up to you. The variable arguments also must be acessed from start to finish, and even tough the iteration can stop early, you cannot begin the iteration from the middle of the list. Since these kind of functions cannot be protyped, the variable arguments will then be promoted to the default datatype(char will be promoted to int, float to double and such).
Recursion
Recursion id the ability of the function to call itself. Every recursive function must have a limiting case so it knows when to stop calling itself. Without one, it can easily enter in a infinite loop. To begin our explanation about recursion, lets start with the old factorial problem, altough it offers no benefit solving this kind of problem with recursion, i believe its a good way to begin a practical demonstration:
int main()
{
int number = 6;
printf("Factorial of %d is %d \n",number, factorial(number));
return;
}
int factorial(int arg) {
if (arg <= 1) {
return 1;
}
else {
return arg * factorial(arg - 1);
}
}
Lets begin our analisys of this recursion function with the important things to take note. First note our limiting case, a condition or state it must enter to end recursion. In this factorial function, the limiting case is receiving an argument lesser equal to 1, which will make our function return the value 1 to the caller ending the function.
The second point is the hability of the function to call itself. It happens everytime the argument is greater than 1. Our function then will return to the caller, which is itself a calculation of the argument multiplied by the calculation of the factorial function receiving as argument the previous argument - 1, and this will happen on and on decreasing the value 1 by one until the argument is 1 and the function ends returning 1.
Next, note how recursion functions tend to stay "open" until the final calculation, or limiting case is performed. That means more than one instance of the function will depend of the other until the last calculation is performed.
Parameter arg may look like its being mixed, but actually, everytime you call a function, even if it the same, a new space is allocated on the system stack, so its like this variable has got a new context and a new scope separated from the one it was called with:
Putting my final toughts about recursion, one would wonder when actually to use them. Recursion has a tendency of being memory costly, and everytime a function call itself, it must allocate stack memory for the new call, the new variables, saving registers and state from previous calls, all of that taking some overhead depending of your application or problem to solve. I personally would use them in situations where the the solution to the problem is way more intuitive using recursion instead of iteration, for problems where you dont need to do many recursions or problems which are more efficient using recursion, doing lesser amount of calculations than an iteration, or the rare exceptions where you really need to create a different local scope. Otherwise, you should think about trying to use iteration whenever is possible, sticking at the same stack space when is needless to allocate another.
Cool. Ironically, I stumbled on this testing a bot :)