Functions
All of our programs so far have lived entirely inside the main()
function. In other words, all the code was in one big pile. So far
this has not been much of a problem because our programs were small.
But we did use functions that others have written. For example, we
have used cmath's pow()
and sqrt()
functions. Even cout
and
cin
are functions, though they are quite a bit more complicated and
a little bit special (actually, the stream operators <<
and >>
are
functions too, as we'll see several weeks from now).
If we did not have, say, sqrt()
, at our disposal as a function, we
would need to include our own code for sqrt()
(using an
approximation method like Newton's) in our big pile of code in
main()
. This is undesirable.
Functions allow the programmer to modularize and isolate code, so that in any single function, only one task is being performed. This generally makes code more understandable.
Additionally, writing a function allows one to potentially hide code. A function can be compiled and provided (in machine-readable form, but not human-readable form) to other programmers. This is very common; e.g., Microsoft provides many functions specific to Windows that others can use (to write their own Windows programs) but that others cannot read.
You can think of a function as simply code that has a name. The name of some chunk of code is what the code does. For example, the
sqrt()
function contains only enough code to find the square root of a number.
Where to write your own functions
For now, we will write functions in the same file as main()
so that
our programs still consist of just one file. Programs need not be just
one file, however, and in the future we will explore ways to split up
code into multiple files (by putting functions in different files).
Functions can be defined above main()
, or they can have just a
prototype (or signature) above main()
with the function
definition below main()
. (Function definitions cannot be placed
inside other functions, although prototypes can.) Here are examples:
#include <iostream>
using namespace std;
// this is an example of a function fully defined above main()
double timesTwo(double x)
{
return 2*x;
}
int main()
{
cout << timesTwo(4.5) << endl;
}
#include <iostream>
using namespace std;
// this is an example of just a prototype above main()
// the function code itself is below main()
double timesTwo(double);
int main()
{
cout << timesTwo(4.5) << endl;
}
double timesTwo(double x)
{
return 2*x;
}
The importance of where the function is defined and defined (above
main()
or not) is that the compiler requires that either the
function prototype or the whole function itself is found above the
code that uses the function. If you try to use a function called
timesTwo()
but the compiler has not seen any function by that name
yet (either as a prototype or definition), then you get a compiler
error.
A more generic look :
type name ( parameter1, parameter2, ...) { statements }
Function return types and parameter types
Every function must have a return type (e.g. int
, double
, etc.),
or the function could be a void
function, which means we write
void
instead of a return type (void
is not technically a
type). The return type goes before the function name. If the function
is not a void function, then the code inside the function must use
return
somewhere, returning a value of the proper type. A void
function does not need the return
command; it can include the
return
command (this causes the function to terminate) but cannot
use return
to return a value.
A function may have parameters. But it need not. A function that has
no parameters has empty parentheses. A function that has two int
parameters would have int x, int y
in the parentheses. The x
and
y
names are usually not written in the function prototype (though
they can be). The x
and y
should be included, however, when the
function is being defined.
The x
and y
names exist only inside the function. Assume a
function is called add
and it has two int
parameters. This is what
it looks like:
int add(int a, int b)
{
return a+b;
}
Then when the function is called (say, from some code in main()
),
two integer values must be provided as arguments: add(4, 12)
(the
result of that function call is 16
, naturally). Inside the function,
the 4
is assigned to the variable name a
and the 12
is assigned
to b
. This a
and b
exist only inside the function.
Consider this example:
#include <iostream>
using namespace std;
int add(int a, int b)
{
return a+b;
}
int main()
{
int a = 15;
int b = 20;
cout << add(a, b) << endl;
}
The a
and b
inside main()
are different variables than those
inside add()
. Even if the code inside the function add()
decided
to change the values of a
and b
, it would only change the values
for the variables known by those names inside the add()
function,
not those known by those names inside main()
. The function add()
cannot access the variables declared inside main()
.
Since the variables inside a function are not the same as those inside a different function, they need not have the same names:
#include <iostream>
using namespace std;
int add(int someSillyNameX, int someSillyNameY)
{
return (someSillyNameX + someSillyNameY);
}
int main()
{
int a = 15;
int b = 20;
cout << add(a, b) << endl;
}
Note that function names (e.g. add
) have the same restrictions as
variable names (i.e. they cannot start with a number or special
symbol, etc.).
Function calling & parameter passing
The functions demonstrated above use call-by-value, which means only the value of the arguments is provided to the function. Here is the textbook's explanation of how call-by-value works:
It is the values of the arguments that are plugged in for the formal parameters. If the arguments are variables, the values of the variables, not the variables themselves, are plugged in.
The first argument is plugged in the for the first parameter, the second argument is plugged in for the second parameter, and so forth.
Functions can also use call-by-reference. If a function uses call-by-reference, then it receives not just the value of an argument but also the memory location of the original variable. All variables keep track of their memory location so they know where their value is stored in memory. When a variable is updated or assigned, the value in the variable's memory location is changed.
When a function uses call-by-reference, it also has access to the memory locations that the original variables (in the calling function) used. So the function can change the values in those memory locations.
Here is an example of a function that uses call-by-reference. It's the "increment" function, which means it takes an integer parameter and increases that integer by one.
#include <iostream>
using namespace std;
void increment(int &x)
{
x++;
cout <<" x here is : " << x <<endl;
}
int main()
{
int a = 5;
increment(a);
cout << a << endl;
return 0;
}
You know the increment()
function uses call-by-reference for its
single parameter because that parameter has an &
in front of
it. When main()
calls increment()
, the variable a
(inside
main()
) is passed by reference to the function. The function does
not name this variable (and its memory location) "a" but instead names
it x
. Otherwise, they are the same variable. Any changes to x
inside the function cause the same changes to a
inside the calling
function (i.e. main()
) because x
and a
use the same memory
location to store their value. We say this is a case of
"call-by-reference" because it's not the value of a
that is being
provided to the function (like it would be if we left out the &
and
therefore had a call-by-value parameter) but rather the memory
location to which a
refers.
Here is the same function but using pass-by-value, what does this print ?
#include <iostream>
using namespace std;
void increment(int x)
{
x++;
cout <<" x here is : " << x <<endl;
}
int main()
{
int a = 5;
increment(a);
cout << a << endl;
return 0;
}
A function can have a mix of call-by-reference (or
"pass-by-reference") and call-by-value parameters. For example, this
function updates two variables w0
and w1
(which are passed by
reference, so that they can be updated) depending on the value of
another variable y
(which is just passed by value):
void updateXYZ(double &w0, double &w1, double y)
{
if(y < 0.0)
{
w0 = w1 = 0;
}
else
{
w0 = pow(w0+w1, 2.0);
w1 = pow(w0-w1, 2.0);
}
}
Pointers as parameters to functions
The C language (which came before C++) didn't have "call-by-reference," so in order to make a function that could change the values of its arguments, pointers were used:
void changeValues(int *px, int *py)
{
// add one to each variable pointed to by the parameters
*px = *px + 1;
*py = *py + 1;
}
C++ can do the same thing (although how the function is used must change):
void changeValues2(int &x, int &y)
{
// add one to each variable
x = x + 1;
y = y + 1;
}
These are equivalent except in how they are used. Here is how the C version (which uses pointers) is used:
int x = 5, y = 8;
changeValues(&x, &y);
Note that you have to provide the "memory location" of the variables
x
and y
to use the function that has pointer parameters.
The C++ version (call-by-reference) can be used in a more straight-forward manner:
int x = 5, y = 8;
changeValues2(x, y);
This is why call-by-reference is useful; it makes the code a little simpler, but has the same effect.
Here is another popular example, swapping of 2 numbers in C++
void swapnum(int &i, int &j) {
int temp = i;
i = j;
j = temp;
}
int main(void) {
int a = 10;
int b = 20;
swapnum(a, b);
cout << " a is " << a << " while b is " << b <<endl;
return 0;
}
and here is the corresponding C version of the swapnum code:
void swapnum(int *i, int *j) {
int temp = i;
i = j;
j = temp;
}
int main(void) {
int a = 10;
int b = 20;
swapnum(&a, &b);
printf("A is %d and B is %d\n", a, b);
return 0;
}
Functions inside classes ("methods")
We can put a function inside a class; in this case, the function can only be used on an existing object of that class.
Recall the Person
class:
class Person
{
public:
string name;
int age; // in years
int height; // in cm
int weight; // in kg
};
Let's add a function that prints the person's weight:
class Person
{
public:
string name;
int age; // in years
double height; // in cm
double weight; // in kg
void printWeight()
{
cout << name << " weighs " << weight << " kg." << endl;
}
};
Suppose we have an object:
Person vignesh;
vignesh.name = "Vignesh S.";
vignesh.age = 25;
vignesh.height = 177;
vignesh.weight = 68;
We can use the printWeight()
function in the following way, using
the object as the prefix (just like when we set values like weight
):
vignesh.printWeight();
Or, we can add a function that returns the weight in pounds:
class Person
{
public:
// ...
double getWeightPounds()
{
return (2.204 * weight);
}
};
And we can use it like so:
double pounds = vignesh.getWeightPounds();
A final word
It is surely time to recover the original sense of "argument" (via Latin arguere, to put in a clear light) as "clarification, proof." The depressing confusion over name/value calling, between real/formal arguments and/or parameters, and how/when/where they are initialized and/or assigned must be resolved here and now. Remember: if you pass by name, the function can corrupt your actual argument, but if you pass by value, the function can only corrupt a copy of your argument. Some sophisticated languages let you pass explicit pointers, pointers-to-pointers, references, references-to-pointers, pointers-to-references, and so on to any depth (whence the phrase "beyond fathomage"), allowing the function to corrupt not only your arguments and their copies, but also those of your erstwhile friends running in distant parts of the system. It's your call, as they say. -- The computer contradictionary