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:
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!)