In This Chapter
Grouping data into classes
Declaring and defining class members
Adding active properties to the class
Accessing class member functions
Overloading member functions
Programs often deal with groups of data: a person's name, rank, and serial number, stuff like that. Any one of these values is not sufficient to describe a person — only in the aggregate do the values make any sense. A simple structure such as an array is great for holding stand-alone values, but it doesn't work well for data groups. This makes good ol' arrays inadequate for storing complex data (such as personal credit records that the Web companies maintain so they can lose them to hackers).
For reasons that will become clear shortly, I'll call such a grouping of data an object. A microwave oven is an object (see Chapter 11 if that doesn't make sense). You are an object (no offence). Your savings account information in a database is an object.
How nice it would be if we could create objects in C++ that have the relevant properties of the real-world objects we're trying to model. What we need is a structure that can hold all the different types of data necessary to describe a single object. C++ calls the structure that combines multiples pieces of data into a single object a class.
A class consists of the keyword class
followed by a name and an open and closed brace. A class used to describe a savings account including account number and balance might appear as follows:
class SavingsAccount { public: unsigned accountNumber; double balance; };
The statement after the open brace is the keyword public
. (Hold off asking about the meaning of the public
keyword. I'll make its meaning public a little later.)
The alternative keyword struct
can be used in place of class
. The two keywords are identical except that the public
declaration is assumed in the struct
and can be omitted. You should stick with class
for most programs for reasons that will become clear later in this chapter.
Following the public
keyword are the entries it takes to describe the object. The SavingsAccount
class contains two elements: an unsigned integer accountNumber
and the account balance
. We can also say that accountNumber
and balance
are members or properties of the class SavingsAccount
.
To create an actual savings account object, I type something like the following:
SavingsAccount mySavingsAccount;
We say that mySavingsAccount
is an instance of the class SavngsAccount
.
The naming convention used here is common: Class names are normally capitalized. In a class name with multiple words such as SavingsAccount
, each word is capitalized, and the words are jammed together without an underscore. Object names follow the same rule of jamming multiple words together, but they normally start with a small letter, as in mySavingsAccount
. As always, these norms (I hesitate to say rules) are to help out the human reader — C++ doesn't care one way or the other.
The following syntax is used to access the property of a particular object:
// Create a savings account object SavingsAccount mySave; mySave.accountNumber = 1234; mySave.balance = 0; // Input a second savings account from the keyboard cout << "Input your account number and balance" << endl; SavingsAccount urSave; cin >> urSave.accountNumber; cin >> urSave.balance;
This code snippet declares two objects of class SavingsAccount, mySave
and urSave
. The snippet initializes mySave
by assigning a value to the account number and a 0 to the balance (as per usual for my savings account). It then creates a second object of the same class, urSave
. The snippet reads the account number and balance from the keyboard.
An important point to note in this snippet is that mySave
and urSave
are separate, independent objects. Manipulating the members of one has no effect on the members of the other (lucky for urSave
).
In addition, the name of the member without an associated object makes no sense. I cannot say either of the following:
balance = 0; // illegal; no object SavingsAccount.balance = 0; // class but still no object
Every savings account has its own unique account number and maintains a separate balance. (There may be properties that are shared by all savings accounts — we'll get to those soon enough — but account and balance don't happen to be among them.)
You use classes to simulate real-world objects. The Savings
class tries to represent a savings account. This allows you to think in terms of objects rather than simply lines of code. The closer C++ objects are to modeling the real world, the easier it is to deal with them in programs. This sounds simple enough. However, the Savings
class doesn't do a very good job of simulating a savings account.
Real-world objects have data-type properties such as account numbers and balances, the same as the Savings
class. This makes Savings
a good starting point for describing a real object. But real-world objects do things. Ovens cook. Savings accounts accumulate interest; CDs charge a substantial penalty for early withdrawal — stuff like that.
Functional programs "do things" through functions. A C++ program might call strcmp()
to compare two character strings or max()
to return the maximum of two values. In fact, Chapter 23 explains that even stream I/O (cin >>
and cout <<
) is a special form of function call
.
The Savings
class needs active properties of its own if it's to do a good job of representing a real concept:
class Savings { public: double deposit(double amount) { balance += amount; return balance; } unsigned accountNumber; double balance; };
In addition to the account number and balance, this version of Savings
includes the function deposit()
. This gives Savings
the ability to control its own future. A class MicrowaveOven
has the function cook()
, the class Savings
has the function accumulateInterest()
, and the class CD
has a function to penalizeForEarlyWithdrawal()
.
Why should you bother with member functions? What's wrong with the good ol' days of functional programming?
class Savings { public: unsigned accountNumber;
double balance; }; double deposit(Savings& s, double amount) { s.balance += amount; return s.balance; }
Here, deposit()
implements the "deposit into savings account" function. This functional solution relies on an outside function, deposit()
, to implement an activity that savings accounts perform but that Savings
lacks. This gets the job done, but it does so by breaking the object-oriented (OO) rules.
The microwave oven has internal components that it "knows" how to use to cook, defrost, and burn to a crisp. Class data members are similar to the parts of a microwave — the member functions of a class perform cook-like functions.
When I make nachos, I don't have to start hooking up the internal components of the oven in a certain way to make it work. Nor do I rely on some external device to reach into a mess of wiring for me. I want my classes to work the same way my microwave does (and, no, I don't mean "not very well"). I want my classes to know how to manipulate their internals without outside intervention.
To demonstrate member functions, start by defining a class Student
. One possible representation of such a class follows (taken from the program CallMemberFunction):
class Student { public: // add a completed course to the record float addCourse(int hours, float grade) { // calculate the sum of all courses times // the average grade float weightedGPA; weightedGPA = semesterHours * gpa; // now add in the new course semesterHours += hours; weightedGPA += grade * hours; gpa = weightedGPA / semesterHours;
// return the new gpa return gpa; } int semesterHours; float gpa; };
The function addCourse(int, float)
is called a member function of the class Student
. In principle, it's a property of the class like the data members semesterHours
and gpa
.
Sometimes functions that are not members of a class are class "plain ol' functions," but I'll refer to them simply as nonmembers.
The member functions do not have to precede the data members as in this example. The members of a class can be listed in any order — I just prefer to put the functions first.
For historical reasons, member functions are also called methods. This term originated in one of the original object-oriented languages. The name made sense there, but it makes no sense in C++. Nevertheless, the term has gained popularity in OO circles because it's easier to say than "member function." (The fact that it sounds more impressive probably doesn't hurt, either.) So, if your friends start spouting off at a dinner party about "methods of the class," just replace methods with member functions and reparse anything they say.
The following CallMemberFunction
program shows how to invoke the member function addCourse()
:
// // CallMemberFunction - define and invoke a function // that's a member of the class Student // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class Student { public: // add a completed course to the record float addCourse(int hours, float grade) {
// calculate the sum of all courses times // the average grade float weightedGPA; weightedGPA = semesterHours * gpa; // now add in the new course semesterHours += hours; weightedGPA += grade * hours; gpa = weightedGPA / semesterHours; // return the new gpa return gpa; } int semesterHours; float gpa; }; int main(int nNumberofArgs, char* pszArgs[]) { // create a Student object and initialize it Student s; s.semesterHours = 3; s.gpa = 3.0; // the values before the call cout << "Before: s = (" << s.semesterHours << ", " << s. gpa << ")" << endl; // the following subjects the data members of the s // object to the member function addCourse() cout << "Adding 3 hours with a grade of 4.0" << endl; s.addCourse(3, 4.0); // call the member function // the values are now changed cout << "After: s = (" << s.semesterHours << ", " << s. gpa << ")" << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
The syntax for calling a member function looks like a cross between the syntax for accessing a data member and that used for calling a function. The right side of the dot looks like a conventional function call, but an object is on the left of the dot.
In the call s.addCourse()
, we say that "addCourse()
operates on the object s
" or, said another way, "s is the student to which the course is to be added." You can't fetch the number of semester hours without knowing from which student to fetch those hours — you can't add a student to a course without knowing which student to add. Calling a member function without an object makes no more sense than referencing a data member without an object.
I can see it clearly: You repeat to yourself, "Accessing a member without an object makes no sense. Accessing a member without an object. Accessing ..." Just about the time you've accepted this, you look at the member function Student::addCourse()
and Wham! It hits you: addCourse()
accesses other class members without reference to an object. Just like the TV show: "How Do They Do That?"
Okay, which is it, can you or can't you? Believe me, you can't. When you reference a member of Student
from addCourse()
, that reference is against the Student
object with which the call to addCourse()
was made. Huh? Go back to the CallMemberFunction example. A stripped-down version appears here:
int main(int nNumberofArgs, char* pszArgs[]) { Student s; s.semesterHours = 10; s.gpa = 3.0; s.addCourse(3, 4.0); // call the member function Student t; t.semesterHours = 6; t.gpa = 1.0; // not doing so good t.addCourse(3, 1.5); // things aren't getting // much better system("PAUSE"); return 0; }
When addCourse()
is invoked with the object s
, all of the otherwise unqualified member references in addCourse()
refer to s
as well. Thus, the reference to semesterHours
in addCourse()
refers to s.semesterHours
, and gpa
refers to s.gpa
. But when addCourse()
is invoked with the Student t
object, these same references are to t.semesterHours
and t.gpa
instead.
The object with which the member function was invoked is the "current" object, and all unqualified references to class members refer to this object. Put another way, unqualified references to class members made from a member function are always against the current object.
The ::
between a member and its class name is called the scope resolution operator because it indicates the class to which a member belongs. The class name before the colon is like the family last name, while the function name after the colons is like the first name — the order is similar to a Chinese name, family name first.
You use the ::
operator to describe a nonmember function by using a null class name. The nonmember function addCourse
, for example, can be referred to as ::
addCourse(int, float)
, if you prefer. This is like a function without a home.
Normally the ::
operator is optional, but there are a few occasions when this is not so, as illustrated here:
// addCourse - combine the hours and grade into // a weighted grade float addCourse(int hours, float grade) { return hours * grade; } class Student { public: // add a completed course to the record float addCourse(int hours, float grade) { // call some external function to calculate the // weighted grade float weightedGPA= ::addCourse(semesterHours,gpa); // now add in the new course semesterHours += hours; // use the same function to calculate the weighted // grade of this new course weightedGPA += ::addCourse(hours, grade); gpa = weightedGPA / semesterHours; // return the new gpa return gpa; } int semesterHours; float gpa; };
Here, I want the member function Student::addCourse()
to call the nonmember function ::
addCourse()
. Without the ::
operator, however, a call to addCourse()
from Student
refers to Student::addCourse()
. This would result in the function calling itself.
A member function can be defined either in the class or separately. When defined in the class definition, the function looks like the following, which is contained in the include file Savings.h
:
// Savings - define a class that includes the ability // to make a deposit class Savings { public: // define a member function deposit() float deposit(float amount) { balance += amount; return balance; } unsigned int accountNumber; float balance; };
Using an include like this is pretty slick. Now a program can include the class definition (along with the definition for the member function), as follows in the venerable SavingsClass_inline program:
// // SavingsClassInline - invoke a member function that's // both declared and defined within // the class Student // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; #include "Savings.h" int main(int nNumberofArgs, char* pszArgs[]) { Savings s; s.accountNumber = 123456; s.balance = 0.0; // now add something to the account cout << "Depositing 10 to account " << s.accountNumber << endl; s.deposit(10); cout << "Balance is " << s.balance << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
This is cool because everyone other than the programmer of the Savings
class can concentrate on the act of performing a deposit rather the details of banking. These details are neatly tucked away in their own include files.
The #include
directive inserts the contents of the file during the compilation process. The C++ compiler actually "sees" your source file with the contents of the Savings.h
file included. See Chapter 10 for details on include files.
For larger functions, putting the code directly in the class definition can lead to some large, unwieldy class definitions. To prevent this, C++ lets you define member functions outside the class.
A function that is defined outside the class is said to be an outline function. This term is meant to be the opposite of an inline function that has been defined within the class. Your basic functions such as those we have defined since Chapter 5 are also outline functions.
When written outside the class declaration, the Savings.h
file declares the deposit()
function without defining it as follows:
// Savings - define a class that includes the ability // to make a deposit class Savings { public: // declare but don't float deposit(float amount); unsigned int accountNumber; float balance; };
The definition of the deposit()
function must be included in one of the source files that make up the program. For simplicity, I defined it within main.cpp
.
You would not normally combine the member function definition with the rest of your program. It is more convenient to collect the outlined member function definitions into a source file with an appropriate name (such as Savings.cpp
). This source file is combined with other source files as part of building the executable program. I describe this in Chapter 21.
// // SavingsClassOutline - invoke a member function that's // declared within a class but // defined in a separate file // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; #include "Savings.h" // define the member function Savings::deposit() // (normally this is contained in a separate file that is // then combined with a different file that is combined) float Savings::deposit(float amount) { balance += amount; return balance; }
// the main program int main(int nNumberofArgs, char* pszArgs[]) { Savings s; s.accountNumber = 123456; s.balance = 0.0; // now add something to the account cout << "Depositing 10 to account " << s.accountNumber << endl; s.deposit(10); cout << "Balance is " << s.balance << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
This class definition contains nothing more than a prototype declaration for the function deposit()
. The function definition appears separately. The member function prototype declaration in the structure is analogous to any other prototype declaration and, like all prototype declarations, is required.
Notice how the function nickname deposit()
was good enough when the function was defined within the class. When defined outside the class, however, the function requires its extended name, Savings::deposit()
.
Member functions can be overloaded in the same way that conventional functions are overloaded. (See Chapter 6 if you don't remember what that means.) Remember, however, that the class name is part of the extended name. Thus, the following functions are all legal:
class Student { public: // grade -- return the current grade point average float grade(); // grade -- set the grade and return previous value float grade(float newGPA); // ...data members and other stuff... };
class Slope { public: // grade -- return the percentage grade of the slope float grade(); // ...stuff goes here too... }; // grade - return the letter equivalent of a number grade char grade(float value); int main(int argcs, char* pArgs[]) { Student s; s.grade(3.5); // Student::grade(float) float v = s.grade(); // Student::grade() char c = grade(v); // ::grade(float) Slope o; float m = o.grade(); // Slope::grade() return 0; }
Each call made from main()
is noted in the comments with the extended name of the function called.
When calling overloaded functions, not only the arguments of the function but also the type of the object (if any) with which the function is invoked are used to resolve the call. (The term resolve is object-oriented talk for "decide at compile time which overloaded function to call." A mere mortal might say "differentiate.")
18.221.66.185