Modern C++ - Functions
Functions, parameter passing
Functions
Functions are fundamental building blocks in procedural programming, they allow us to break large, complex code into smaller more maintainable parts, hiding implementation details from the view of a higher abstraction, reuse earlier implemented actions and apply variability by using parameters.
The C++ language has been designed to make functions easy tom implement and use, and effective to call.
A set of functions (and perhaps other global objects) can be compiled in separate translation unit and can be used as a library.
Wording: the name of a parameter in a function declaration is called parameter, while the actual value passed in the call is argument. The full list of parameters is called prototype.
Function definition and declaration
The return type and the parameter list is part of the type of the function.
int f();
int *fip();
int ( *pfi )();
declare a function with no parameters returning int, a function with no parameters returning pointer to int, and a pointer to function with no parameters returning int. Functions with no intended return value should be declared with void return type. Array return type is not allowed.
For declarations, we can use formal parameter names, but they are only for descriptive purposes:
1 int f(int *x, int *y); // x and y has no role in declaration
2 int f(int *, int *); // equivalent to the above
For historical reasons, functions with no parameters can be write in two forms:
1 int f(); // function with no parameter
2 int g(void); // function with no parameter, C compatible
When we want to express that we do not know the exact parameters of a function, like in the case of printf we use the ellipsis notation:
1 int printf(const char *format, ...);
2 int fprintf(FILE *stream, const char *format, ...);
The meaning of the ellipsis is that zero or more additional parameters of unknown type. To write such variadic parameter function is tricky (and usually done by the va_ macros from <stdarg.h> header.
When we know nothing about the parameters, we can also use ellipsis:
1 int f(); // function with no parameter
2 int g(...); // function with no parameter, C compatible
Function call
When a function f is called, then the declaration prototype should match to the call:
- The number of the parameters should be the same.
- Each argument of the call shall have a type such that its value may be assigned to an object of the type of the corresponding parameter of the prototype.
If the number of parameters does not match with the number of arguments of the call, the behavior is undefined. If the function definition prototype have ellipsis or the argument in the call are incompatible with the correspondent parameter, the behavior is undefined.
If the called function has a prototype then the arguments are implicitly converted, as if by assignment, to the corresponding parameters. Ellipsis stops the conversions, only the default argument promotions are executed.
1 double fahr2cels(double);
2 //...
3 printf("%f\n", fahr2cels(36)); // ok, 36 is converted to double
The function call is a sequence point, i.e. first the argument expressions are evaluated in undefined order, only then the function body is starting to execute. (The comma between parameters are different from the comma operator.)
( *t[f1()] ) ( f2(), f3(), f4() );
The functions f1, f2, f3, f4 can be called in any order.
Recursive functions are allowed both directly or indirectly:
1 int factorial(int n)
2 {
3 if ( 1 == n )
4 return 1;
5 else
6 return n * factorial(n-1);
7 }
When this function is called with an argument smaller then one, infinite recursion happens and the program behavior is undefined.
Parameter passing by value
Arguments by default are passed by value, i.e. they are copied from the argument expression to the parameter as it would be a local automatic variable declared in the beginning of the function.
1 #include <iostream>
2 void increment(int i)
3 {
4 ++i;
5 std::cout << "i in increment() " << i << std::endl;
6 }
7 int main()
8 {
9 int i = 0;
10 increment(i);
11 increment(i);
12 std:.cout << "i in main() = " << i << std::endl;
13 return 0;
14 }
$ g++ -ansi -pedantic -Wall -W par.cpp
$ ./a.out
i in increment() = 1
i in increment() = 1
i in main() = 0
We can simulate the parameter passing by address with pointers:
1 #include <iostream>
2 void increment(int *ip)
3 {
4 ++*ip;
5 std::cout << "i in increment() = " << *ip << std::endl;
6 }
7 int main()
8 {
9 int i = 0;
10 increment(&i);
11 increment(&i);
12 std::cout << "i in main() = " << i << std::endl;
13 return 0;
14 }
$ g++ -ansi -pedantic -Wall -W par.cpp
$ ./a.out
i in increment() = 1
i in increment() = 2
i in main() = 2
Reference parameters
Parameters can be passed by reference. In this case the parameter behavior is like the function would define a local refernece variable which is initialized to the actual argument passed to the function.
1 #include <iostream>
2 void increment(int &ir)
3 {
4 ++ir;
5 std::cout << "i in increment() = " << ir << std::endl;
6 }
7 int main()
8 {
9 int i = 0;
10 increment(i);
11 increment(i);
12 std::cout << "i in main() = " << i << std::endl;
13 return 0;
14 }
$ g++ -ansi -pedantic -Wall -W par.cpp
$ ./a.out
i in increment() = 1
i in increment() = 2
i in main() = 2
Reference parameters can be changed inside the function and since the actual memory referred by the parameter is out of the function, the changes will be permanent after returning from the function.
Array parameters
Array parameters are passed as pointers to the first array element.
1 #include <stdio.h>
2 int f( int *t, int i)
3 {
4 return t[i];
5 }
6 int main()
7 {
8 int arr[] = {1, 2, 3, 4};
9 printf("%d\n", f(arr,1));
10 printf("%d\n", f(&arr[0],1));
11 return 0;
12 }
$ gcc -ansi -std=c99 -Wall -W a.c
$ ./a.out
2
2
Therefore these parameter declarations are equivalent:
1 int f( int *t, int i) { return t[i]; }
2 int f( int t[], int i) { return t[i]; }
3 int f( int t[4], int i) { return t[i]; }
4
5 // array limits are not checked: this is not reported as error
6 int f( int t[4], int i) { return t[6]; }
Pointers to function
Type of a pointer to function includes the return type and the parameter list.
1 #include <iostream>
2 void increment(int &ir)
3 {
4 ++ip;
5 std::cout << "i in increment() " << ir << std::endl;
6 }
7 int main()
8 {
9 void (*fp)(int &); // pointer to void (int*)
10 int i = 0;
11 fp = increment;
12 (*fp)(i); // call increment()
13 fp (i); // simplified notation
14 std::cout << "i in main() " << i << std::endl;
15 return 0;
16 }
$ g++ -ansi -pedantic -Wall -W par.cpp
$ ./a.out
i in increment() = 1
i in increment() = 2
i in increment() = 3
i in main() = 3
A pointer to function can be:
- point to a function by a compatible type.
- assigned to a similar pointer variable.
- checked against NULL pointer.
- indirected in a function call.
Return type
Return values are converted to the return type of the function:
1 double f(void)
2 {
3 int i;
4 // ...
5 return i; // converted to double
6 }