Classes = Data + Responsibility
Dr. Lawlor, CS
202, CS, UAF
In C++, a "class" is basically a struct with some functions inside
of it. The basic idea behind a class is to collect together both
data and the code that works on the data. Similar to how
functions collect related bits of code, and a struct collects related
variables, a class is a sort of hybrid of the two, containing both code
and data.
A group of...
|
Is called a...
|
related code
|
function
|
related data
|
struct
|
related data and code
|
class
|
For example, a "person" class might collect together both "data
members", like the person's name and salary, and functions like getting
their paycheck. Here's the syntax--basically a class is like a
struct, but with functions (called "methods") inside of it. Note
that "get_paycheck" doesn't need any parameters, because it is called
on an "instance" of a class (here "boss").
class person {
public:
string name; // the person's full legal name
int salary; // how much the person makes per hour
// Return the amount of this person's weekly paycheck.
int get_paycheck(void) {
return salary * 40;
}
};
int foo(void) {
person boss;
boss.name="James Pocketsworth IV";
boss.salary=137;
return boss.get_paycheck();
}
(Try this in NetRun now!)
Notice that inside the class method "get_paycheck", "salary" refers to
the current person instance. If we create a different instance,
then the new instance gets its own salary:
int foo(void) {
person boss;
boss.name="James Pocketsworth IV";
boss.salary=137;
person minion;
minion.name="Marty McMinion";
minion.salary=7;
cout<<"The boss makes "<<boss.get_paycheck()<<" / week\n";
cout<<"The minion makes "<<minion.get_paycheck()<<" / week\n";
return boss.get_paycheck()+minion.get_paycheck();
}
(Try this in NetRun now!)
The nice part about collecting everything about a person into one class
is we can now write bigger, more useful methods, without having to pass
lots of parameters. For example, we can add a method to print
paychecks:
class person {
public:
string name; // the person's full legal name
int salary; // how much the person makes per hour
// Return the amount of this person's weekly paycheck.
int get_paycheck(void) {
return salary * 40;
}
// Format and print this person's weekly paycheck
void print_paycheck(void) {
cout<<"Pay to the order of: "<<name<<"\n";
cout<<" the sum of "<<get_paycheck()<<" dollars\n";
}
};
int foo(void) {
person boss;
boss.name="James Pocketsworth IV";
boss.salary=137;
person minion;
minion.name="Marty McMinion";
minion.salary=7;
boss.print_paycheck();
minion.print_paycheck();
return boss.get_paycheck()+minion.get_paycheck(); // total payroll
}
(Try this in NetRun now!)
We can also keep employees in an array, and just loop over the employees:
int foo(void) {
const int n=2;// employee count
person arr[n];
arr[0].name="James Pocketsworth IV";
arr[0].salary=137;
arr[1].name="Marty McMinion";
arr[1].salary=7;
int total_payroll=0;
for (int e=0;e<n;e++) {
arr[e].print_paycheck();
total_payroll += arr[e].get_paycheck();
}
return total_payroll;
}
(Try this in NetRun now!)
The nice part about this setup is "information hiding". Let's say
we need to modify this program to compute federal witholding, which is
a function of the employee's annual salary. We can pretty easily
add a method to "person" that computes their yearly salary:
// Return the amount this person makes in a (typical) year.
int get_yearly(void) {
return get_paycheck()*52; // 52 weeks/year
}
And then based on their yearly salary we can then compute their tax
rate (of course, it's *way* more complex than this, mostly because it's
designed to avoid discontinuities):
// Return this person's overall tax rate
float get_taxrate(void) {
if (get_yearly()<30000) return 0.10;
else /* income over threshold */ return 0.28;
}
Finally, we reduce each person's paycheck by the amount of tax witholding:
// Format and print this person's weekly paycheck
void print_paycheck(void) {
cout<<"Pay to the order of: "<<name<<"\n";
cout<<" the sum of "<<get_paycheck()*(1.0-get_taxrate())<<" dollars\n";
}
(Try this in NetRun now!)
OK, that wasn't too bad. But how do we change the outer function
foo? Well, we don't! The beautiful part about information
hiding is that everything we just did happened *inside* person, and the
code outside of person doesn't need to change at all.
For a big program, this is a huge advantage--you can work on one part of the program without messing up the other parts.
Programming Style with Classes
There are several popular conventions for naming classes:
- "person": C++ standard style is lowercase, with underscores_between_words.
- "Person": Java standard style is initial capitals, with more InitialCapitalsBetweenWords.
- "CPerson": Microsoft Foundation Classes style; the "C" means "class".
One common convention is "set and get methods". For each data
member X of your class, you add a "set_X" method to change the value,
and a "get_X" method to access the value. This is often a little
safer than directly accessing X, because the set and get methods can do
sanity checking (say, check for negative salary), while there's no way
to stop a direct access from inserting garbage values. The
set and get methods provide a good place to insert debugging
breakpoints (say, tell me when somebody reads this value). You
can enforce the use of your get and set methods by marking the direct
member X "private"; the compiler will then disallow direct accesses.
Mathematically inclined programmers like to find the "class invariant"
or "predicate", usually some mathematical relation that the members of
the class must satisfy. You can then add code to check these
invariants or predicates, and indicate an error if they're violated.
Here's a more complete example with slightly better style, where we
keep track of tax witholding in a separate "company" object. Note
that both the "person" and "company" class methods call the ordinary
function "print_check", which is perfectly fine--you can mix the
object-oriented style with regular functions.
void print_check(string to,float amount) {
cout<<"Pay to the order of: "<<to<<"\n";
cout<<" the sum of "<<amount<<" dollars\n";
}
class company {
private: // note: making these private prevents other code from accessing them.
float ytd_payroll; // money out the door to employees
float ytd_withholding; // money to send off the IRS
public:
void init(void) { // set up company
ytd_payroll=0.0; ytd_withholding=0.0;
}
void add_paycheck(float payroll,float withholding) {
// note: can sanity-check payroll and withholding here...
ytd_payroll += payroll;
ytd_withholding += withholding;
}
void print_paycheck(void) {
print_check("IRS",ytd_withholding);
}
// Compute the total money leaving out the door
float total_expenses(void) {
return ytd_payroll+ytd_withholding;
}
};
class person {
public:
string name; // the person's full legal name
int salary; // how much the person makes per hour
// Return gross yearly salary
int yearly_salary(void) {
return salary*40*52; /* * hours/week * weeks/year */
}
// Return fraction of your total income that goes to IRS
float get_tax_rate(void) {
if (yearly_salary()<20000) return 0.10; // 10%
else /* salary over threshold */ return 0.28;
}
// Print the weekly paycheck
void print_paycheck(company &c) {
float gross=salary*40;
float net=gross*(1.0-get_tax_rate());
float withholding=gross-net;
print_check(name,net);
c.add_paycheck(net,withholding);
}
};
int foo(void) {
const int n=2;
person arr[n]; // employee list
arr[0].name="Biggles Pocketsworth IV";
arr[0].salary=137;
arr[1].name="Marty McMinion";
arr[1].salary=4;
company c; c.init();
for (int e=0;e<n;e++) arr[e].print_paycheck(c);
c.print_paycheck(); // ... to IRS
return c.total_expenses();
}
(Try this in NetRun now!)