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
- Code to a concept, rather than defining related variables that refer
to the same concept
- Define a class that abstracts the concept and contains these
variables as data fields
- E.g., suppose you are parsing information about various computers.
Each record contains (to find the best value):
- Model Name
- Price
- Score between 0 and 100
Discovering Classes — bestval.cpp
Discovering Classes
- Consider the 2 sets of variables: best_name,
best_price, best_score, and next_name,
next_price, next_score
- Each describes a product
- Common concept
- We need to develop a Product class
Common Error
Mixing >> and getline input
- getline reads until the next newline, which it extracts
- Stores all characters, but for the newline
- >> :
- Skips leading whitespace
- Reads until next whitespace (or non-number)
- Does not extract the trailing character, even
the newline
- After using >> , but before a getline call,
insert:
string remainder; // Read remainder of line
getline(cin, remainder);
// Now you are ready to call getline again
to consume the leading newline left by >>
Interfaces
- To define a class, first specify its public interface
- The public interface consists of all member functions we want
to apply to objects of that type
- We are describing the object's behavior
- 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
Interfaces
Product class
- The interface is specified in the class definition
- Product class header:
class Product
{
public:
Product();
void read();
bool is_better_than(Product b) const;
void print() const;
private:
implementation details
};
Interfaces
Parts
The public interface can be divided logically into 3 parts:
- Constructors
- Initialize new objects
- Same name as class
- If no parameters, called the default constructor
- Mutators
- Modify an object (read())
- Accessors
- Simply query an object, without modifying it
(is_better_than() and print())
- Tagged as a const method
Interfaces
Public
- These methods only tell what an object can do, not
how it does it
- This is all that the rest of the program needs
- Variables (and helper methods) are hidden in the private
section
Revised bestval example — product1.cpp
Is this easier to read, to understand?
Syntax
: Class Definition
class ClassName
{
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.
Common Error
Forgetting a Semicolon
- No semicolon after statement blocks
- Always after a type definition, such as a
class:
class Product
{
public:
...
private:
...
}; // Don't forget semicolon
- For compatibility reasons, compiler may report error several lines
later
Encapsulation
Each Product object must store data (attributes)
class Product
{
public:
Product();
void read();
bool is_better_than(Product b) const;
void print() const;
private:
string name;
double price;
int score;
};
Encapsulation
- Member data should be private
- Only constructors and member functions of that class may
access/modify private data
- These fields can not be accessed directly:
int main()
{
...
cout << best.name; // Error – use print() instead
...
}
- All data access must occur through the public interface
Encapsulation
- An object's data is effectively hidden from the programmer
- Implementation details are of no concern to the user
- User should only be concerned with what it does, not
how it does it
Encapsulation
Benefits
- An object is responsible for its own data
- Implementation may change without affecting client code (important
with larger programs)
- E.g., a system is hosted on a new platform
- Guarantees that object remains in a valid state
Encapsulation
- Consider the Time class:
class Time
{
public:
Time();
Time(int hrs, int min, int sec);
void add_seconds(long s);
int get_seconds() const;
int get_minutes() const;
int get_hours() const;
long seconds_from() const;
private:
... // Hidden data representation
};
Encapsulation
- If the data fields were accessible, we might see this:
Time liftoff(19, 30, 0);
...
// Looks like the liftoff is getting delayed by another six hours
liftoff.hours = liftoff.hours + 6;
- But, 25:30:00 is not a valid time of day
Member Functions
- Each member function advertised in the class interface must
be implemented separately
- So, Product's read method would be defined this way,
outside of the class definition:
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);
}
Member Functions
- The Product:: prefix defines a
member function for that class
- May be another class with a read() method
- To call this method, given the object declaration:
Product next;
next.read();
Member Functions
- If a member function does not modify any of its
own member data (like an accessor), then it should be
tagged as const
- Part of the interface. A promise not to modify the object
- This is done for both the declaration and definition:
void Product::print() const
{
cout << name
<< " Price: " << price
<< " Score: " << score << "\n";
}
Member Functions
Implicit Parameters
- A data field in a member function denotes the
data field of the object for which the member function was called
best.print();
- 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 (implicit_parameter.price == 0) return true;
if (b.price == 0) return false;
return implicit_parameter.score / implicit_parameter.price
> b.score / b.price;
}
Syntax
: Member Function Definition
return_type ClassName::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.
Common Error
const Correctness
- Apply const for each accessor, as you write them
- Failure to follow this rule yields classes that are not usable
- If an object is const, then you may only call its
const methods
Default Constructors
- Constructors initialize all data fields of an object
- A constructor always has the same name as the class
- Each class needs at least 1 constructor
- The default constructor is the constructor that doesn't
take any arguments
- The default constructor generally set fields to a
default value (if one makes sense)
- It is supplied, if you do not define it explicitly
- All primitive data members should be initialized
Default Constructors (examples)
Product::Product()
{
price = 1;
score = 0;
}
- price is set to 1 to avoid division by 0
- score are set to 0
- name is automatically set to the empty string
(string's default constructor)
- The Time class initializes to current time
Default Constructors — product2.cpp
Constructors with Parameters
- Classes may have multiple constructors:
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;
};
- All constructors have the same name as the class, but have
different parameter lists
Constructors with Parameters
- When constructing, compiler chooses the constructor that matches the
arguments supplied:
Employee joe; // Uses default construction
Employee Lisa('Lisa Lee', 105000); // Uses Employee(string, double) construction
- Implementation of the 2nd constructor is straightforward:
Employee::Employee(string employee_name, double initial_salary)
{
name = employee_name;
salary = initial_salary;
}
- Could be much more complex
Constructors with Parameters
- Objects might have data fields that are, in turn, other objects
- E.g., add scheduled work hours to Employee:
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;
};
Constructors with Parameters
- The constructor must initialize the time fields using Time's
constructors
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);
}
- If not initialized at construction, member objects created using
their default constructor (if provided)
Syntax
: Constructor Definition
ClassName::ClassName(parameter1, parameter2, ..., parametern)
{
statements
}
Example:
Point::Point(double xval, double yval)
{
x = xval; y = yval;
}
Purpose:
Supply the implementation of a constructor.
Common Error
Forgetting to Initialize All Fields in a Constructor
- Ensure each constructor provides useful initializations to
all fields
- Consider this new constructor for Employee:
Employee::Employee(string n)
{
name = n;
// Oops – salary not initialized
}
- A call to get_salary() will return a random salary
- Set salary to 0
- Document it
Common Error
Trying to Reset an Object by Calling a Constructor
- Constructors create objects
- Can not be called on an existing object:
Time homework_due(19. 0, 0);
...
homework_due.Time(); // Error
- Simple remedy: overwrite existing object with a new one:
homework_due = Time() ; // OK
Advanced Topic
Calling Constructors from Constructors
- By the opening brace of the constructor, all members are initialized
- This is inefficient:
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);
}
Advanced Topic
(cont)
- arrive and leave already built, using default
constructor
- Use initialiser list:
Employee::Employee(string employee_name, double initial_salary,
int arrive_hour, int leave_hour)
: arrive(arrive_hour, 0, 0),
leave(leave_hour, 0, 0)
{
name = employee_name;
salary = initial_salary;
}
- Only correct way to initialize objects with no
default constructor
- Works for first-class attributes, too
Syntax
: Constructor with Field Initializer List
ClassName::ClassName(parameters)
: field1(expressions), ..., fieldn(expressions),
{
statements
}
Example:
Point::Point(double xval, double yval)
: x(xval), y(yval)
{
}
Purpose:
Supply the implementation of a constructor, initializing data fields
before the body of the constructor.
Advanced Topic
Overloading
- 2 or more functions having the same name
- Must have different parameter lists
- E.g., given these 2 definitions:
void print(Employee e) ...
void print(Time t) ...
- When the compiler sees
print(x);
it checks the type of x, calls the appropriate function
Advanced Topic
(cont)
Overloading Operators
- Most operators can be overloaded
- One of the operands needs to be of some class type
- E.g., replace Product::is_better_than() with >:
bool Product::operator>(Product b) const
{
if (price == 0) return true;
if (b.price == 0) return false;
return score / price > b.score / b.price;
}
- So
if(next.is_better_than(best)) ...
becomes
if(next > best) ...
Accessing Data Fields
- Private data only accessible to members of that same
class
- All other functions (and methods of other classes) must use the
public interface
- E.g., raise_salary() cannot read nor set salary
directly:
void raise_salary(Employee& e, double percent)
{
e.salary = e.salary * (1 + percent / 100); // Error
}
- Must use Employee interface:
void raise_salary(Employee& e, double percent)
{
double new_salary = e.get_salary()
* (1 + percent / 100);
e.set_salary(new_salary);
}
Accessing Data Fields
- Employee setters and getters are simple:
double Employee::get_salary() const
{
return salary;
}
void Employee::set_salary(double new_salary)
{
salary = new_salary;
}
- Do not automatically write setters/getters for all
fields
- Avoid revealing implementation details; stay flexible
- Consider Product; we didn't use get_score or
set_price
- Don't automatically supply a setter for each getter
- Time has get_minutes(), but not
set_minutes()
Comparing Member and 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);
}
now as a method of Employee:
class Employee
{
public:
void raise_salary(double_percent);
...
};
void Employee::raise_salary(double percent)
{
salary = salary * (1 + percent / 100);
}
Comparing Member and Nonmember Functions
- As a nonmember function, it is called with two explicit parameters:
raise_salary(harry, 7);
- As a member function it is called using the dot notation:
harry.raise_salary(7);
- Which is better?
- Depends on ownership
- If you are the class author, write useful functions as members
- If you are a client, don't extend the class. Write functions
Comparing Member and Nonmember Functions
- A member function can invoke another member function on
its implicit parameter without using the dot notation
- Adding a print() method to Employee:
class Employee
{
public:
void print() const;
...
};
void Employee::print() const
{
cout << "Name: " << get_name()
<< "Salary: " << get_salary();
<< "\n"
}
Comparing Member and Nonmember Functions
- All functions pass explicit arguments by value (by default)
- Member functions pass the implicit parameter by reference
|
Value Parameter
(not changed) |
Reference Parameter
(can be changed) |
| Explicit Parameter |
No modifer
void print(Employee) |
Use & modifer
void raise_salary(Employee & e, double
p)
|
| Implicit Parameter |
Use const modifer
void Employee::print()const |
No modifer
void Employee::raise_salary(double p)
|
Quality Tip
File Layout
Keep source files neat and organized:
- Included header files
- Constants
- Classes
- Global variables (if any)
- Functions
Separate Compilation
- For large programs, or multiple programmers,
split your code into separate source files
- Only (re)compile the small bit that was changed
- Multiple programmers working simultaneously, in separate files
- Header file (e.g. product.h) contains:
- definitions of classes
- declarations of constants
- 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 — prodtest/product.h
Separate Compilation — prodtest/product.h
- Special code is needed in header files,
to prevent multiple inclusion
#ifndef PRODUCT_H
#define PRODUCT_H
...
#endif
- Otherwise, the compiler will complain about 2 classes with the same
name
Separate Compilation — prodtest/product.cpp
Separate Compilation — prodtest/product.cpp
- The source (implementation) file contains definitions of member
functions (including constructors), and global variables
- The source file includes its own header file
- The compiler needs to see the definition of Product to
compile its member functions
#include "Product.h"
- Comments are in the header (interface) file
Separate Compilation — prodtest/prodtest.cpp
- product.cpp does not contain a
main function
- Other programs might use this class
- Each will supply its own main()
- A simple test program (driver) follows:
Separate Compilation — prodtest.cpp
Separate Compilation — prodtest/prodtest.cpp
Compiling
(Note: details depend on your compiler. We demonstrate Gnu's
compiler)
- Both source files are compiled:
g++ -c product.cpp
g++ -c prodtest.cpp
yielding object files
- Object files are then linked:
g++ -o prodtest product.o prodtest.o
producing an executable called prodtest
- Never explicitly compile header files!
Separate Compilation
- Shared constants are placed in the header file
- To share nonmember functions:
- Place prototype (declaration) in the header file
- Place the function definition into a source file
- To share global variables:
- Declare them in a header file as extern
extern GraphicWindow cwin;
- Define them in a source file:
GraphicWindow cwin;