Functions

Every single concept we have covered so far appears in almost every programming language you may learn. There is only one left and that is the concept of functions.
Let’s first look at an example.
int array[] = {1, 2, 3};
for (int i = 0; i < 3; i++)
{
  array[i] += 2;
}

The example illustrates the last thing you learned in the previous lecture. Every single element in the array is increased by 2. Now imagine we had two arrays and we wanted to do the same process on both of them – increase each element by two. If the arrays had the same number of elements, this would be a piece cake:

int array[] = {1, 2, 3};
int second_array[] = {3, 4, 5};
for (int i = 0; i < 3; i++)
{
  array[i] += 2;
  second_array[i] += 2;
}

Now the problems begin if we add a third array whose elements also need to be increased by two, but the array has more than 3 elements. We can overcome this problem by adding a second loop:

int array[] = {1, 2, 3};
int second_array[] = {3, 4, 5};
int third_array[] = {5, 6, 7, 8};
for (int i = 0; i < 3; i++)
{
  array[i] += 2;
  second_array[i] += 2;
}
for (int i = 0; i < 4; i++)
{
  third_array[i] += 2;
}
This can be a solution, but there’s something wrong here. We started using loops to reduce the amount of similar code, but now we’re increasing it. These two loops have identical logic, but the difference is the number of elements. At this point, we need some new way to structure our code and we will use functions.
Functions are blocks of code which can be called at any point in the code after they are declared.
Imagine functions as a black box – we put the adequate information or data in the box and the box gives us some result back. The only difference here is that we can actually create and see inside of the black box. So how do we create these functions? There are two concepts you need to know which define a function:
  • The function’s signature or prototype (consists of the function’s name, return value and its parameters) – imagine it like the shape of the black box. We know what is the name of the function, what are the inputs and what is the output
  • The function’s body or definition (consists of the function’s code block) – imagine it like the inside of the black box. This is where all of the logic of the function is stored (the same way a loop has a body)
Generally, this is the pseudocode for a function definition:
<return_type> function_name(<param_type> arg1, <param_type> arg2, ...)//function's prototype
{
  //function's definition
  some_instructions;
  return some_value; //value must be of the type <return_type>
}
//function_name is the name of the function
//<return_type> is the return value of the function (or the output)
/*"arg1" and "arg2" are the parameters of the function. A function may have as many parameters as
you like*/
//<param_type> can be any mentioned data type or user defined type (more about this later)
//"some_instructions;" is some code the function may have
/*"return some_value" tells us that the return (or output) of the function will be whatever is
stored in the variable "some_value"*/
Now let’s look at a simple example. Imagine you wanted to make a function which adds two integers. First we need to determine the inputs and outputs. The inputs could just be two integer variables and the output should be an integer. Lastly, the body of the function would only contain the addition of these elements and an instruction to supply that result to the output. The last part is done using the keyword return.
You might have had an eureka moment here. Every time we wrote a program in C++, at the end of the main function we wrote return 0, which literally means that every time we ran any program in C++, the function main, which contains all of our code, always returned 0.
Now let’s write out the function for addition, calling the function add.
int add(int a, int b)
{
  int c = a + b;
  return c; //c must be the same type as <return type>. int in this case
}
Let’s go through each thing here word-by-word:
  • the int keyword at the start means that the function will return an integer
  • add is the name of the function
  • in the brackets we say what should the user give to the function when he wants to call it (arguments of the function)
    • in this example, there are two integer arguments named a and b
  • The body of the function contains two instructions
    • A new integer variable “c” is defined as the sum of the arguments
    • return c is called, which means the function outputs the value of c and finishes
Okay, we defined a function, but how do we use it? Anytime you want to use a function you just need to write the name of the function and in the brackets that follow write the arguments of the function separated by a comma (if there is more than one argument).
Let’s try and use the add function in our main program.
#include <iostream>

using namespace std;

int add(int a, int b)
{
  int c = a + b;
  return c;
}

int main()
{
  int x = 3;
  int y = 4;
  int sum = add(x,y); //calling function "add" and putting its return value into "sum"
  int another_sum = add(1, 2); /* You can also supply the function with just the type values
  instead of first storing them inside a variable*/
  cout << "sum = " << sum << endl;
  cout << "another_sum = " << another_sum << endl;
  return 0;
}

Output:

sum = 7
another_sum = 3
Now let’s get back to our first problem. Hence our arrays all have the same logic:
  • Go through each element
  • Increment each element by some number
We can make a function doing just that! Firstly we need to specify what the function should do:
  • The input should be some information about the array and the type of manipulation we want to do on each element:
    • The actual array
    • its size
    • The incrementing value
  • The output isn’t specified – void
The first part (the input) is straightforward. We can just copy the code we already have, with some changes. The second part (the output) on the other hand is a bit puzzling. We said that every function has a return type, but in this case we don’t need to return anything. For this reason, there is a special data type included in C++ which is used in function declarations (among other things) when the function doesn’t return anything and it’s called void. So, because we don’t need an output, our function can return void.
Let’s write it out:
void increment_elements(int array[], int number_of_elements, int increment)
{
  for(int i = 0; i < number_of_elements; i++)
  {
    array[i] += increment;
  }
}

