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, a scope that 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, member functions must be attached or bound to an object when they are called.
(a) | (b) |
print();
If the program called the function this way, what would the function print?p0.print();
p1.print();
p2.print();
The current focus is on single-class programs, and so we emphasize how member functions access member variables and the concepts that surround 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 functions 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 is sort of 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 in a better position 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) |
Bar
is the name of a classBar
defines three overloaded functions named foo
x
, y
, and z
are instances of Bar
x
is the implicit argument and there are no explicit argumentsx
is the implicit argument and y
is an explicit argumentx
is the implicit argument and y
and z
are both explicit argumentsSummary
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. But the implicit argument seems to be missing, but it's there, its presence implied by the member function rules.In a sense, we are right back where we began our discussion of implicit and explicit, but we are 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. Each concept that this paragraph touches upon is more easily illustrated with an example, and 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) |
Object z is copied to the parameter t2, which is explicit (i.e., clearly visible) in the function definition. Object y is copied to the implicit add parameter, which is implicit (i.e., its presences is suggested by how member functions work but it is not named in the definition). When the add function needs to access a member variable that belong 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 - neither an object name nor the dot operator is needed: hours.
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, including all of the string functions, in our program. The string class function definitions are placed in a file named "string.cpp" and compiled to create an object file (ends with a ".obj" extension on Windows and with a ".o" extension on Unix and Linux). The object code is placed in a library and is extracted from the library and included in our program by the linker (Windows) or the loader (Unix/Linux/macOS).
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
If a function is small, we might 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 probably 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 complete class version of the Time example is included in the examples at the end of the chapter.
Short member functions are appropriately defined 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, functions defined inside the class are implemented 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 |
Class Foo { public: void function() { . . . . } }; |
Class Foo { public: void function() }; inline void function() { . . . . } |
class Foo { public: void function(); }; |
Source |
void Foo::function() { . . . . } |
a.i | a.ii | b |
---|
inline
keyword.