Today introduces a new paradigm in how you call functions. Up until this point, everything in the class has been pass-by-value. The value of an argument to a function is copied into the function's parameter. Today introduces a way to not copy, but instead directly change the value in the calling function's variable.
Recall that arguments to our functions have been passed by value, meaning that inside the function we get a copy of the argument object given in the function call. Sometimes, however, we'd like to get the actual object from the function call rather than a copy. There are 3 basic reasons for this:
Let's take a simple example to see how passing by reference works. Suppose we
have a variable that stands for the hour (with a 24 hour clock), and we go
through a simulation that keeps incrementing the hour. A simple
++
won't do, because once we hit 25, the hour goes back to 1. So,
we might want a function incHour
that increments the hour
correctly ... which means that the hour variable with which the function gets
called should be modified. We might be tempted to say something like this:
void incHour(int);
...
void incHour(int h) {
h++;
if (h == 25)
h = 1;
}
However, since pass-by-value gives us a copy, nothing really gets
changed!
(See diagram on right.)
We need to pass the hour by reference, which
means we get the actual object from the function call, not a copy.
This is indicated in C++ by putting an &
after the
type of the argument.
void incHour(int&);
...
void incHour(int& h) {
h++;
if (h == 25)
h = 1;
}
The visualization here is that the parameter h
is like a wormhole to the argument incHour
was called
with. (See diagram on right.)
Now this does what we want.
For example, if this function definition is used in the following code fragment:
int hour = 18;
for(int i = 0; i < 8; i++) {
cout << "Hour " << i + 1
<< " of my 8 hour day starts at "
<< hour << ":00" << endl;
incHour(hour);
}
Important Note: This example is good for showing how pass-by-reference works, but a lousy example for showing when you should be using it! In this case, you should write incHour() as a function that takes its parameter with pass-by-value and returns the new time, rather than modifying its argument.
Hour 1 of my 8 hour day starts at 18:00 Hour 2 of my 8 hour day starts at 19:00 Hour 3 of my 8 hour day starts at 20:00 Hour 4 of my 8 hour day starts at 21:00 Hour 5 of my 8 hour day starts at 22:00 Hour 6 of my 8 hour day starts at 23:00 Hour 7 of my 8 hour day starts at 24:00 Hour 8 of my 8 hour day starts at 1:00
No constant values for passing by reference.
Note that incHour(6)
will not compile! Why? Well when you pass
something like this by reference, that says that it might get modified, and the
constant value 6 is not something that can be modified! You need to pass
variables, not constants, to functions that take arguments by reference
rather than by value.
Terminology: lvalue vs. rvalue
If a function takes a parameter by reference, the technical term for what kind
of object can be an argument is lvalue, which literally means
"anything that can appear on the left-hand side of an assignment".
Another term involved in these
kinds of discussions is rvalue, which means anything that can appear
on the right-hand side of an assignment — which is a much bigger class
of objects. We require lvalues for the left-hand sides of assignments
(obviously), for arguments to functions in which the parameter is passed by
reference, and also as operands for ++ and -- expressions (and +=, -=. *=,
...).
swap
!With multi-parameter functions, we really start to see some
interesting reasons to use pass-by-reference. For example, one of
the most common operations in computing is the swap
.
For example, suppose we read two int's in from the user and we want to print out all the integer values from the smaller of the two up to the larger (comma-separate). If the user is kind enough to enter the numbers so that the frst is the smaller, we would write:
int a, b;
cin >> a >> b;
for( int i = a; i < b; i++ )
cout << i << ',';
cout << b << endl;
But what if we want to allow the user to enter the two numbers
in either order, and have the program work regardless? If we had
a function swap
that took two int
s and
swapped their values, we could do the following:
int a, b;
cin >> a >> b;
if( b < a )
swap(a,b); // the famous swap!
for( int i = a; i < b; i++ )
cout << i << ',';
cout << b << endl;
... which is a whole lot more convenient than, for example,
writing separate for-loops for the a<b case and the b<a case.
The function swap
will need to change the values of
the variables it's passed, so they must be passed by reference.
void swap(int&, int&);
...
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void polar2rect(double,double,double&,double&);
...
void polar2rect(double r, double theta, double &x, double &y) {
x = r*cos(theta);
y = r*sin(theta);
}
With this definition, if I had a vector represented by
double
s radius
and angle
, I
could convert it to rectangular coordinates stored in variables
x
and y
by writing:
polar2rect(radius,angle,x,y);
istream
.
cin
and all the ifstream
objects
we declare are of type istream
.
Suppose:
T
whose value I'd like to be the number
of seconds elapsed between two events.
hh:mm:ss
format. This is actually a fair bit of work... not difficult, but time consuming.
cin
) and from a file (using an ifstream
object
named filein
) at various points in my program.
readtime
that could take
either cin
or filein
as an argument, read the
ellapsed time in hh:mm:ss
format, and return it in seconds.
Since both are istream
objects, it would be natural to write the
function
int readtime(istream);
What would it mean? For example:
ostream
and istream
) by reference.
With this in mind, we'd define:
int readtime(istream&);
...
int readtime(istream& in) {
int h, m, s;
char c;
in >> h >> c >> m >> c >> s;
return h*3600 + m*60 + s;
}
With these definitions, I can say
int k = readtime(cin);
when I need to read in a time from the keyboard, and I can say:
int m = readtime(filein);
... when I need to read in a time from the file to which
filein
is attached.
Program Run | data.txt |
Enter file name: data.txt
3" difference |
3' 2" 2' 11" |
readLength
that would take care of the reading and converting for me."
And so we might arrive at the following proposed solution.
Prototype and definition | Main |
|
|
When I run this on the file data.txt
from above,
the result is
0" differenceThis is not what I want. What happened?
readLength
creates the variable
fin
, opens data.txt
and attaches it
to fin
, and reads in the first line, i.e.
3' 2"
.
fin
,
being a local variable, is destroyed.
readLength
, and the
exact same thing happens. In other words, readLength
creates
the variable fin
, opens data.txt
and attaches it to
fin
, and reads in the first line, i.e. 3' 2"
.
main
L1
and L2
both have the same value!
The moral of the story is this:
main
should
be the one that creates an ifstream
object and
attaches it to data.txt
, and that
ifstream
object is what should then be passed to
readLength
in the two function calls.
Prototype and definition | Main |
|
|
Now we have a working program, one that prints out
3"
when we run it on the file data.txt
from above,
just like it's supposed to.
Notice that
readLength
takes the parameter fin
by reference.
Why?
istream
is more flexible than ifstream
readLength
function that is potentially useful in
other programs as well.
readLength
even more usefull than it already is?
cin
has type istream
ifstream
objects like DFin
in the previous
program are also of type istream
(as well as being of type
ifstream
).
istream
and type ifstream
at
the same time? Well, you manage to be of type "college
student" and "midshipman" at the same type. It's possible
because "midshipman" is a specific kind of "college student".
In other words, "midshipman" is a subtype of "college student".
The readLength function doesn't use anything specific to
ifstream
that other istream
objects
don't also have or do. Therefore, we could simply rewrite
readLength
so that it takes an
istream
object.
Prototype and definition | Main |
|
|
Everything works just like before. So what's the advantage?
Well, since cin
is an istream
object
(though not an ifstream
object), we can read a
length from the keyboard with readLength
by the
function call readLength(cin)
. So now we have a
readLength
that's even more powerful because it
can read from files and from the keyboard.
What's the moral of this story? If you have a function that
reads (or writes) information, and if the function doesn't
use any ifstream
(or ofstream
)
specific things like .open
or .close
,
define your function to take an argument of type istream
(or ostream
). This way it can read (or write)
to files and to the screen. BTW: remember to pass by reference!
max
defined as
int max(int a, int b) {
return (b > a) ? b : a;
}
but that my program had three int
s, x
,
y
and z
, amongst which I need the largest. Were
I to write
max(x,y,z)
the compiler would complain ... the only max
function it
knows about only takes two arguments! However, I could say the following:
max(max(x,y),z)
This is our first example of composition of functions. When the
function max
gets called, its two argument expressions are
evaluated. The first is max(x,y)
, which evaluates to the
larger of the two values, and the second is simply z
. So,
what we get out of this is the maximum of all three values.
The most important thing I can tell you about composing functions, is that there is really nothing to talk about. Function arguments are given in your code by expressions, right? And those expressions are evaluated before calling the function to produce the argument objects that are passed to the function. So, whether or not the argument expressions themselves contain function calls is immaterial — the system works the same.
|
|