A Student's C Book: 1.3. Functions
Level 1. Introduction to C
1.3. Functions
I receive Inputs, You receive Output
What is a function? Roughly speaking, in the mathematical sense, it is a mapping from some input space to some output space such that no two different output values correspond to the same input value. In programming, we also call inputs the arguments given to a function. In fact, we even make a slight distinction between the arguments and the parameters of the functions. We call variable inputs, used in the output expression of the function, the arguments, and exact valued inputs, used in the final computation of the function's output, the arguments. Let's now recall a simple mathematical function as an example:
$$ f(x) = x + 1 $$
A computation function (i.e., a function in programming) is not too much different than a mathematical one. It has all the components that the mathematical one has:
- A name (it is named f in our example)
- Parameters or input variables (it is just a single parameter named x in our example)
- A (function) body (it is the expression on the RHS, which is x+1 in our example)
There are even more similarities. As one is able to get the output of the function by putting a number (i.e., an argument) in between the opening and closing parentheses that come right after the function name, the exact same can be done in programming using the C language.
$$ f(5) \text{ in math gives 6} $$
$$ f(5) \text{ in programming also gives 6} $$
We can even use additional variables to store the function output:
$$ y = f(5)\ \text{in math (now, y = 6)} $$
$$ int y = f(5);\ \text{in programming (now, y = 6)} $$
There is a slight difference between defining a mathematical and a computational function, though. While the writing $f(x) = x + 1$ on a piece of paper is enough for a mathematical definition of a function, we should type the following in a .c text file for the computation definition of the same function:
int f(int x){ return x + 1; }
First of all, we have to specify two things that were not included in the mathematical definition: (1) the type of each input parameter and (2) the type of output value. Assuming that the domain and codomain is the set of integers, it now becomes
int f(int x) = x + 1
Notice the keyword int in front of the function name. This keyword is put in front of the function name to tell the computer that the output value of the function f is an integer number. The second int keyword inside the parentheses tells the computer the type of parameter x.
Second of all, we use the equals sign (=) for variable-value assignments in programming. Instead of using the equality operator, programmers have decided to put everything inside a pair of opening and closing curly braces after specifying all the function parameters. This is shown below:
int f(int x){ x + 1 }
The last couple of changes to be made in order to get to the correct computational definition of our function in C are the following:
- We have to type the special word return in front of the expression that is going to be the output of the function. That is, int f(int x){ return x + 1 }
- C expects to see a semicolon (;) to make sure that our statement is properly closed (as one is expected to put a dot at the end of each finished sentence in English). That is, int f(int x){ return x + 1; }
So, the final and complete version of the computation definition of our simple function is given as follows:
int f(int x){ return x + 1; }
It is also worth mentioning that the output value of functions in programming is also called their returned value or simply a return value. Now, if we wanted to define a simple addition function $\mathtt{add}(x, y) = x + y$ that works for real numbers in C, it would look like this:
float add(float x, float y){ return x + y; }
Since the data type in front of the function name indicates the type of the function's output, it had to be float. We can logically infer this because we know the output expression, that is, $x + y$. Since both $x$ and $y$ are real numbers (they are called floating-point numbers in programming) that have the type float, adding two floating-point numbers will surely result in another floating-point number.
Simple pattern matching
Functions in C can do more than just returning a value. For example, a computational function could have its own set of (local) variables that can only be used by the function itself. Instead of returning $x + 1$ directly, something as shown below is possible:
int f(int x){ int y = x + 1; return y; }
This function behaves the same as its previous version. The difference is that this version memorizes a value computed from $x + 1$ and then returns/outputs the memorized value, whereas the first version just outputs the computed final value without memorizing it. In this case, it is meaningless to memorize the output value before returning it, but in the following case, it might be useful:
int f(int x){ int y = x + 1; printf("%d + 1 = %d", x, y); return y; }
In the version shown above, when the computer executes/calls this function, it will first memorize the final output value in the variable named y, then display its value on the screen, and finally return it as the output. Notice how both parameter x and the local variable y are used multiple times within the function. This is totally fine because inside the function's scope (i.e., function body), x will hold the number provided as an input argument, and y will hold the value of $x+1$ unless you explicitly modify their values later on.
As a side note, I should also mention that C does not care whether all the statements (i.e., your commands to the computer that can also be thought of as sentences in programming) are typed on a single line or multiple lines according to their original order. It also does not care about the number of multiple white spaces in between the symbols. For example, for C language, all the following statements mean the same:
int my_x;
int my_x;
int my_x ;
However, you should keep in mind that while there is no difference between putting a single white space or more in between the symbols, there is certainly a difference between putting one or more white spaces and not putting any. For example, int my_x; is not the same as int my x; for this exact reason. In the former case, my_x is the name of your integer variable, and in the latter case, my is the variable name, and the computer will complain about x because it will now understand what this symbol represents as it has been separated by a whitespace from the beginning of your variable name. Also note that the different numbers of more than one white space only matter inside a pair of double quotes (for example, printf("a b"); is not the same as printf("a b");). To demonstrate how C doesn't care about the lines, let's also rewrite our infamous $f(x) = x + 1$ function:
int f (int x) {
int y = x + 1;
return y;
}
Yeah, this looks much better now. It is more readable than putting everything on a single line and then indefinitely scrolling your window horizontally like a true freak. It is better for us that C does not care about such irrelevant white spaces and lines because these things should not affect how our function (or program in general) must behave. The order, however, matters. For example, we cannot write return y; on the line above the int y = x + 1; since the computer starts reading our program from the top left and it moves from left to right on the current line, then goes to the beginning of the next line right after the previous one and does the same there until it finishes reading the whole code. This is also how we read books, papers, and so on. That is why order matters. Imagine reading a page of a math book where the author speaks about how addition and integration work before mentioning anything about the numbers.
Pattern matching in C is simple and yet powerful enough to get the job done. Imagine the following writings on a piece of paper from one of your math classes:
$$ f(x) = x + 1 $$
$$ g(x, y) = f(x) + f(-2) \cdot f(y) $$
$$ a = 3 $$
$$ g(2, a^2) = ? $$
You could easily calculate the answer for the last line as all the information you need is given above it. Moreover, to calculate the output of $g(2, a^2)$, you would perform a simple pattern matching (even though you might not realize it). The pattern matching would go as follows:
- $g(2, a^2)$ uses $a^2$ as the second argument which is defined right above as $a = 3$. So, we need to calculate $g(2, 3^2)$ which is also equal to $g(2, 9)$;
- $g(2, 9)$ refers to the function definition $g(x, y)$, and therefore $x = 2$ and $y = 9$;
- Since we know that $g(x, y) = f(x) + f(-2) \cdot f(y)$, we can substitute the x and y with their corresponding values. That is, $g(2, 4) = f(2) + f(-2) \cdot f(9)$;
- The definition of f is also given to us in the first line, so, we can calculate $f(2) = 3$, $f(-2) = -1$, and $f(9) = 10$ by simply substituting its parameter x by $2$, $-2$, and $9$, respectively;
- Now, the output expression of $g(2, 4)$ becomes $g(2, 4) = f(2) + f(-2) \cdot f(9) = 3 + (-1) \cdot 10 = -7$;
- Therefore, $g(2, a^2) = -7$.
This simple pattern matching by using variable-value substitutions is what happens in C programming as well. Let's see a quick example in C (we'll assume that the numbers can be real numbers):
#include <stdio.h>
double f (double x) {
double y = x + 1;
return y;
}
double g(double x, double y){
double constant = f(-2);
return f(x) + f(y) + constant;
}
int main(){
double a;
printf("Please enter a = ");
scanf("%lf", &a);
double output = g(2, a*a); // we don't have square operator, so, we use multiplication
printf("g(2, a*a) = %lf", output);
return 0;
}
When the C program shown above is about to be executed by your computer, it will start reading the file line by line and from left to right first to ensure that everything is in the correct order and there are no syntactic errors in your writing. Then, if the program seems to be correct, it will be able to execute it starting from the main() function. So, the execution will go like this, starting from the first line inside the main() function:
- double a; tells the computer that we have a variable that holds a real number, but the value is not known yet.
- printf("Please enter a = "); tells the computer to display "Please enter a = " on the screen. This an optional step to let the user know that a value of a is expected from him/her.
- scanf("%lf", &a); tells the computer to get a real number from the user via the keyboard and keep it inside the variable named a. After the user enters a number, the computer will know what number the (double) a holds, and therefore, its value will become known.
- double output = g(2, a*a); tells the computer to compute the function $g(2, a^2)$ by using the pattern matching and all the information about the function definitions given above the main() function.
- printf("g(2, a*a) = %lf", output); tells the computer to display the text "g(2, a*a) = [some value here depending on the user input]" on the screen for the user to know the answer. For example, if the user entered the number 3 on the terminal, the output would exactly look like this: g(2, a*a) = -7
- return 0; tells the computer to return integer 0 as the output value of the main() function.
Hopefully, everything I have covered up until now is clear to you. If you still have some doubts about some parts of the post, make sure to open up a text editor and start playing with the examples given in this post. Rewrite them by hand (without using a copy-paste functionality), play with them, change them, run them, and make sure you are practicing along with reading this tutorial. You will learn C by practicing a lot. I mean A LOT!
Function calling and Recursion
Given the function definition $f(x) = x + 1$, one is surely able to "use" this function with any arbitrary real number as the argument. For example, one could use $f(-3)$ in any expression, and that would be perfectly fine by the rules of mathematics because $f$ is a well-defined function that maps -3 to -2. We get the mapped output (-2 in our example) by computing the output expression of the underlying function ($x+1$ in our example) with the given input (-3 in our example). This process is also called function calling or function invocation in programming. We invoke/call a C function by typing the function name, followed by the arguments. Let's take a look at the following example:
double f(double x){
return x + 1;
}
int main(){
double y_1 = f(-3); // function call with argument -3, output is -2.0
double y_2 = f(45.2); // function call with argument 45.2, output is 46.2
double pi = 3.1415;
double y_pi = f(pi); // function call with argument 3.1415, output is 4.1415
return 0;
}
Now, imagine we have a bit complicated function whose output values are computed in terms of its other output values. We can take the factorial function as an example. If we let the $\mathtt{facto}: \mathbb{N} \rightarrow \mathbb{N}$ denote the factorial function, then we can define it as follows:
\begin{cases}
1,& \quad\text{if}\ n = 0, \\
n \cdot \mathtt{facto}(n-1),& \quad\text{if}\ n > 0
\end{cases}$$
This function uses its own output values for different inputs whenever $n > 0$. In programming, we say, the function calls itself, and a function calling itself is called a recursive function.
unsigned int facto(unsigned int n){
if(n == 0){
return 1;
}
return n * facto(n-1);
}
Another recursive function is the Fibonacci function that generates the Fibonacci series $0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 45, \cdots$. Mathematically speaking, this function is defined as $\mathtt{fib}_{n+1} = \mathtt{fib}_{n} + \mathtt{fib}_{n-1}$ where $\mathtt{fib}_0 = 0$ and $\mathtt{fib}_1 = 1$. In C, we can define it as follows:
unsigned int fib(unsigned int n){
if(n < 2){
return n;
}
return fib(n-1) + fib(n-2);
}
Recursion is a very powerful concept in programming. It sometimes eases the job of programmers multiple folds. However, sometimes, it causes longer run times (due to the slow program execution) and may even result in crashes due to the reasons that I will not talk about in this post. Such things are going to be covered in the future post(s) of this tutorial series.
Table of Contents
- Preface
- Level 1. Introduction to C
- Hello, World!
- Basics
- Your computer can memorize things
- Your computer can "talk" and "listen"
- Compiling and Running programs
- Functions $\leftarrow$ you are here
- I receive Inputs, You receive Output
- Simple pattern matching
- Function calling and Recursion
- Control Flow
- Branching on a condition
- Branching back is called Looping
- Pointers
- Memory address of my variable
- Pointer Arithmetic
- Arrays
- Data Structures
- All variables in one place
- Example: Stack and Queue
- Example: Linked List
- Level 2. Where C normies stopped reading
- Data Types
- More types and their interpretation
- Union and Enumerator types
- Padding in Structs
- Bit Manipulations
- Big and Little Endianness
- Logical NOT, AND, OR, and more
- Arithmetic Bit Shifting
- File I/O
- Wait, everything is a file? Always has been!
- Beyond STDIN, STDOUT, and STDERR
- Creating, Reading, Updating, and Deleting File
- Memory Allocation and Deallocation
- Stack and Heap
- Static Allocations on the Stack
- Dynamic Allocations on the Heap
- Preprocessor Directives
- Compilation and Makefile
- Compilation Process
- Header Files and Source Files
- External Libraries and Linking
- Makefile
- Command-line Arguments
- Your C program is a function with arguments
- Environment variables
- Level 3. Becoming a C wizard
- Declarations and Type Definitions
- My pointer points to a function
- That function points to another function
- Functions with Variadic Arguments
- System calls versus Library calls
- User mode and Kernel mode
- Implementing a memory allocator
- Parallelism and Concurrency
- Multiprocessing
- Multithreading with POSIX
- Shared Memory
- Virtual Memory Space
- Creating, Reading, Updating, and Deleting Shared Memory
- Critical Section
- Safety in Critical Sections
- Race Conditions
- Mutual Exclusion
- Semaphores
- Signaling
- Level 4. One does not simply become a C master
Comments
Post a Comment