Chapter 6: Classes
Chapter Goals
- To be able to implement your own classes
- To master the separation of interface and
implementation
- To understand the concept of encapsulation
- To design and implement accessor and mutator
member functions
- To understand object construction
- To learn how to distribute a program over
multiple source files
Discovering Classes
- If you find yourself defining a number of related
variables that all refer to the same concept, define a class that
abstracts the concept and contains these variables as data fields.
-
Example:
Suppose you are reading information
about
computers:
- Model Name
- Price
- Score between 0 and 100
- Sample data:
ACMA P600
995
77
Alaris Nx868
798
57
AMAX Powerstation 600
999
75
-
We are trying to find the product for which the
value (score/price) is highest. Sample code excerpt:
if (next_score / next_price > best_score / best_price)
{ best_name = next_name;
best_score = next_score;
best_price = next_price;
}
- Each of these sets of variables describes a product
(a best product, and the next product read in).
-
We need to develop a Product
class.
Interface
- To define a class, we first need to specify an interface.
- The interface consists of all member functions
(methods) we want to apply to objects of that type.
- The interface is specified in the class
definition.
-
Example:
Product class member functions:
- Make a new product.
- Read in a product object.
- Compare two products and find out which one
is better.
- Print a product.
- Product class interface (a part of the
definition):
class Product {
public:
Product();
void read();
bool is_better_than(Product b) const;
void print() const;
private:
implementation details
};
- The first member function is a constructor:
the function that is used to initialize
new objects.
- This constructor has no parameters so it is
called a default constructor.
- Default constructors are automatically used when
you construct an object without parameters.
Product best; /* default construction */
- As a general rule, every class should have a
default constructor.
- The read() member function is a mutator
function - an operation that modifies
the object.
- The last two member functions are accessor
functions - functions that query
the object for some information
without changing it.
- The keyword const indicates that the
implicit parameter object is not
modified by the member function.
Syntax 6.1: Class
Definitions
class Class_name { public: constructor declarations member function declarations private: data fields };
Example:
|
class Point { public: Point (double xval, double yval); void move(double dx, double dy); double get_x() const; double get_y() const; private: double x; double y; };
|
Purpose:
|
Define the interface and data
fields of
a class. |
|
- To use objects in our programs, we only need to
use the interface.
- The interface definition of the class just
declare the constructors
and member functions.
Encapsulation
-
Classes are broken into public and private
sections. To
use objects in our program we only need to use the interface. To enable
any function to access the
interface functions, they are placed in the public
section.
- The data items are defined in the private
sections of the class definition.
- Example:
Every product object has a name field, a
price field and a score field.
class Product {
public:
Product();
void read();
bool is_better_than(Product b) const;
void print() const;
private:
string name;
double price;
int score;
};
- Only constructors and member functions of the
class can access private data.
- Hiding data is called encapsulation.
Encapsulation forces indirect access to
the data, guaranteeing that it cannot accidentally be put in an
incorrect state.
- Example:
class Time {
public:
Time();
Time(int h, int m, int s);
void add_seconds(long s);
long seconds_from(Time t) const;
int get_seconds() const;
int get_minutes() const;
int get_hours() const;
private:
int hours; /* conjecture */
int minutes; /* conjecture */
int seconds; /* conjecture */
};
- Suppose the programmer could access data fields
of the Time class
directly. This allows to create invalid time:
Time liftoff(19, 30, 0);
/* liftoff is delayed by six hours */
/* won't compile, but suppose it did */
liftoff.hours = liftoff.hours + 6;
-
Does this mean liftoff is at 25:30:00?
- The constructors and add_seconds function guarantee
that all times are always valid, thanks to the encapsulation mechanism.
Member Functions
- Each member function advertised (declared) in the
class
interface must be implemented separately.
- A Class_name:: prefix
makes it clear that we are defining a member function for that class.
void Product::read()
{ cout << "Please enter the model name: ";
getline(cin, name);
cout << "Please enter the price: ";
cin >> price;
cout << "Please enter the score: ";
cin >> score;
string remainder; /* read the remainder of the line */
getline(cin, remainder);
}
- It is perfectly legal to have member functions in
other classes as well; it is important to specify exactly which
function goes with which class.
- A member function is called object.function_name().
next.read();
-
When we defining an accessor function, we must
supply the keyword const
following the closing parenthesis of the parameter list.
- When the keyword const is used in the
declaration, it must also be placed in the member function definition.
void Product::print() const
{ cout << name
<< " Price: " << price
<< " Score: " << score << "\n";
}
- The keyword const indicates that the
member function will not
modify the object that called it.
-
When you refer to a data field in a member
function, such as name or price, it denotes the data field of the object for which the
member
function was called:
best.print();
The Product::print() function prints best.name,
best.score and best.price.
- The object to which a member function is applied
is called the implicit parameter.
- A parameter explicitly mentions in the function
definition is called an explicit parameter.
bool Product::is_better_than(Product b) const
{ if (b.price == 0) return false;
if (price == 0) return true;
return score / price > b.score / b.price;
}
if (next.is_better_than(best)) ...
Syntax 6.2: Member Function
Definition
return_type Class_name::function_name(parameter1, ..., parametern) [const]opt { statements }
Example:
|
void Point::move(double dx, double dy) { x = x + dx; y = y + dy; } double Point::get_x() const { return x; }
|
Purpose:
|
Supply the implementation of a member
function. |
|
Default Constructors
- The purpose of a constructor is to initialize all
data fields of an object.
- A constructor always has the same name as the
class.
Product::Product()
{ price = 0;
score = 0;
}
-
Most default constructors initialize the data
fields to zero, but not always.
- The Time class initializes to
current time.
- Non-numeric data (e.g. string data) may have
another value.
Constructors with Parameters
- It's possible to create a class with more than
one constructor.
Employee Fred; /* default construction */
Employee Harry("Harry Hacker", 40000); /* alternate construction */
- All constructors have the same name as the class,
but have different parameter lists.
class Employee {
public:
Employee();
Employee(string employee_name, double initial_salary);
void set_salary(double new_salary);
string get_name() const;
double get_salary() const;
private:
string name;
double salary;
};
- Whenever two functions have the same name but are
distinguished by their parameter types, the function name is overloaded.
- The second constructor sets the data fields of
the new object based on the parameters.
Employee::Employee(string employee_name, double initial_salary)
{ name = employee_name;
salary = initial_salary;
}
Syntax 6.3: Constructor
Definition
Class_name::Class_name(parameter1, ..., parametern) { statements }
Example:
|
Point::Point(double xval, double yval) { x = xval; y = yval; }
|
Purpose:
|
Supply the implementation of a
constructor. |
|
- Sometimes a constructor gets more complex because
one of the data fields is itself an
object of another class with its
own constructor.
-
Example:
Suppose we modify the Employee
class so that each Employee has scheduled work hours.
class Employee {
public:
Employee(string employee_name, double initial_salary,
int arrive_hour, int leave_hour);
. . .
private:
string name;
double salary;
Time arrive;
Time leave;
};
The constructor must
initialize the time fields
using the Time() constructor.
Employee::Employee(string employee_name, double initial_salary,
int arrive_hour, int leave_hour)
{
name = employee_name;
salary = initial_salary;
arrive = Time(arrive_hour, 0, 0);
leave = Time(leave_hour, 0, 0);
}
Accessing Data Fields
- Only member
functions of a class are allowed to
access the private data
fields of objects of that class.
void raise_salary(Employee& e, double percent)
{ e.salary = e.salary * (1 + percent / 100 ); /* Error */
}
- All other
functions must go through the public
interface of the class. Data
fields must be accessed by accessor and
mutator functions.
void raise_salary(Employee& e, double percent)
{ e.set_salary(e.get_salary() * (1 + percent / 100 )); /* OK */
}
- Not every data member needs accessor functions
(the Product class did not have a get_score()
function).
-
Not every get_ function needs a
matching set_ function (the Time class can get_minutes()
but not set_minutes()).
- Remember that implementation is supposed to be
hidden - just because a class has member functions named get_
or set_ does not necessarily explain how the class is
designed.
class Time {
public:
Time();
Time(int h, int m, int s);
void add_seconds(long s);
long seconds_from(Time t) const;
int get_seconds() const;
int get_minutes() const;
int get_hours() const;
private:
long time_in_secs;
};
-
The data representation is an internal
implementation detail of the class that is invisible to the class user.
Time::Time(int hour, int min, int sec)
{ time_in_secs = 60 * 60 * hour + 60 * min + sec;
}
int Time::get_minutes() const
{ return (time_in_secs / 60) % 60;
}
int Time::seconds_from(Time t) const
{ return time_in_secs - t.time_in_secs;
}
Comparing Member Functions with Nonmember Functions
- Consider the nonmember function
void raise_salary(Employee& e, double percent)
{ double new_salary = e.get_salary() * (1 + percent / 100);
e.set_salary(new_salary);
}
versus the member function
void Employee::raise_salary(double percent)
{ salary = salary * (1 + percent / 100);
}
- A nonmember function is called with two explicit
parameters.
raise_salary(harry, 10);
- A member function is called using the dot
notation, with one explicit parameter and one implicit parameter.
harry.raise_salary(10);
- A member function can invoke another member
function on the implicit parameter without using the dot notation.
void Employee::print() const
{ cout << "Name: " << get_name()
<< "Salary: " << get_salary()
<< "\n";
}
- employee.cpp
- Member functions automatically (by default) pass
the implicit parameter by reference.
- Nonmember functions automatically (by default)
pass their parameters by value.
|
Explicit Parameter |
Implicit Parameter |
Value Parameter
(not changed) |
Default
Example:
void print(Employee);
print(harry);
|
Use const
Example:
void Employee::print()const;
harry.print();
|
Reference Parameter
(can be changed) |
Use &
Example:
void raiseSalary(Employee& e, double p);
raiseSalary(harry, 10);
|
Default
Example:
void Employee::raiseSalary(double p);
harry.raiseSalary(10);
|
Separate Compilation
-
When your code gets large or you work in a team,
you will want to split your code into separate source files.
- Saves time: instead of recompiling the entire
program, only recompile files that have been changed.
- Group work: separate programmers work on
separate files.
-
The header file (e.g.
product.h) contains:
- definitions of constants
- definitions of classes
- declarations of nonmember functions
- declarations of global variables
-
The source file (e.g. product.cpp)
contains
- definitions of member functions
- definitions of nonmember functions
- definitions of global variables
Separate Compilation (product.h)
Separate Compilation (product.cpp)