Functions
This book has used functions (called methods when part of a C++ class) already but we have not explored them in any detail.
A function is a named group of statements that perform a task. A function in the C programming language will normally return a value which can be any valid type. Alternately a function can be declared as a void function when no value is returned.
Values, known as arguments (sometimes called parameters), can be passed into a function to be used by the code statements within the function. Arguments are optional but when present they take the form of variable declarations (type and name) with subsequent arguments separated by commas.
Functions are a great way of breaking code down into manageable blocks. They can be particularly useful when the same (or very similar) code is needed in more than one place in a program. Also, if expressive function names are used they can help make code more readable and thus easier to maintain.
The Arduino C++ compiler (like all such C++ compilers) requires functions to be declared as well as defined (this simplifies the task of the compiler and linker) but as that is normally managed for us by the Arduino IDE we can just concentrate on defining the function and its code.
The general format for a function is:
return_type function_name(argument list) {
// some processing
return return_value; // not required for a void function
}
The function statements are all contained within a pair of curly braces {}.
Earlier, a function to calculate the factorial value of a number was used to demonstrate the use of a while loop.
unsigned long calcFactorial(int x) { unsigned long factorial = 1; while (x > 0) { factorial *= x; --x; } return factorial; }
There the function return type was an unsigned long and the single argument was an int type that had been given the variable name x within the function. The factorial of the variable x is calculated and then the final value returned to the code line that “called” the function. This is managed by the return statement. The function might have been called by an expression like:
unsigned long myVal = calcFactorial(4);
which would assign the value 24 to the variable myVal. In this example, I passed a constant (4) to the function but any value stored in an int variable could have been passed or indeed the result of any expression that could be evaluated to an int type.
To demonstrate the use of multiple arguments we could reproduce the built in max() function (strictly, the built in version is a macro but we have not looked at them yet).
void setup() { Serial.begin(115200); byte a = 244; int b = 12; Serial.println(ourMax(a, b)); } long ourMax(long x, long y) { if(x > y) { return x; } return y; }
Which demonstrates the use of multiple return statements and the fact that the compiler is happy for us to pass byte and int types to a function that specifies a long type for the two arguments.
As an exercise, run the above code on your Arduino and then afterwards change the argument types for the ourMax function from long to byte. Then change the int value b in the setup function to 3276. Upload the program again to review the result. You should see the number output to the serial monitor is 244 as the int value has overflowed the byte argument type. Some languages would have balked at a code line that passed an int to a function expecting a byte but C assumes that you know what you are doing and lets you get on with it.
Function arguments are passed by value (mostly)
When a value type (char, byte, int, long, etc.) is passed as an argument to a function then a copy is made of the value and the copy passed to the function. This allows the function to make internal changes to the passed value without affecting the original variable in the calling function. This is known as passing arguments by value.
Variables passed by value as arguments and any variables declared within a function have local scope. Even better than that, unless they are declared with the static modifier, they are created in part of the SRAM memory called “the stack” and destroyed when the function terminates. There is no lingering memory overhead from variables with local scope.
Arrays are not passed to a function “by value” but using a mechanism called “by reference”. This avoids the memory (and process) overhead of making a copy of an array but does require a different approach. When you include an array in the argument list then what is actually passed is a pointer to the array. There is much more to come shortly on pointers but for the moment we can work with the idea that what is passed to the function is the memory address of the first element of an array.
The output from the following might initially surprise you.
void setup() { Serial.begin(115200); int test[] = {23, 76, 81, 1, 9, 677, 9876, 11}; Serial.print("Array test element count: "); Serial.println(sizeof(test) / sizeof(test[0])); Serial.println(getMax(test)); } int getMax(int mInts[]){ Serial.print("Size of argument mInts: "); Serial.println(sizeof(mInts)); }
When uploaded and run the Serial Monitor output would look like
Array test element count: 8
Size of argument mInts: 2
Where the sizeof(mInts) is 2 because that is the size of the value holding the address of the first element of the array, 2 bytes (on an Arduino Uno). The expression sizeof(test) / sizeof(test[0]) calculated the number of elements in the test array by dividing the total size of the array by the size of one of the elements.
It is usually best practice to also pass the total number of array elements to a function that is going to be working with an array passed as an argument. The code below shows that being added to the getMax() function. You can see that the array handling code is written within the function just as if it were a local variable.
void setup() { Serial.begin(115200); int test[] = {23, 76, 81, 1, 9, 677, 9876, 11}; Serial.print("Maximum value: "); Serial.println(getMax(test, sizeof(test) / sizeof(test[0]))); } int getMax(int mInts[], int arrayLen){ int maxVal = -32768; // lowest 16 bit int value for(int i = 0; i < arrayLen; i++) { if (mInts[i] > maxVal){ maxVal = mInts[i]; } } return maxVal; }
This happily outputs the largest int value in the array to the Serial Monitor (9876 as you would expect).
There is more to say on passing arrays as arguments later in the section on pointers.
Returning to the example, the array test[] is not protected by default from change within the maxVal() function as an argument passed by value would be. If the function had included a line like the following:
mInts[1] = maxVal;
then the second element in the array test[] would have had the value changed to 9876.
It is clearly the programmer’s responsibility to protect variables passed by reference from inadvertent change. If the code had used the const keyword when defining the function argument mInts[] then the compiler would object to a statement directly setting one of the array elements to a new value.
int getMax(const int mInts[], int arrayLen)
However, it might be that you want to change a value that is external to the function. In that case the variable can be deliberately passed as an argument by reference instead of by value. This is one way to write a function that can effectively return more than one result
void setup() { Serial.begin(115200); float test = 32.8765; long res2 = 0; long res1 = myFunct(test, &res2); Serial.print("Result: "); Serial.println(res1); Serial.print("Other result: "); Serial.println(res2); } long myFunct(float num, long* b) { long a = floor(num); // floor returns the int part of num *b = a * a; // sets the value of res2 back in the setup() function // to the square of a return a; }
The second argument to myFunct() indicates that what is going to be passed is a pointer to the variable and not a copy of that variable. This allows the function to change the value of that variable. We are jumping ahead a bit here using a pointer but hopefully you will remember that variables passed by value are isolated within a function but that it is possible to set things up so that a function can change variables declared outside of that function even if they are not global variables (those with global scope).
Function overloading
It is sometimes very convenient to have two or more versions of a function where each version has different arguments types or a different number of arguments. This improves code readability and consistency with the compiler left to figure out which version you are using in any given situation. Any family of overloaded functions you create all share the same meaningful name while being able to support variations to the arguments.
When your code uses Serial.print() you just pass an int or float with the reasonable expectation that there is a version of Serial.print() ready to output a representation of the argument value to the Serial Monitor. Of course, if you then decide that you would like more than two decimal places to be displayed for a float value you might call yet another overloaded version of print().
The rule for overloading is that functions with the same name must have a different number of arguments and/or different argument types. Just returning different value types is not sufficient and will throw a compiler error.
int getSquare(int var) {
return var * var;
}
float getSquare(float var) {
return var * var;
}
are fine but if you were to add a third option:
int getSquare(float var) {
return (int)(var * var);
}
then the compiler will complain with the message: “new declaration of ‘int getSquare(float)’”. The only external difference between the original function accepting a float type as an argument and the new one was the return data type and that is not enough because the compiler would be unable to decide unequivocally which version you wanted to use. Even assigning the return value to the specified type would be ambiguous in C.
It will not have escaped your notice that a function with a parameter type of float will happily accept an int variable value at run time. This means that both the int and float versions of our getSquare() functions are candidates for use in a line of code like:
int x = 5;
x = getSquare(x);
What the compiler does is decide upon the best candidate in any set of matching functions. If there is no clear best match then the compiler may generate an error. Deciding upon a hierarchy of matches can be complex as the compiler may have to assess the impact of implicit casts when finding a good match. If you write a short program and try passing a long value to getSquare() then the Arduino compiler will object as it decides the call is ambiguous. If you then try a byte variable then there will be no ambiguity as this will be automatically cast to an int.
Overloaded functions can call each other. This is one way to implement default argument values. One way to implement something like the Serial.print(float) function could be:
void ourPrint(float f) { ourPrint(f, 2); } void ourPrint(float f, int decPlaces) { // clever code to print a floating point number // search for Print.cpp file to see how }
The sample code above has one function that accepts a float argument and another that accepts an additional argument for the number of decimal places to be printed. The first overloaded function calls the second with the number of decimal places set to a default.
In fact, the Arduino Serial.print() functions uses a better way.
Default Argument Values
To set default values for arguments we have to do something that, ordinarily, the Arduino IDE does for us. We have to declare the function with its default value before we define it (when we actually write the code lines of the function).
If we write a function in the Arduino IDE like:
float getFraction(int arg1, int arg2) {
return (float)arg1 / (float)(arg2);
}
before compiling the code, the IDE creates an additional line of code to declare the function. In this example, it would look like:
float getFraction(int, int);
Ordinarily we would not see this line as it is only created by the IDE in a version of our code that is passed to the C++ compiler used by the IDE.
The declaration (better called a function prototype) would be used by the C++ compiler and (possibly) the linker. If you look at some non-Arduino C++ code on-line then you will normally see function prototypes like this added before the code for any given function appears in a program.
We can rely upon the IDE to create the initial required declaration but we can also add an extra version. This gives us the opportunity to set one or more default value.
getFraction() is probably not a great example but it has the benefit of being a simple one.
Suppose that we wanted to implement a default behavior where it would return a value that was half the first int argument if the divisor (arg2) was omitted. This would need arg2 to default to the value 2. So, at the head of our code (and outside any function) we could add a line:
float getFraction(int arg1, int arg2 = 2);
Then if we were to only supply one argument to getFraction(), the default second value of 2 would be used and the return value would be half the value set for arg1. If we do supply a second argument then that will always be used by the function.
A function might have multiple default values. Indeed, all of the arguments can have default values. The only rule is that default argument values should always follow after any declared arguments that do not have a default.
int newFunction(int a, int b, int c = 2, int d = 7);
is valid but
int newFunction(int a, int b = 21, int c, int d = 7);
is not and will be rejected by the compiler because the argument c has no default value or perhaps because b does have one.
Well the requirement that arguments with default should follow all other arguments holds good in most instances as you are pretty unlikely to have multiple alternate sets of defaults as this could quickly lead to ambiguity. However, the compiler will allow something like:
int mult(int a, int b = 7); int mult(int a = 3, int b); void setup() { Serial.begin(115200); Serial.print(mult()); } int mult(int a, int b) { return a * b; // returns 21 if no arguments presented }
Where the first default for b is simply substituted into the second line by the compiler alongside the newly defined default value for a. I can’t think of any advantage in using this as a technique and there is a clear disadvantage when it comes code readability so perhaps best taken as a compiler anecdote.
Functions that accept a list of arguments
It is not always possible to define in advance just how many arguments a function might need to be passed at run time. You could create a prototype for an overloaded function that accepted some notional maximum but there is a better way – using va_list and the associated macros.
To include library support for va_list you should add a line to the top of the program.
#include <stdarg.h>
For a function to support a variable list of arguments at least the first argument needs to be named while subsequent arguments can be represented as an ellipsis (‘…’).
int getSum(int x, …) {
}
The function itself should declare an instance of va_list, giving it a name. The list is then initialised using the va_start() function that is passed the va_list name and the name of the first function argument. After the arguments have been accessed within the function, va_end() should be used to clean up.
int getSum(int x, ...) { va_list args; va_start(args, x); /* Function Statements */ va_end(args); }
The rest of the arguments can then be accessed using the va_arg() macro. However, it is not all beer and skittles as you have to know the variable type for each argument and the total number of arguments. This is clearly not a difficult problem if they are all intended to be of the same type or if there is a pre-defined pattern.
You will undoubtedly run into (non-Arduino) C code on the Internet that makes use of the printf() function. This function accepts a variable number of arguments but is able to use the content of the first argument (the format string) to determine the total number and the types of the following data items.
Our simple example however might use the first argument to set the number of int arguments in total. Then it might read something like:
void setup() { Serial.begin(115200); Serial.print("Series total is "); Serial.println(getSum(9, 34, 76, 54, 34, 12, 22, 90, 101, 33)); } int getSum(int x, ...) { int sum = 0 ; va_list args; va_start(args, x); for(int i = 0 ; i < x ; i++) { sum += va_arg(args, int); } va_end(args); return sum; }
If the need arises, the argument list can be revisited by using the va_start() again.
The example function returns unexpected values if it attempts to access an argument not in the list or if (say) floating point numbers or long integers were supplied when int values were expected.
You are probably left thinking that while having the capacity to write a function that accepts a variable number of arguments is a great language feature, it is not one that you are going to use that often for Arduino development tasks.
Arguments as Constants
Many programmers use the const variable modifier for arguments to a function when they know that a given argument acts as a constant within that function. The view is that this ensures that such a value can’t be accidentally changed following some future code modification. If the argument is being passed by value (which is normal) then there would be no external consequence to such a change. There is a better case for using the const variable modifier for arguments passed by reference when there is no intention to change the external value in the calling code.
You will see this pattern in sample code and it is up to you as a programmer to adopt this approach if you so choose.
Recursion
The C programming language support recursion by allowing a function to call itself. A programmer might implement recursion as a way to break a particular problem down into smaller instances of the same problem; solving each in turn to arrive at a solution for the whole. Later in this book, in the chapter on sorts and then later the chapter on data structures you will meet recursive functions that simplify the code.
Recursion is a powerful programming technique but carries a memory overhead that might limit its use. Each recursive call makes an additional demand upon the “stack” to store new function argument values and to keep track of the “current” program pointer within each function making those recursive calls so that, eventually, program control can return to the start point.
Code downloads for this chapter are available here.