Part 3: Functions
As we saw earlier, functions are logical blocks of code that do a specific task. We covered all the basics so we can start organizing our code in functions. In this part we will take an in-depth look at functions and learn those features that will help us create efficient and robust code.
Functions
In programming we break our code into functions. This practice has a lot of benefits. Here we are listing some of them.
- Modularity: we can break complex problems into smaller tasks easier to handle, understand and maintain.
- Reusability: these smaller functions can be used as part of a solution for another problem. This reduces redundancy and saves us a lot of time.
- Readability: the code is now organized and easier to read and understand. We see functions with clear names describing what they do, instead of complex code.
- Maintainability: breaking code int autonomous functions makes it easier to locate possible problems and fix them.
- Testing: it is easier to test simple and small functions and make sure they do what we intended. This makes the whole process of debugging our software a lot easier.
- Abstraction: none needs to know how a certain function works. They only need to know what it does and how to use it.
- Collaboration: breaking the problem into smaller tasks means that more people can work on it without interfering with each other and develop the solution faster.
We have two main types of functions:
- Standard Library Functions, which are predefined by the language
- User-defined Functions, developed by the user
So far we have seen some Standard Library Functions like std::in or std::cout. We have also seen the user defined function main, which is required by the language, but we have to write. In this part of the tutorial, we will focus on User-defined Functions, and how we can organize them in our projects.
Function Definition
In C++ we create our own functions. Here is what a function looks like:
returnType functionName(argument_list) {
// function body
}
Functions in C++ can return a result. The type of this result is the returnType. This can be an integer, a double and so on. If it does not return anything we say it is of type void. The argument list can be a list of variables or values, depending on the function, which will be passed to it in order to get the results we need.
Here is a simple example function:
int simple_function(int arg1, double arg2){
// print the arguments received
std::cout << "arg1=" << arg1 << ", arg2=" << arg2 << "\n";
// and return some value
return 3;
}
It does not do anything fancy but we can experiment with the arguments we pass to it and the values it returns.
Calling a function
Ow that we have created a function we need to call it. All we have to do is write its name and the arguments we want to pass, and the compiler will figure everything out for us:
// call the function and get the return value
int ret = simple_function(8, 2.34);
std::cout << "function returned:" << ret << "\n";
// we may ignore the return value
simple_function(8, 2.34);
As we see in the example we can call the function and ignore the return value. Be very careful when you ignore the return value of a function, because it is there for some reason, the simplest being a way to let you know if it succeeded or failed to do what it was supposed to do.
Declaring a function
As we said in the beginning, C++ is a strong typed language. Everything has to have a concrete type that cannot change throughout the program execution. The compiler has to know all the entities and their exact definition before using them. To assist with the declarations of the program entities we us the header files. We have seen them in part 1. So now we are going to put our function declarations in headers.
Declaring a function is not so hard. All we need is the returnType, the functionName and the argument_list. This is the first line we saw in the syntax of the function definition earlier on. Here is the syntax of the function declaration:
returnType functionName(argument_list);
And our simple_function would be declared like this:
int simple_function(int arg1, double arg2);
Function overloading
Function Overloading is a feature of the language that allows us to have multiple functions with the same name. For the compiler ,and the users of our code of course, these functions must have different arguments. This means that they should have:
- Different number of arguments.
- Types of arguments, the arguments of each function must be of different type, for example one integer and one double versus two doubles.
- Different order of arguments, for example one integer and one double versus one double and one integer.
Upon compilation the compiler determines the correct function to call based on the types of the arguments we pass in the call. This is one of the most profound reasons why C++ needs a declaration of everything, either variables or functions. One thing we must mention here is that overloading is based only on the argument list and not on the return type of the function.
Here are the declarations of some possible versions of the add_numbers function:
// the declaration of a function that adds two numbers
double add_numbers(double a, double b);
// a function that adds three numbers
double add_numbers(double a, double b, double c);
// or add a double and an integer
double add_numbers(double a, int b);
// or add an integer and a double
double add_numbers(int a, double b);
This code is calling the functions, and the compiler will generate the correct call:
int main()
{
// calculate and store the sum of two numbers
// the function is declared in part4b.h and
// implemented in part4b.cpp
double result = add_numbers(2.3, 4.5);
// three doubles
result = add_numbers(2.3, 4.5, 6.7);
// or a double and an integer
result = add_numbers(2.3, 4);
// or add an integer and a double
result = add_numbers(5, 6.7);
return 0;
}
An obvious benefit of this feature of the language is that we do not need different names for functions that actually do the same thing, making I easy to remember and reuse our code.
We can skip the declaration of a function as long as its definition comes before it is used. In that case we can say that the definition acts as declaration as well, but only for functions that are written after this function. This is not a good practice though because the function is not publicly available.
Default arguments
In C++, default arguments allow us to specify a value for a function parameter that will be used if no argument is provided for that parameter when the function is called. This can make our code more flexible and easier to read.
There are some rules we need to follow when we specify default arguments for a function:
- Default values must be specified in the function declaration.
- All subsequent parameters after a default parameter must also have default values.
Here is an example to illustrate default arguments:
int multiply(int i = 1, int times = 1) {
return i * times;
}
// the first call returns the number tripled
m = multiply(2, 3);
// while the first simply returns the number
int m = multiply(2);
// finally, the third returns the absolute default
m = multiply();
As we said before this definition acts as a declaration too, so we can specify our default arguments. So, we should declare our function and define it like this:
// our function declaration with the default arguments
int multiply(int i = 1, int times = 1);
// the definition cannot declare the default arguments
// we write them in comments so we do not forget
int multiply(int i /*= 1*/, int times /*= 1*/) {
return i * times;
}
As expected the first call passes the two values to be multiplied. In the second call the times parameter is omitted, and the compiler uses the default value 1. In the third call we leave out both parameters and the compiler fills in the default values.
Using default arguments makes our code easier to read and maintain, with less chances for something to go wrong. This is another feature that helps us create more robust code faster and easier.
Functions & variables
Type is only one attribute of a variable. It is just how much space the variable takes and what values it can hold. But where is that space allocated? When is that space no longer available?
In this section we will take a closer look at the variables, their scope and lifespan and the storage they take. What the different types are and how the differ from each other.
Local variables
Local are the variables that declared inside a function. In all the examples we have seen so far the variables are local. They were declared within the body or scope of the function. Their so called ‘lifespan’ is from their declaration to the end of the function. These variables that initialized when their declaration is reached and released when they go out of scope, are also called automatic variables.
There were variables that were declared in the declaration of a for loop. In older definitions of the language their lifespan was until the end of the function. In modern specification their lifespan is until the end of the loop. Compilers enable one of the two via compilation options. I would recommend though that you go with the latest specification.
In general variables ‘live’ inside the block of code they are declared. With block of code, we mean the statements that are enclosed within curly brackets. So, if you declare a variable inside the block for an if clause then it is not accessible outside that block.
The arguments of the function are local variables as well. The only difference is that they are initialized automatically with the values we provide in the function call.
The following example shows it all:
void local_variables(int i, double d) {
int j;
for (int k = 0; k < 10; ++k) {
std::cout << k << "\n";
}
++k; // error k is out of scope
// i is a local variable so the next statement is perfectly valid,
i = 100;
if (i > 10) {
double l = 100 * d;
}
std::cout << l << "\n"; // error l is out of scope
}
Static local variables
Static local variables are variables that retain their values between function calls. Regular local variables are created and destroyed every time a function is called. Static local variables on the other hand, are initialized only once and persist for the lifetime of the program.
Static local variables are visible only the function, just like ordinary local variables. This means they do not mess with the global namespace and cannot be used to transfer data between functions. Yet they give us the persistence of a global variable.
In the example we create a simple counter to count how many times a function is called. The counter is created the first time the function is called. This means the line static int localCount = 0; is called only once. For this reason, it is essential to add the initialization in this line as well. In any subsequent call this line is skipped.>/
int static_variables() {
// count how many times the function was called
static int localCount = 0;
++localCount;
return localCount;
}
Break the program in multiple source files
Breaking your code into multiple files can offer several benefits:
Improved Readability: As your codebase grows, keeping everything in a single file can make it difficult to navigate and understand. Splitting code into smaller, focused files makes it easier to read and maintain.
Better Organization: By separating code based on functionality or concerns, you can keep related pieces of code together. For example, you might have one file for user interactions and another for data processing.
Easier Reusability: When code is modularized into separate files, you can easily reuse components across different parts of your project or even in other projects.
Simplified Collaboration: In a team setting, having code split into multiple files allows different team members to work on different parts of the project simultaneously without causing conflicts.
Enhanced Maintainability: Smaller files are easier to test, debug, and update. If a bug arises, you can quickly locate and fix it without sifting through a large monolithic file.
Faster Compilation: In languages like C++, splitting code into multiple files can speed up the compilation process since only the modified files need to be recompiled.
Here are some basic steps we take towards that goal:
Identify Modules: Determine the different modules or components of your project. For example, if we’re writing a game, we might have modules for graphics, input handling, game logic, etc.
Create Header Files (.h): For each module, we create a header file that declares the functions, macros, and data structures that will be used by other parts of the program. For example, graphics.h might declare functions like initGraphics() and drawSprite().
Create Source Files (.cpp): Implement the functions declared in the header files in corresponding source files. For example, graphics.cpp would contain the definitions of initGraphics() and drawSprite().
Use Include Guards: Ensure that each header file has include guards to prevent multiple inclusions. This is typically done using #ifndef, #define, and #endif preprocessor directives.
Static functions
In C++ all functions are globally accessible. We just declare them in the header, and they are ready to use in our project. Breaking our project into files gives us the opportunity to hide the internal details of our code. The initGraphics function for example can be broken into simpler tasks like initGPU and setGraphicsMode. These two functions can also reside in the graphics.cpp file and be completely inaccessible from anywhere in our code, but the code inside the graphics.cpp file.
This can be done by declaring them static. It is better to declare them at the top of the file and then define them anywhere within the file.
Here is graphics.h:
#ifndef __graphics_h__
#define __graphics_h__
int initGraphics();
int drawSprite();
#endif //__graphics_h__
And here is graphics.cpp:
#include <iostream>
#include "graphics.h"
static int initGPU();
static int setGraphicsMode();
int initGPU() {
return 1;
}
int setGraphicsMode() {
return 1;
}
int initGraphics() {
initGPU();
setGraphicsMode();
return 1;
}
int drawSprite() {
return 1;
}
Global Variables
Global variables in C++ are variables that are declared outside of any function, typically at the top of the source file.. They can be accessed and modified by any function or class within the same program. We can access them from other files as well if we declare them.
To declare a global variable outside the file it was defined we need the extern keyword. This declares the variable’s name and type to the compiler but prevents it from allocating space in computer memory for it. The memory will be allocated be the code generated from the definition.
The following example will make it all clear. In the add_functions.cpp file we define a global variable named myGlobalCount like this:
// a global variable that counts
// how many times our functions are called
int myGlobalCount = 0;
In the header file add_functions.h we declare the variable like this:
// declare a global variable as extern
extern int myGlobalCount;
The definition of the variable was put in the beginning of the add_functions.cpp file so it is completely accessible from any function within that file. On the other hand, declaring it in the add_functions.h makes it accessible to every function in functions.cpp file that has included the add_functions.h header.
Global variables are very convenient, they are accessible from anywhere in the program, let us easily share information between functions and they are persistent throughout the execution time of the program.
But there is a price we have got to pay for this. First of all, they clutter the global namespace increasing the risk of name conflicts. Allowing everybody to modify them can lead to errors that are very hard to find. Finally, they reduce program modularity.
So, we need to balance the pros and cons very carefully when we define global variables.
Static global variables
Global variables can also be declared with the static keyword. This limits their visibility only in the file they are declared, and they are inaccessible from other source files. They cannot be declared as extern in a header and used in other files, because that would lead to a link error, the linker would not find the appropriate declaration when assembling (linking) the code to one executable.
Function pointers (pointers to functions)
Function pointers are pointers that store the addresses of functions. They provide an alternative way to call a function, or dynamically pass the function to call as an argument to another function. We can obtain a pointer to a function by using its name. Here is an example of using a function pointer:
#include <iostream>
void function(int i) {
std::cout << "i=" << i << "\n";
}
// take the function to use as argument
void check_out(int value, void (*fun_ptr)(int)) {
// use the function we were told
fun_ptr(value);
}
int main() {
// get the function pointer
void (*fun_ptr)(int) = function;
// and use it to call it
fun_ptr(33);
for (int i = 0; i < 4; i++) {
// pass the pointer as an argument
check_out(i * 3, function);
}
}
In the beginning we have the function we will call using a pointer. To declare a variable that is a pointer to a function we use the syntax: return_type (*var_name)(arg_list). The name of the variable must be in the parenthesis with the address operator because otherwise the compiler assumes we mean a return_type* due to operator precedence. Using function pointers is a very straightforward operation and helps us write flexible an dynamic code.
Summary
- In C++ we can use functions to organize our code
- Functions can be overloaded and have default arguments
- The stack and the heap are used to store local and global variables