Programming Workshop 2 (CSCI 1061U)
Faculty of Science, Ontario Tech University
Functions are building blocks of programs. We can characterize functions as input-process-output units. Each functions takes an input, processes it, and returns a value. Functions are sometimes also referred to as procedures, subprograms, subroutines, and methods. In C++, we will refer to functions simply as “functions.” The inputs to the function are often called function arguments or function parameters. A function can return at most 1 value.
Consider the following C++ function that adds three numbers.
int sum(int a, int b, int c)
{
int sum = a + b + c;
return sum;
}
We can understand the above code as follows:
sum
is the name of the functionint a, int b, int c
refers to the inputs to this
function. Here the function takes three inputs. We also specify the
types of these inputs. Here the inputs are of type
int
.int
before the function defines the return type. This
shows that this function returns a value of type int
.It is also possible for a function to not return a value. Consider the following function that prints the value of the input passed to it.
void print_value(double a)
{
std::cout << a << std::endl;
}
Notice that the return type is set to void
. This return
type indicates that the function doesn’t return a value.
It is also possible to write a function that doesn’t take an input. Consider the following function
double value_of_pi()
{
return 3.14159;
}
Function value_of_pi
takes no input. It simply returns a
value of type double
.
Sometimes a function takes no input and returns no value. Consider the following example
void say_hello()
{
std::cout << "Hello" << std::endl;
return;
}
Similar to function print_value
shown above, this
function doesn’t return a value. We can still use the
return
keyword as shown above.
The real power of C++ comes from the fact that it comes with
libraries that are full of pre-defined functions that we can use. In
order to use these pre-defined functions, we have to use the appropriate
#include
statement. Consider the following program
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "Hello world" << std::endl;
return 0;
}
The above program will give an error if we do not include the
statement #include <iostream>
. This is because we are
using the std::cout
(operator) that is defined in
iostream
header file (more on this later).
Lets consider another example
#include <iostream>
#include <math>
int main(int argc, char** argv)
{
std::cout << "Sq root of 25 is " << sqrt(25.0) << endl;
return 0;
}
Notice that we included the statement
#include <math>
. This is because we are using the
pre-defined function sqrt
, which is defined in the
math
header file.
Three pieces to using a funtion are:
The folowing examples illustrates these pieces.
In order to use a function, we first need to define it, i.e., we need to specify its inputs, what it does, its return value, and then we call it. Lets consider the following code
#include <iostream>
// funtion definition
int sum(int a, int b, int c)
{
int s = a + b + c;
return s;
}
int main(int argc, char** argv)
{
int a = 1, b = 20, c = 34;
// function call
int t = sum(a, b, c);
std::cout << "Sum of a, b, and c is " << t << std::endl;
return 0;
}
In the code above, ln. 4–8 define the function. These include function signature (ln. 4) and function body (ln. 5–8). Later in the code the function is called on ln. 15. Note that the function is only executed if it is called.
It is perfectly legal to call a function multiple times. It is this feature of a function that enables re-use.
The following code will give a compile-time error.
#include <iostream>
int main(int argc, char** argv)
{
int a = 1, b = 20, c = 34;
// function call
int t = sum(a, b, c);
std::cout << "Sum of a, b, and c is " << t << std::endl;
return 0;
}
// funtion definition
int sum(int a, int b, int c)
{
int s = a + b + c;
return s;
}
The reason being that the compiler hasn’t seen the function when it
encounters the function call at ln. 8. When compiler encounters ln. 8,
it has no idea what sum
refers too. We can avoid this by
declaring the function beforehand. This way compiler would know that
sum
is a function, which will be defined later. We can
declare the function by adding the following statement before the
main
funciton.
int sum(int, int, int);
Or to make things clearer
int sum(int a, int a, int a);
It turns out that among other things, include files, contain function declarations. So now we know why we need to use the include directive.
In C++, all functions are equal. This means that we cannot define a
function inside another function. Keyword return
is used to
return a value back to the function that is calling the function. In the
example above, function main
calls function
sum
. So when return
is encountered in
sum
, the value (and the program control) is returned to its
caller function (which is function main
).
main
functionIn C++, main
is a special function. Each program must
have one and only one main
function. The program is exited
(i.e., shutdown) when main
function calls
return
. The operating system calls the main
function. The value returned by the main
function is passed
on to the operating system.
Variables declared inside the body of the function are available only within that function. This allows us to use the same variables names in different functions. The function arguments are also available in the function body as variables. This is way we need to explicitly name function inputs when defining functions; where as, we don’t need to explicitly name function inputs when declaring functions (we still need to provide the types for function inputs). In C++, function arguments are passed by value. This has an important consequence. Any changes made to function arguments inside a function are not visible to the caller function.
Consider the following code
#include <iostream>
int add(int a, int b)
{
int result = a + b;
return result;
}
int sub(int a, int b)
{
int result = a - b;
return result;
}
void multipy_by_2(int a)
{
= a*2;
a }
int main()
{
int a = 1, b = 3;
int a_plus_b = add(a, b);
int a_minus_b = sub(a, b);
(a);
multiply_by_2
return 0;
}
Notice that we are using variable result
in functions
add
and sub
. There is no danger though, each
function defines its own result
variable that is only
visible within this function. Also, notice that we changed the value of
argument a
in function multiply_by_2
.
This, however, doesn’t effect the value of a
in function
main
. Later, we will look at techniques to break this
restriction. For now, can you think of reasons why you would want the
changes made to an argument be visible to the caller function?
Global constants or global variables are declared “outside” the function body. The global variable and constants are available to every function within the file.
We use const
keyword to declare a global constant as
follows:
const double PI = 3.14159;
Global variables can be defined as follows
int total;
In general it is not a good idea to use global variables.
Variables declared inside a block (i.e., between a pair of
{' '}
) have block-scope. These variables are only visible
inside that block. Consider the following two cases.
#include <iostream>
int add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int a = 1, b = 3;
std::cout << "Outside: " << sum(a, b) << std::endl;
{
int a = 3;
std::cout << "Inside: " << sum(a, b) << std::endl;
}
return 0;
}
When you execute this program, you will get teh following output
Outside: 4
Inside: 6
This is because the variable a
defined inside the block
has no relationship to the a
defined outside the block. We
often use this feature in for loops.
for (int i=0; i<10; ++i)
{
:cout << i << std::endl;
std}
Here variable i
is only defined within the for loop
block.
Blocks can be nested as well.
There are two methods of passing arguments. The above examples all use call-by-value. Value of the argument is copied and then passed into the function. We have already observed the consequences. If an argument value is changed within the function, this change is not reflected outside of the function (i.e., in the caller function).
Another way to call a function is to use call-by-reference. Here, the address of the argument is passed into the function, What this means that when a value is changed within the function, the change is reflected outside the function.
The following example uses call-by-value
void inc(int a)
{
+= 1;
a }
The following example, on the other hand, uses call-by-references
void inc(int& a)
{
+= 1;
a }
We use the ampersand &
to indicate
call-by-reference.
You should use caution when using call-by-reference, since the changes made in a function are visible outside of it. Still when used properly, this feature allows for great flexibility in program design.
It is also more efficient to pass arguments using call-by-reference.
Recall that data is copied when passing arguments using call-by-value.
Call-by-reference provides an elegant solution to unnecessary data copy.
It is possible to indicate that the passed value should not be modified
within the function using a const
references as
follows.
int add(const int& a, const int& b)
{
return a + b;
}
In the above code, we can use a
and b
as
r-values, but not as l-values. In other words, we are not allowed to
assign values to a
or b
.
In C++ it is possible to have two functions with the same name, but different arguments. This is called function overloading. It is important to have different arguments, othwerise compiler will have no way of disambiguating the two functions when a function is called. Note that return type cannot be used for this disambiguation. Why is that?
Consider the following code
int sum(int a, int b)
{
return a+b;
}
int sum(int a, int b, int c)
{
return a+b+c;
}
This is perfectly valid. The compiler can easily distinguish between the two functions based upon the number of arguments at call time as follows
// Calling first function
int result = sum(1,32);
// Calling second function
int result = sum(1,3,44);
The reason we can’t use the return type for function disambiguation is because we are allowed to call a function without mentioning its return type as follows
(1,2) sum
Looks for exact match. Failing that looks for compatible match where automatic type conversion is possible.
Type can be promoted (int
to double
) or
demoted (double
to int
). In the first case, no
data is lost. While in the second case there is data loss.
Consider the following code
int volume(int length, int width=1, int height=10)
{
return length*width*height;
}
We can call this function as follows
(3); volume
Note that we didn’t need to specify the values for width
and height
. In C++ each arguments are position based. So
the following is not allowed.
int volume(int length=2, int width, int height=10)
{
return length*width*height;
}
Why? Because if we call it as follows
(3); volume
The compiler wouldn’t know if 3
is for
length
or for width
.
With practice you will figure out how best to use function overloading alongwith default arguments.
Yes! Functions increase readability, encourage re-use, and allow you to write large programs. You simply can’t avoid writing functions. As you gain more experience, breaking down a large program into functions will be become second nature. Functions also provide procedural abstraction. The caller only needs to know what a function does, not how it does it. This is a powerful feature of functions. This allows you to swap one function for another as long as function signatures match (i.e., same name, same return type, same arguments). The caller doesn’t know, nor cares, about function body.