C++ is generally considered an 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:
For example, the Time structure we defined in Chapter 9 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 alternate syntax for doing things we have already done, but in many cases the alternate 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 pparent that every function takes at least one Time structure as a parameter.
This observation is the motivation for member functions. Member function differ from the other functions we have written in two ways:
In the next few sections, we will take the functions from Chapter 9 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.
As I said, 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.
In Chapter 9 we defined a structure named Time and wrote a function named printTime
struct Time {To call this function, we had to pass a Time object as a parameter.
Time currentTime = { 9, 14, 30.0 };To make printTime into a member function, the first step is to change the name of the function from printTime 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 parameter 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. A pointer is similar to a reference, but I don't want to go into the details of using pointers yet. The only pointer operation we need for now is the * operator, which converts a structure pointer into a structure. In the following function, we use it to assign the value of this to a local variable named time:
void Time::print () {The first two lines of this function changed quite a bit as we transformed it into a member function, but notice that the output statement 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 currentTime = { 9, 14, 30.0 };The last step of the transformation process is that we have to declare the new function inside the structure definition:
struct Time {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 definition 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.
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 ()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.
Let's convert increment to a member function. Again, we are going to transform one 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) {By the way, remember that this is not the most efficient implementation of this function. If you didn't do it back in Chapter 9, you should write a more efficient version now.
To declare the function, we can just copy the first line into the structure definition:
struct Time {And again, to call it, we have to invoke it on a Time object:
Time currentTime = { 9, 14, 30.0 };The output of this program is 9:22:50.
The original version of convertToSeconds looked like this:
double convertToSeconds (const Time& time) {It is straightforward to convert this to a member function:
double Time::convertToSeconds () const {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 exist. The answer, as you can see in the example, is after the parameter list (which is empty in this case).
The print function in the previous section should also declare that the implicit parameter is const.
Although the process of transforming functions into member functions is mechanical, 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 the them implicitly, but to access the instance variables of the other we continue to use dot notation.
bool Time::after (const Time& time2) const {To invoke this function:
if (doneTime.after (currentTime)) {You can almost read the invocation like English: "If the done-time is after the current-time, then..."
Another function we wrote in Chapter 9 was makeTime:
Time makeTime (double secs) {Of course, for every new type, we need to be able to create new objects. In fact, functions like makeTime 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) {First, notice that the constructor has the same name as the class, 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 referring 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.
Earlier we declared and initialized some Time structures using squiggly-braces:
Time currentTime = { 9, 14, 30.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 in the same program.
If you define a constructor for a structure, then you have to use the constructor to initialize all new structures of that type. The alternate syntax using squiggly-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 takes the ppropriate parameters.
For example, it is common to have a constructor that takes one parameter for each instance variable, and that assigns the values of the parameters to the instance variables:
Time::Time (int h, int m, double 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 currentTime (9, 14, 30.0);The final example we'll look at is addTime:
Time addTime2 (const Time& t1, const Time& t2) {We have to make several changes to this function, including:
Here's the result:
Time Time::add (const Time& t2) const {The first time we invoke convertToSeconds, there is no pparent 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 next line of the function invokes the constructor that takes a single double as a parameter; the last line returns the resulting object.
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 {Notice that in the structure definition I don't really have to include the prefix Time:: at the beginning of every function name. The compiler knows that we are declaring functions that are members of the Time structure.
Time.cpp contains the definitions of the member functions (I have elided the function bodies to save space):
#include <iostream.h>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 there are none):
#include <iostream.h>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 programs:
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 one of the statements that appeared in the first program we wrote:
#include <iostream.h>iostream.h is the header file that contains declarations for cin and cout and the functions that operate on them. When you compile your program, you need the information in that header file.
The implementations of those functions 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.