11. Member functions¶
11.1. Objects and functions¶
C++ is generally considered and object-oriented programming language, which means that it provides features that support object-oriented programming.
It’s not easy to define object-oriented programming, but we have already seen some features of it:
Programs are made up of a collection of structure definitions and function definitions, where most of the functions operate on specific kinds of structures (or objects).
Each structure definition corresponds to some object or concept in the real world, and the functions that operate on that structure correspond to the ways real-world objects interact.
For example, the Time
structure we defined in the
Structures chapter obviously corresponds to the way people
record the time of day, and the operations we defined correspond to the sorts
of things people do with recorded times. Similarly, the Point
and
Rectangle
structures correspond to the mathematical concept of a point and
a rectangle.
So far, though, we have not taken advantage of the features C++ provides to support object-oriented programming. Strictly speaking, these features are not necessary. For the most part they provide an alternative syntax for doing things we have already done, but in many cases the alternative syntax is more concise and more accurately conveys the structure of the program.
For example, in the Time
program, there is no obvious connection between
the structure definition and the function definitions that follow. With some
examination, it is apparent that every function takes at least one Time
structure as an argument.
This observation is the motivation for member functions. Member functions differ from other functions in two ways:
When we call the function, we invoke in on an object, rather than just call it. People sometimes describe this process as “performing an operation on an object” or “sending a message to an object.”
The function is declared inside the
struct
definition, in order to make the relationship between the structure and the function explicit.
In the next few sections, we will take the functions from the Structures chapter and transform them into member functions. One thing you should realize is that this transformation is purely mechanical; in other words, you can do it just by following a sequence of steps.
Anything that can be done with a member function can also be done with a nonmember function (sometimes called a free-standing function). But sometimes there is an advantage to one over the other. If you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing.
11.2. print
¶
In the Structures chapter we wrote a fucntion named
print_time
that operated on Time
structures.
struct Time {
int hour, minute;
double second;
};
void print_time(const Time& time)
{
cout << time.hour << ":" << time.minute << ":" << time.second << endl;
}
To call this function, we had to pass a Time
object as an argument.
Time current_time = {9, 14, 30.0};
print_time(current_time);
To make print_time
into a member function, the first step is to change the
name of the function from print_time
to Time::print
. The ::
operator
separates the name of the structure from the name of the function; together
they indicate that this is a function named print
that can be invoked on a
Time
structure.
The next step is to eliminate the parameter. Instead of passing an object as an argument, we are going to invoke the function on an object.
As a result, inside the function, we no longer have a paramenter named
time
. Instead, we have a current object, which is the object the
function is invoked on. We can refer to the current object using the C++
keyword this.
One thing that makes life a little difficult is that this
is actually a
pointer to a structure, rather than a structure itself. In the following
function, we use it to assign the value of this
to a local variable named
time
:
void Time::print()
{
Time time = *this;
cout << time.hour << ":" << time.minute << ":" << time.second << endl;
}
The first two lines of the function changed quite a bit as we transformed it into a member function, but notice that the output stream itself did not change at all.
In order to invoke the new version of print
, we have to invoke it on a
Time
object:
Time current_time = {9, 14, 30.0};
current_time.print();
The last step of the transformation process is that we have to declare the new function inside the structure definition:
struct Time {
int hour, minute;
double second;
void print();
};
A function declaration looks just like the first line of the function definition, except that it has a semi-colon at the end. The declaration describes the interface of the function; that is, the number and types of the arguments, and the type of the return value.
When you declare a function, you are making a promise to the compiler that you will, at some point later on in the program, provide a definition for the function. This is sometimes called the implementation of the function, since it contains the details of how the function works. If you omit the definition, or provide a definition that has an interface different from what you promised, the compiler will complain.
11.3. Implicit variable access¶
Actually, the new version of Time::print
is more complicated than it needs
to be. We don’t really need to create a local variable in order to refer to the
instance variables of the current object.
If the function refers to hour
, minute
, or second
, all by
themselves with no dot notation, C++ knows that it must be referring to the
current object. So we could have written:
void Time::print()
{
cout << hour << ":" << minute << ":" << second << endl;
}
This kind of variable access is called “implicit” because the name of the object does not appear explicitly. Features like this are one reason member functions are often more concise than nonmember functions.
11.4. Another example¶
Let’s convert increment
to a member function. Again, we are going to
transform on of the parameters into the implicit parameter called this
.
Then we can go through the function and make all the variable accesses
implicit.
void Time::increment(double secs)
{
second += secs;
while (second >= 60.0) {
second -= 60.0;
minute += 1;
}
while (minute >= 60) {
minute -= 60;
hour += 1;
}
}
To declare the function, we can just copy the first line into the structure
definition and remove the Time::
in front of it:
struct Time {
int hour, minute;
double second;
void print();
void increment(double secs);
};
And again, to call it, we have to invoke it on a Time
object:
Time current_time = {9, 14, 30.0};
current_time.increment(500.0);
current_time.print();
The output of this program is 9:22:50.
11.5. Yet another example¶
The original version of convert_to_seconds
looked like this:
double convert_to_seconds(const Time& time)
{
int minutes = time.hour * 60 + time.minute;
double seconds = minutes * 60 + time.second;
return seconds;
}
It is straightforward to convert this to a member function:
double Time::convert_to_seconds() const
{
int minutes = hour * 60 + minute;
double seconds = minutes * 60 + second;
return seconds;
}
The interesting thing here is that the implicit parameter should be declared
const
, since we don’t modify it in this function. But it is not obvious
where we should put information about a parameter that doesn’t explicitely
exist. The answer, as you can see in this example, is after the parameter list
(which is empty in this case).
The print
function in the Implicit variable access section should
also declare that the implicit parameter is const
.
Implicit variable access makes this function easy enough to read that we can even drop the temporary variables without harming readability:
double Time:convert_to_seconds() const
{
return (hour * 60 + minute) * 60 + second;
}
11.6. A more complicated example¶
Although the process of transforming functions into member functions is
machanical, there are some oddities. For example, after
operates on two
Time
structures, not just one, and we can’t make both of them implicit.
Instead, we have to invoke the function on one of them and pass the other as an
argument.
Inside the function, we can refer to one of them implicitly, but to access the instance variables of the other we continue to use dot notation.
bool Time::after(const Time& t2) const
{
if (hour > t2.hour) return true;
if (hour < t2.hour) return false;
if (minute > t2.minute) return true;
if (minute < t2.minute) return false;
return (second > t2.second);
}
To invoke this function:
if (done_time.after(current_time)) {
cout << "The bread will be done after it starts." << endl;
}
You can almost read the invocation like English: “If the done-time is after the current time, then…”
11.7. Constructors¶
Another function we wrote in the Structures chapter was
make_time
:
Time make_time(double secs)
{
Time time;
time.hour = int(secs / 3600.0);
secs -= time.hour * 3600.0;
time.minute = int(secs / 60.0);
secs -= time.minute * 60;
time.second = secs;
return time;
}
Of course, for every new type, we need to be able to create new objects of
that type. In fact, functions like make_time
are so common that there is
a special function syntax for them. These functions are called constructors
and the syntax looks like this:
Time::Time(double secs)
{
hour = int(secs / 3600.0);
secs -= hour * 3600.0;
minute = int(secs / 60.0);
secs -= minute * 60;
second = secs;
}
First, notice that the constructor has the same name as the struct, and no return type. The arguments haven’t changed, though.
Second, notice that we don’t have to create a new time object, and we don’t
have to return anything. Both of these steps are handled automatically. We can
refer to the new object - the one we are constructing - using the keyword
this
, or implicitly as shown here. When we write values to hour
,
minute
, and second
, the compiler knows we are refering to the instance
variables of the new object.
To invoke the constructor, you use syntax that is a cross between a variable declaration and a function call:
Time time(seconds);
This statement declares that the variable time
has type Time
, and it
invokes the constructor we just wrote, passing the value of seconds
as an
argument. The system allocates space for the new object and the constructor
initializes its instance variables. The result is assigned to the variable
time
.
11.8. Initialize or construct?¶
Earlier we declared and initialized some Time
structures using
curly-braces:
Time current_time = {9, 14, 30};
Time bread_time = {3, 35, 0};
Now, using constructors, we have a different way to declare and initialize:
Time time(seconds);
These two functions represent different programming styles, and different points in the history of C++. Maybe for that reason, the C++ compiler requires that you use one or the other, and not both.
If you define a constructor for a structure, then you have to use it to initialize all new structures of that type. The alternative syntax using curly-braces is no longer allowed.
Fortunately, it is legal to overload constructors in the same way we overloaded functions. In other words, there can be more than one constructor with the same “name,” as long as they take different parameters. Then, when we initialize a new object the compiler will try to find a constructor that has matching parameters.
For example, it is common to have a constructor that has a parameter for each instance variable, and that assigns the values of the arguments to the instance variables:
Time::Time(int h, int m, double s)
{
hour = h; minute = m; second = s;
}
To invoke this constructor, we use the same funny syntax as before, except that the arguments have to be two integers and a double:
Time current_time(9, 14, 30.0);
11.9. One last Time example¶
The final example we’ll look at is add_time
:
Time add_time(const Time& t1, const Time& t2)
{
return make_time(convert_to_seconds(t1) + convert_to_seconds(t2));
}
We have to make several changes to make it a member function, including:
Change the name from
add_time
toTime::add
.Replace the first parameter with an implicit parameter, which should be declared
const
.Replace the use of
make_time
with a constructor invocation.
Here’s the result:
Time Time::add(const Time& t2) const
{
return Time(convert_to_seconds() + t2.convert_to_seconds());
}
The first time we invoke convert_to_seconds
, there is no apparent object!
Inside a member function, the compiler assumes that we want to invoke the
function on the current object. Thus, the first invocation acts on this
;
the second invocation acts on t2
.
The result of these two function calls are added, and the sum is sent to the
version of the Time
constructor that takes a single double. Finally the
Time
object returned by this constructor is returned by the add
member
function.
11.10. Header files¶
It might seem like a nuisance to declare functions inside the structure
definition and then define the functions later. Any time you change the
interface to a function, you have to change it in two places, even if it is a
small change like declaring one of the parameters const
.
There is a reason for the hassle, though, which is that it is now possible to separate the structure definition and the functions into two files: the header file, which contains the structure definition, and the implementation file, which contains the functions.
Header files usually have the same name as the implementation file, but with
the suffix .h
instead of .cpp
. For the example we have been looking at,
The header file is called Time.h
, and it contains the following:
struct Time {
// instance variables
int hour, minute;
double second;
// constructors
Time(int, int, double);
Time(double);
// modifiers
void increment(double);
// functions
void print() const;
bool after(const Time&) const;
Time add(const Time&) const;
double convert_to_seconds() const;
};
Time.cpp
contains the definitions of the member functions (we have elided
the function bodies to save space):
#include <iostream>
#include "Time.h"
using namespace std;
Time::Time(int h, int m, double s) ...
Time::Time(double secs) ...
void Time::increment(double s) ...
void Time::print() const ...
bool Time::after(const Time& time2) const ...
Time Time::add(const Time& t2) const ...
double Time::convert_to_seconds() const ...
In this case the definitions in Time.cpp
appear in the same order as the
declarations in Time.h
, although it is not necessary.
On the other hand, it is necessary to include the header file using an
include
statement. That way, while the compiler is reading the function
definitions, it knows enough about the structure to check the code and catch
errors.
Finally, main.cpp
contains the function main
along with any functions
we want that are not members of the Time
structure (in this case their
aren’t any):
#include <iostream>
#include "Time.h"
using namespace std;
void main()
{
Time current_time(9, 14, 30.0);
current_time.increment(500.0);
current_time.print();
cout << endl;
Time bread_time(3, 35, 0.0);
Time done_time = current_time.add(bread_time);
done_time.print();
cout << endl;
if (done_time.after(current_time)) {
cout << "The bread will be done after it starts." << endl;
}
}
Again, main.cpp
has to include the header file.
It may not be obvious why it is useful to break such a small program into three pieces. In fact, most of the advantages come when we are working with larger program:
- Reuse:
Once you have written a structure like
Time
, you might find it useful in more than one program. By separating the definition ofTime
frommain.cpp
, you make it easy to include theTime
structure in another program.- Managing interactions:
As sytems become large, the number of interactions between components grows and quickly becomes unmanageable. It is often useful to minimize these interactions by separating modules like
Time.cpp
from the programs that use them.- Separate compilation:
Separate files can be compiled separately and then linked together into a single program later. The details of how to do this depend on your programming environment. As the program gets large, separate compilation can save a lot of time, since you usually need to compile only a few files at a time.
For small programs like the ones in this book, there is no great advantage to splitting up programs. But it is good for you to know about this feature, especially since it explains the statements that appeared in the first program we wrote:
#include <iostream>
using namespace std;
The first line includes the iostream
header file that contains the
namespace std
, which in turn
contains declarations for cin
and cout
and the functions that operate
on them. We could have written std::cout
every time we wanted to use
cout
, but the second line, using namespace std;
, eliminates the need
to do so.
The implementations of the functions cin
and cout
are stored in a
library, sometimes called the
standard library, that
gets linked to your program automatically. The nice thing is that you don’t
have to recompile the library every time you compile a program. For the most
part, the library doesn’t change, so there is no reason to recompile it.
11.11. The arrow operator ->
and this
¶
Our own objects are data types that we created, and we can also create pointer variables to their addresses in memory.
With our previously created Point objects:
struct Point {
double x, y;
};
we can now create a pointer to a Point
with:
Point p = {3, 4};
Point* pp = &p;
The question is, can we access the instance variables x
and y
from our
pointer pp
? It turns out we can, but we need a new operator, ->
,
called the arrow operator:
pp->x = 5; // p.x is now 5
The arrow operator enables access to instance variables and member functions of a pointer to an object.
If we want to turn our Point
structure into an object with constructors
and member functions, we might want a constructor that takes the values of
x
and y
as arguments and initializes the instance variables:
struct Point {
double x, y;
Point(double x, double y) {
this->x = x;
this->y = y;
}
};
The this
pointer is an implicit parameter for all member functions,
pointing to the data members of the instance of the object.
11.12. Overloading the +
operator¶
A natural operation we might want to perform on points is addition. Addition
for Cartesian points is easy: you just add the x’s and y’s together. We could
create an add
member function much like we did with Time
s, but since
C++ supports operator overloading we can instead define
the +
operator for Point
values:
Point Point::operator+(const Point& other) {
return Point(x + other.x, y + other.y);
}
The left operand will now be implicitly referenced, while the right operand is passed as a constant reference parameter. We can now create two points and then create a third point that is the sum of these.
Point p1 = Point(3, 4);
Point p2 = Point(2, 1);
Point p3 = p1 + p2;
We can also overload the stream insertion operator, <<
, so that we can
output points just like we do built-in types.
Let’s first add a to_string()
member function like we did for Time
objects.
string Point::to_string() {
string s = "(" + std::to_string(x) + ", " + std::to_string(y) + ")";
return s;
}
Now we can now overload the stream insertion operator, <<
, to take an
output stream. and a
Point
as arguments, and to use our new to_string()
member function to
send the sequence of characters we want to the output stream.
ostream& operator<<(ostream& out, Point p) {
out << p.to_string();
return out;
}
Notice that is not a member function. The header file, Point.h
, looks
like this:
#include <iostream>
#include <string>
using namespace std;
struct Point
{
double x, y;
Point();
Point(double, double);
Point operator+(const Point&);
string to_string();
};
ostream& operator<<(ostream&, Point);
You will further explore Points
and Times
in the exercises.
11.13. Glossary¶
- constructor(2)¶
A special function that initializes the instance variables of a newly-created object.
- function declaration¶
A statement that declares the interface to a function without providing the body. Declarations of member functions appear inside structure definitions even if the definitions appear outside.
- implementation¶
The body of a function, or the details of how a function works.
- interface¶
A description of how a function is used, including the number and types of the parameters and the type of the return value.
- invoke¶
To call a function “on” an object, in order to pass the object as an implicit parameter.
- operator overloading¶
Defining built-in operators (
+
,-
,*
,/
, etc.) for user-defined types. C++ supports this usingoperator
followed by the operator symbol being defined as the name of a member function.