Before going any further, we would like to address a subtly detail about functions. We mentioned that a function has a declaration and a definition. The question now is: Can they be separated? The answer is yes. Let’s say we wanted to rewrite the first example using a function, but still had no idea how to write the function’s body at the moment. Can we use the function in the main program and write the implementation for the function later? You guessed it! If a function needs to be used before it’s defined, the only thing that needs to be written before calling the function is the function’s prototype (declaration).

#include <iostream>

using namespace std;

void increment_element(int array[], int number_of_elements, int increment);

int main()
{
  int array[] = {1, 2, 3};
  int second_array[] = {3, 4, 5};
  int third_array[] = {5, 6, 7, 8};
  increment_elements(array, 3, 2);
  increment_elements(second_array, 3, 2);
  increment_elements(third_array, 4, 2);
  return 0;
} 

This can also be written in a more compact way! If a function before it’s use is just declared, you can remove the name of the parameters and just keep the data types of the parameters:

//just the prototype
void increment_element(int [], int, int);
Let’s now rewrite the first example using all of the concepts covered so far:
  • We want to rewrite our code so that instead of writing individual loops for our arrays, we want to call a function
  • We will write just the function’s prototype (declaration) and use it in our main program where it’s needed
    • Additionally, we will not use parameter’s names in the function’s prototype (declaration)
  • After we are done with the main program we will define our function
This is the complete code for this example:
#include <iostream>

using namespace std;

//function declaration
void increment_elements(int [], int, int);

int main()
{
  int array[] = {1, 2, 3};
  int second_array[] = {3, 4, 5};
  int third_array[] = {5, 6, 7, 8};
  increment_elements(array, 3, 2);
  increment_elements(second_array, 3, 2);
  increment_elements(third_array, 4, 2);
  return 0;
}

//function definition
void increment_elements(int array[], int number_of_elements, int increment)
{
  for(int i = 0; i < number_of_elements; i++)
  {
    array[i] += increment;
  }
}

Every element of the arrays will be incremented with the given value of “increment”.

Parameters by value and by reference

In C++ and also in some other languages, there is a concept called parameters by value and by reference. To really understand this concept, it is best if we looked at a practical example.
We saw that every single function (excluding void functions) return some value. What if you wanted to return multiple values? Imagine a simple problem which takes two integers as parameters and just increments both parameters. Because we are already supplying two integers, can we just increment those? Then we don’t need to return anything, right? Let’s see this in action.
#include <iostream>

using namespace std;

void increment(int a, int b)
{
  a++;
  b++;
}

int main()
{
  int x = 3;
  int y = 4;
  increment(x,y);
  cout << x << endl;
  cout << y << endl;
  
  return 0;
}

Output:

3
4
Hugh? It seems the function had no effect, but how is that possible? The truth is that the function actually had effect, but not on the variables x and y. To really understand this, we need to look at what actually happens when a function is called. The parameters a and b aren’t actually just placeholders for the variables x and y. They are real parameters. What actually happens is when a function is called, some space in the memory is left out for the parameters. When the user supplies the function with the adequate parameters, the value of those variables get copied into the parameters. These parameters are automatic variables – they are automatically declared once the function is called and are destroyed once the function returns. So our program did the following steps:
  1. Called the main function
  2. Defined variables x and y with their appropriate values
  3. Called the function “increment”
  4. Declared the variables a and b automatically and assigned them with the values from x and y
  5. Incremented the variables a and b
  6. Returned from the increment function
  7. Returned from the main function
This type of function parameters are called parameters by value, because only the value of the variables is visible in the code and not the variables themselves.
This now holds a very big problem. If we can’t supply the variables because their values are just copied into automatic variables, we have no way of changing values to variables inside of a function, or do we? If every time we call a function and the variable stays the same, its contents just gets copied. So, what is the solution? Instead of supplying the function with a value, we could supply it with the memory location of our variables? In that case, instead of our function having integer value parameters, it must have parameters which are pointers to integers!
#include <iostream>

using namespace std;

void increment(int *a, int *b)
{
  (*a)++;
  (*b)++;

}

int main()
{
  int x = 3;
  int y = 4;
  increment(&x, &y);
  cout << x << endl;
  cout << y << endl;
  return 0;
}

Output:

4
5
So even though the value supplied to the function is copied (in this case the addresses of the variables), we don’t mind because we are essentially working with the values which are stored at those addresses.
We can do even better! One thing which may be inconvenient is supplying the function with the addresses of the variables instead of the actual variables. It works, but we need to keep in our head that we should always supply the addresses. If you remember from the last chapter, there is a way to use pointer-like logic without actually using address manipulation, but the actual variable and that is with references. If we use references instead, we need to change a couple of things:
  • The parameters are now references to integers
  • We don’t need to increment the values pointed to by the arguments, but the arguments themselves
  • We don’t need to call the function with the addresses of the variables, but the variables themselves
#include <iostream>

using namespace std;

void increment(int &a, int &b)
{
  a++;
  b++;

}

int main()
{
  int x = 3;
  int y = 4;
  increment(x, y);
  cout << x << endl;
  cout << y << endl;
  return 0;
}

This type of parameters are called parameters by reference, because the variables are parsed to the function by reference – not only is the value of the variables visible in the function’s body, but also the variables themselves. Any change you make to the arguments in the function are automatically transferred to the variables supplied.

Table of contents

Leave a Reply