Classes and member functions build on previously introduced basic function concepts. Please review the following as needed:
One of the benefits of classes is that they provide an intermediate scope, which is more restricted than global scope but less restricted than local scope. An intermediate, class-level scope enables programmers to allow some functions to access member data while denying all other functions access to it. Specifically, C++ controls access to member variables with three keywords: public
, private
, and protected
. In this way, classes provide a mechanism for allowing some functions to share member variables without suffering from the problems that inevitably occur when all functions have access to all variables. Unlike the functions appearing in previous chapters, programs must attach or bind member functions to objects when calling them.
(a) | (b) |
The current chapter focuses on single-class programs; this section emphasizes how member functions access member variables and the concepts surrounding that access.
It is easiest to explore the relationship between member functions and member variables with an example. To better understand the differences between "normal" and member functions, we begin by reviewing the add function as it appears in the original structure version of the Time example, first introduced in chapters 5 and 6.
Function Call | Function Definition |
---|---|
z = add(x, y); |
Time add(Time t1, Time t2) // struct version { int i1 = t1.hours * 3600 + t1.minutes * 60 + t1.seconds; int i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds; return make_time(i1 + i2); } |
The next step continues converting Time from a structure to a class. Specifically, it converts the add function into a member of the Time class.
class Time { private: int hours; int minutes; int seconds; public: Time add(Time t2) { int i1 = hours * 3600 + minutes * 60 + seconds; int i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds; return Time(i1 + i2); } }; |
class Time { private: int hours; int minutes; int seconds; public: Time add(Time t2); }; |
(a) | (b) |
inline
keyword.The class version of the add function seems to be missing the first argument, t1, and all references to it in the body of the function! How can you add one of anything? This puzzle is an object-oriented optical illusion - the class version of the add function does have two Time arguments. Whenever a program calls a member function, the function is called through or bound to an instance of the class that defines the function, and that object is passed to the function where it becomes the default target of the function's operations. The next figure illustrates this important concept.
Function Call | Function Definition |
---|---|
z = x.add(y); |
Time Time::add(Time t2) { int i1 = hours * 3600 + minutes * 60 + seconds; int i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds; return Time(i1 + i2); } |
(a) | (b) |
t2.hours * 3600 + t2.minutes * 60 + t2.secondsthe dot operator binds the explicit argument t2 to the member variables hours, minutes, and seconds. But in the expression
hours * 3600 + minutes * 60 + secondsthe member variables seem unattached to any object. Whenever a member variable is referenced without being explicitly tied to an object, the reference is made to the object bound to the function by default. So, in the calculation of i1, the member variables belong to the object named x, which is the object bound to add by the function call (a).
The this
pointer
this
to store the calling object's address. So, we rewrite the first function statement as
int i1 = this->hours * 3600 + this->minutes * 60 + this->seconds;
It is common practice to call the object bound to a member function "this object," "this argument," or "the implicit argument."
Both objects are clearly visible in the member function call but only one object is explicitly visible in the function definition. Nevertheless, both objects are present in the member function definition, but one object is implied or implicit in how member functions work. From this point forward, it will be convenient to label objects as either implicit or explicit. Doing this warrants a bit of explanation.
These two terms are often confusing, so let's begin by defining them in general before we use them to describe features of an object-oriented program:
Now, we are better positioned to use the terms to help us understand member functions and arguments.
Next, let's explore how we use the terms "implicit" and "explicit" to label objects in a member function.
We label the objects involved in member functions based on their role or position in the function call so we can more easily talk about them. This chapter uses two names: implicit and explicit argument (or implicit and explicit object). The following figure illustrates how these arguments are related to a function call.
Bar foo(); Bar foo(Bar t2); Bar foo(Bar t2, Bar t3); |
x.foo(); |
x.foo(y); |
x.foo(y, z); |
(a) | (b) | (c) | (d) |
Summary
The program temporarily binds function calls to the implicit argument (or object), which is always on the left side of the dot or arrow operator. If there are explicit arguments, the program passes them inside the parentheses. We can see the explicit parameters in the function definition and the corresponding arguments in the function call. We can also see the parameter's name in each function statement using the parameter. However, the implicit argument seems to be missing; nevertheless, it's there, its presence implied by the member function rules. C++ programs pass the implicit object's address to the automatically defined pointer variable namedthis
.
In a sense, we are right back where we began our discussion of implicit and explicit, but in a better position to add some critical detail. All the objects or arguments are visible in a member function call but not in a function definition. The compiler builds member function calls to bind the call to an object automatically, but the mechanism doesn't use the name of the calling object. Nevertheless, the member function may access the member variables of every object involved in the function call, including the implicit and all of the explicit objects. But what about member variables that are private
? The keyword private
secures member variables at the class level but not at the object level. An example is the easiest way to illustrate and understand these concepts, so we return to the Time class and the add function introduced above.
Time y; Time z; . . . Time x = y.add(z); |
(a) |
Time add(Time t2) { int i1 = hours * 3600 + minutes * 60 + seconds; int i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds; return Time(i1 + i2); } |
(b) |
The function call copies object z to the parameter t2, which is explicit (i.e., clearly visible) in the function definition. It copies object y to the implicit add parameter, this. Object-oriented member functions automatically define this, making its presence in the function generally invisible and otherwise unnamed. When the add function needs to access a member variable that belongs to t2 (which is a copy of z), it explicitly names t2 and then uses the dot operator to select the specific member variable: t2.hours. When the add function needs to access a member variable that belongs to the implicit object (which is a copy of y), it does so by simply naming the member variable - the access doesn't need an object name or a selection operator: hours. (If desired, programmers can use an extended notation: this-<hours, but it isn't required.)
As we saw with structures in Chapter 4, we can make it easier to reuse a data structure if we separate the specification (the structure specification and the function prototypes) from the implementation (i.e., the function definitions). This organization is also routine with classes. Take, for example, the string class. If we write a program that needs to use the string class, we add #include <string>
to the top of our program, which includes the class specification and the string function prototypes, in our program. The C++ compiler system places the string class function definitions in a separate source-code file, compiles them to machine code, and stores the machine code in a library. The linker (Windows) or the loader (Unix/Linux/macOS) extracts the string function machine code from the library and incorporates it with our programs when we compile them.
We followed this organization in the chapter 6 version of the Time example, which consisted of three files: Time.h, Time.cpp, and driver.cpp. Converting the Time example from a structure-based to a class-based program requires
Finally, there is one last, fortunately small, change that we need to make when we define a member function outside of the class:
If a function is small, we can include its body in the class. Doing this creates an inline function but without the inline
keyword. The question is, "What is a small function?" There is no absolute size, but a good rule of thumb is that one to three statements constitute a small function, especially if they are short. More than seven statements are too many for a function to be considered small. (Attempting to inline large functions causes the final executable to become unnecessarily large; see Figure 2(b).) Furthermore, any attempt to create an inline function is merely a suggestion that the compiler may silently ignore.
The examples at the end of the chapter include the complete class version of the Time example.
It's appropriate to define short member functions inside a class, but it is better to only prototype longer functions inside the class and define them outside the class. There are two reasons for following this practice: First, it makes class specifications shorter and easier to read, and second, C++ automatically implements functions defined inside the class as inline functions (compare Figures 1 and 2). (However, even when we use the inline
keyword, inlining is only a suggestion that the compiler may choose to ignore.) No clear rule distinguishes a "short" function from a "long" one, but a reasonable rule of thumb is that functions with one to three statements are short, and functions with more than six are long. The size and complexity of the statements themselves govern in the gray area of four to six statements. Finally, we must include the class name and the scope resolution operator to define a member function outside its class.
File | Defined As An Inline Function | Defined Out Of Class | |
---|---|---|---|
In The Class | Inline Keyword | ||
Header (.h) |
Class Foo { public: void function() { . . . . } }; |
Class Foo { public: void function() }; inline void function() { . . . . } |
class Foo { public: void function(); }; |
Source (.cpp) |
void Foo::function() { . . . . } |
a.i | a.ii | b |
---|
inline
keyword.