CS320 Lecture: Implementing Classes in C++; 9/11/96
Encapsulation; Public and Private Members; Revised 9/22/97
Designing a Class Interface; friends.
Materials: Transparencies: Side by side class description and interface
for class Session
bank.h (abbreviated)
atm.h
atmparts.h (page 1 only)
transaction.h (class Transaction only)
status.h
session.cc
main.cc
transaction.cc (implementation of base class only)
Design suggestions from text
MacGregor/Sykes class design guidelines
Handout: Instance vs class variables/methods
I. Preliminary Comments
- ----------- --------
A. Stroustrup philosophy in design of C++:
"I find Kierkegaard's almost fanatical concern for the individual and
keen psychological insights much more appealing than the grandiose
schemes and concern for humanity in the abstract of Hegel or Marx.
Respect for groups that doesn't include respect for individuals of those
groups isn't respect at all. Many C++ design decisions have their
roots in my dislike for forcing people to do things in some particular
way. In history, some of the worst disasters have been caused by
idealists trying to force people into 'doing what is good for them'.
Such idealism not only leads to suffering among its innocent victims,
but also to delusion and corruption of the idealists applying the force.
I also find idealists prone to ignore experience and experiment
that inconveniently clashes with dogma or theory. Where ideals
clash and even sometimes when pundits seem to agree, I prefer
to provide support that gives the programmer a choice.
My preferences in literature have reinforced this unwillingness
to make a decision based on theory and logic alone. In this
sense, C++ owes as much to novelists and essayists such as
Martin A. Hansen, Albert Camus, and George Orwell, who never saw
a computer, as it does to computer scientists such as David
Gries, Don Knuth, and Roger Needham. Often, when I was tempted
to outlaw a feature I personally disliked, I refrained from
doing so because I did not think I had a right to force my
views on others ... A high degree of tolerance and acceptance
that different people do think in different ways and strongly
prefer to do things differently is to me far preferable.
...
In particular, I do not try to enforce a single style of design
through a narrowly defined programming language. People's ways
of thinking and working are so diverse that an attempt to force
a single style would no more harm than good. Thus, C++ is
deliberately designed to support a variety of styles rather
than a would-be 'one true way'."
Stroustrup-Design and Evolution of C++ (Addison Wesley, 1994) pp. 23-24.
B. My approach here
For pedagogical reasons, I will not try to cover all the alternative
ways of doing things in C++ - but will instead focus on one
approach to each task which integrates seamlessly with the design
methods we have already discussed. Sometimes, I will also mention
one or more alternatives as well. I will leave it to you to explore
other alternatives on your own as you become more familiar with
the language. However, for class use, I would encourage you to
use the style taught in class.
C. Turning a class description form into a C++ implementation is done in
two parts - a class declaration (normally in a .h file) plus a
class implementation (normally in corresponding .cc file).
1. The class declaration includes all the information in the class
description form.
2. The class implementation contains an implementation of each method
declared for the class, and perhaps other things as well.
II. Declaring a Class
-- --------- - -----
A. Basic form of a class declaration:
class NAME [ : public BASECLASS ]
{
public:
DECLARATIONS (PROTOTYPES) FOR CONSTRUCTOR, MUTATOR, ACCESSOR,
AND DESTRUCTOR METHODS - NORMALLY IN THAT ORDER
private:
DECLARATION OF PRIVATE METHODS, IF ANY
DECLARATIONS OF FIELDS
};
Note that these sections correspond very closely to the class
description form - except that there is nothing directly corresponding
to the "states" portion of the form. (The state information has to
actually be encoded by the value of some field.)
B. Example: SIDE BY SIDE TRANSPARENCY OF SESSION CLASS DESCRIPTION
FORM AND SESSION.H.
Note:
1. public: and private: sections
a. public usable by all, private only accessible within
implementation of methods of the class (compiler protected).
Example:
class foo
{
public:
int bar();
private:
int _x;
};
int foo:bar()
{ return _x ++; } // legal
main()
{
foo f;
...
cout << f.bar(); // legal
cout << f._x; // illegal - detected by compiler
}
b. Standard practice: methods are public, fields are private
c. Can have private methods (used to implement other public methods)
d. Can have public fields (therefore accessible to all) - but this
is bad practice (violates encapsulation).
2. Methods are declared by giving a full prototype (as in operation
description forms) followed by a ;
RETURN-TYPE NAME([TYPE ARG [, TYPE ARG]...]);
Methods always have parentheses after name - even if empty.
3. Our book suggests declaring methods in the order
constructor(s)
mutators
accessors
destructor
a. Constructor method(s) are distinguished by name of method being
same as name of class.
- When an instance of a class is created, a constructor is
automatically called.
- Before main program for global variables
- Upon entry into function or block for local variables
- Within expression for temporaries
- When creating an object dynamically using new
- "Miranda rule" for constructors
b. Mutators/accessors should be named appropriately
- Note that accessor prototypes (cardNumber(), PIN()) are
followed by const. This indicates that these methods do
not change the object's fields (i.e. they could be applied to
an object that is declared as a constant).
- Mutators are allowed to change the object's fields.
(In this case, the first two alter the value of _PIN; the
doFailedTransactionExtension method turns out not to alter
the state of the object, but this was not clear at the
time it was first designed.)
c. Not shown here are destructors
- Name is ~CLASSNAME (e.g. ~Session())
- Automatically called when an instance of the class is destroyed
- After termination of main for global variables
- Upon exit from function or block for local variables
- Within an expression for temporaries (when no longer needed)
- When delete is used to destroy an object created using new
However: an object created using new that is never explicitly
destroyed by delete is not destructed!
- "Miranda rule" for destructors - but only in certain cases where
a destructor is needed because a component of the object has a
destructor that needs to be called. (Unlike construction,
destruction is not mandatory).
4. Fields are declared by giving a type then name (or comma-separated
names) followed by a ; - no parentheses
5. Anonymous enum type created for field _state (only variable that
will ever hold a value of this type).
6. "Forward" declaration of class Transaction:
a. Transaction is declared in transaction.h. It turns out that
Transaction objects contain a reference to the Session object
that created them, and Session objects contain a pointer to
the currently active Transaction object. So no matter which
class is declared first, there will be a problem!
b. Solved by use of an INCOMPLETE CLASS DECLARATION of the form
class CLASSNAME ;
- Declares that the class exists, but provides no information
about it.
- Must be followed by a full declaration later.
- Variables of an incomplete class type cannot be declared until
after the declaration has completed, but pointers and
references can be declared. (The issue is that the compiler
does not know how much memory to allocate for an object until
its class has been declared; but size of a pointer/reference
is independent of size of object it points to.)
7. Field _currentTransaction must be a pointer, not a reference,
because value will change.
8. Closing } is followed by a ;
C. The public section of the declaration is the class interface. The
private section really shouldn't need to be in the declaration, but
is there as a concession to practicality so that the compiler can
figure out the size of objects of the class and reserve space for
variables of class type, as well as to support inline methods
(to be discussed later).
D. Further examples: walk through additional .h files
1. bank.h - TRANSPARENCY (ABBREVIATED) - Note:
a. class declaration can also declare other data types - e.g.
enum AccountType
- will be referred to by these names inside class declaration
and methods
- must be referred to as Bank::AccountType elsewhere
b. use of reference parameters (Money &) for items that will be
returned from methods
c. Two uses of const in declarations of accountName() and
rejectionDescription():
- const after () - these are accessors, so do not alter state
of Bank object
- const before return type - these return a pointer to a
character string constant. The caller is NOT allowed to
alter the contents of the string returned - e.g. the
following would be illegal:
const char * foo =
theBank.rejectionDescription(BANK::NO_SUCH_ACCOUNT);
foo[0] = 'X'; // Illegal - rejected by compiler
Likewise, the following would also be illegal:
char * foo =
theBank.rejectionDescription(BANK::NO_SUCH_ACCOUNT);
// Compiler will not allow a const char * pointer to be
// stored into a non-const pointer
2. atm.h - TRANSPARENCY - Note:
a. Parameters to constructor - must be supplied when a variable of
the class is created. The following appears in main.cc:
ATM theATM(42, "GORDON COLLEGE", theBank);
- location parameter is declared const char * as a promise
that string passed to it will not be changed.
b. Accessor methods are const. (The operations may alter the
state of the component part that performs that operation, but
not of the ATM itself, which simply forwards the message to
the right component)
c. String parameters to getMenuChoice(), issueReceipt(),
and reportTransactionFailure() are const char * - method will not
alter them.
- Note that const on parameters is an issue only with pointers
and references
- Reference parameters in issueReceipt are NOT const because
they will be altered, since they are used for return values.
d. Anonymous enumeration type for variable _state
e. Fields _cardReader etc. that contain references to component
parts.
- These use keyword class to create an incomplete type
declaration - the detailed declarations of the parts is not
relevant to clients of ATM.
- We can use a reference here, rather than a pointer,
because parts will be created when ATM is created and will not
change. When this is the case, use of a reference is
preferable.
3. atmparts.h - TRANSPARENCY (PAGE 1 ONLY) - Note:
a. Several related classes included in one .h file
b. Close correspondence between these methods and some declared
in atm.h - the ATM object will simply forward the message to
the right component. (But many of these are non-const
because the state of the ATM component is altered.)
c. The only class that depends on this interface is ATM.
4. transaction.h - TRANSPARENCY (class Transaction only)
a. The method chooseTransaction() is declared static.
i. Recall that in OO systems most computation is done by sending
MESSAGES to OBJECTS, which are handled by appropriate METHODS.
As a result, most of the time when a method is called it is
associated with a specific object. (In fact, each instance
method has an implicit first parameter which is a pointer to
the object on which it was invoked.)
ii. There are times, however, when a method cannot be associated
with a specific object. This is the case here - the whole
purpose of chooseTransaction() is to construct a new object
of a specific subclass.
iii. A method like this is called a CLASS METHOD, because the
message is really being sent to the class, rather than to
a specific object.
iv. In C++, class methods are declared static in the interface.
b. Names of types declared as part of other classes must
be preceeded by a scope specifier - e.g.
Status::Code
TRANSPARENCY: status.h (note no .cc file will be needed)
c. Several methods are declared virtual. This supports polymorphism,
and will be discussed later.
d. Several instance variables are declared protected. This relates
to subclasses and inheritance, and will be discussed later.
e. The field _lastSerialNumberAssigned is declared static.
i. Ordinarily, each object of a class has its own copy of each
field - e.g. each Transaction has its own session, serialNumber,
and resulting balances.
ii. Sometimes, though, we have a need for a field which exists in
a single copy shared by ALL instances of the class. Such a
field is called a CLASS VARIABLE.
iii. This is the case here. The constuctor for Transaction will
assign a serial number to each object as it is constructed.
To do this, we need to keep track of what serial numbers have
already been used. That is what _lastSerialNumberAssigned is
used for.
iv. In C++, a class variable is declared static in the class
declaration. All objects of the class share one copy of the
variable.
5. HANDOUT - Instance versus class variables and methods
III. Implementing a Class
--- ------------ - -----
A. Basic form of a class implementation
INCLUDE DIRECTIVES FOR CLASS DECLARATION AND OTHER NEEDED HEADERS
DEFINITIONS FOR EACH METHOD AND CLASS VARIABLE
(NORMALLY IN THE ORDER OF DECLARATION)
B. Example: TRANSPARENCY: SESSION.CC
1. Implementation (.cc) file will #include interface (.h) file, plus
any other .h files needed
a. If the class being implemented uses objects of some other class
in its interface, then .h files for those classes must be
#included BEFORE its .h file.
Example: status.h and money.h must be included before bank.h,
because methods declared in bank.h take parameters of
type Status::Code and Money. (Many other examples
could be cited.)
b. If the class uses objects for some other class for its
implementation (but not interface), then .h files for these classes
can be #included before or after its .h file (depending on needs
for their interface)
Example: atm.h - We will see that session uses methods of ATM
(such as getTransactionChoice(), ejectCard(),
and retainCard()) to implement its methods
transaction.h - Session creates transaction objects and uses
their doTransactionUseCase method.
c. Standard libaries are normally #include-d first
Examples: None here.
2. Rest of the file is implementation of each method
Note:
a. Use of scope specifier (Session::) before name of each method
being implemented
i. Method names must be unique within a class, but different
classes can have methods with the same name. The complete
name of a method, therefore, must include the class of
which it is a part unless the class is implicit in the context -
which is not the case here.
ii. Note the order RETURN-TYPE SCOPE::METHODNAME(ARGS)
iii. Note, too, that if the return type or parameter belongs to a
different class, then it may need a scope specifier as well
Example: Status::Code is the return type for the method
Session::doInvalidPINExtension()
b. Methods are a special kind of C++ function, and are implemented
accordingly.
i. Basic syntax:
PROTOTYPE -- must match declaration in interface exactly,
including use of const.
{ BODY }
Examples: doSessionUseCase(), doInvalidPINExtension,
doFailedTransactionExtension(),
cardNumber(), PIN()
ii. Special syntax for constructor
PROTOTYPE
: INITIALIZERS
{ BODY }
- The initializers part is a comma separated list of field names
and values (or base class names and values). Each value can be
any C++ expression, and can use the constructor's parameters
Example: Session() initializers
- With two exceptions, you are not under any obligation to
initialize a field at this point. You can, instead, assign
it a value later.
Example: the _PIN field is not initialized, because its value
is not obtained until the use case is started. It
is assigned a value by the first statement in
doSessionUseCase()
Exceptions:
- If the field is an object (not a simple value) and it does
not have a default constructor (one that takes no
parameters), then you must supply an explicit constructor
call with appropriate parameters.
- A field which is of a reference type must be initialized
here, since it cannot be assigned to
Example: _atm, _bank
- Even if you are not obligated to initialize a given field,
it is good practice to do so if you can give it a reasonable
value.
c. Within an instance method, there is an implicit parameter which is a
pointer to the specific object that received the message that
led to this method being invoked. The fields and methods of
this object are accessible without qualification.
Example: In the initializers list in Session(), we can refer to
the _state and _cardNumber attributes of the Session
object being constructed.
Example: In doSessionUseCase, we use the _atm, _PIN,
_currentTransaction, and _state fields of the Session
object which received the doSessionUseCase message
(This is not the case, however, with a class method - one
declared as static.)
d. Methods of other classes are normally invoked by using the syntax:
VARIABLE.METHOD(ARGS)
or REFERENCE.METHOD(ARGS)
Example: _atm.getPIN() in doSessionUseCase() and several
other similar calls
or POINTER->METHOD(ARGS)
Example: currentTransaction->doTransactionUseCase() in
doSessionUseCase()
Any of these syntaxes have the same effect - the appropriate
method is called, with an implicit parameter pointing to the
object that was named when the method was called.
e. Likewise, methods and fields of other objects of the same class
can be referenced by using the OBJECT. or POINTER -> syntax.
C. There are three important kinds of method that are handled a bit
differently from other methods
1. CONSTRUCTOR METHODS
a. A constructor method is a method whose name is the same as the
name of its class (e.g. Session() is a constructor for objects
of class Session).
i. It can have parameters but most not have a return type specified.
ii. A class can have more than one constructor, provided the
argument types are different.
Example: In the homework, you have been working with a chess
game, which includes the class Game that models a
single game. This class might have two constructors:
Game() - no parameters - starts up a game from scratch
Game(file) - resumes game saved in file
iii. One special type of constructor is the COPY CONSTRUCTOR. It
takes a single parameter of const reference its own type -
e.g. a copy constructor for Session would be declared
Session(const Session &);
The copy constructor is used whenever an object needs to be
copied - e.g. when passing a parameter by value or generating
a run time temporary.
iv. If a class does not have any constructors declared at all, the
compiler generates a default constructor that takes no
parameters and does nothing (except, if necessary, to use
default constructors to initialize fields of class types).
v. If a class does not have a copy constructor, the compiler
generates one that simply copies all the fields.
b. We have already noted that the syntax for implementing a
constructor includes a special provision for initializing the
fields of the object being constructed.
c. A constructor method is usually not called explicitly - e.g. if
session is an object of class Session, then a call of the form
session.Session()
would be unusual (though not illegal). This is because
a constructor is automatically called whenever an object
is created, either via a variable definition or through the use
of the new operator. (If parameters are needed for the
constructor, they must be supplied.)
i. Example of an implicit constructor call when a variable
is defined: TRANSPARENCY - main.cc
definitions of theATM, theBank
NOTE: Global variable objects are constructed before the main
program starts up. Local variable objects are constructed
when execution reaches the line on which the declaration
appears.
(There are no globals in this program)
ii. Example of a constructor call in conjunction with operator new:
TRANSPARENCY - transaction.cc (partial)
four instances of new in chooseTransaction()
2. DESTRUCTOR METHODS
a. A destructor is a method whose name is of the form ~CLASSNAME
(e.g. ~Session()). It can have neither parameters nor return
type (not even void).
b. A DESTRUCTOR METHOD is always called when an object is
destroyed, either via program termination, or via exiting the
block to which it is local, or via the use of the delete
operator.
Example: TRANSPARENCY - session.cc
delete _currentTransaction in doSessionUseCase()
c. If an object being destroyed has fields which have destructors,
their destructors are automatically called (the code to do so
is supplied by the compiler when the destructor is compiled).
d. There are no examples of destructor methods in the ATM system.
Explicit destructors are typically only written when some
special action needs to be taken - e.g. freeing up resources that
the object has allocated.
Example: Consider the following simple class
class Person
{
public:
Person(const char * name);
private:
const char * _name;
};
Person::Person(char * name)
: _name(new char[strlen(name) + 1])
{ strcpy(_name, name); }
The constructor allocates space to hold a private copy of the string
passed as a parameter to it.
This class should have a destructor to free up this space:
- Add Person::~Person() to the declaration
- Implementation:
Person::~Person()
{ delete [] _name; }
3. CLASS METHODS
a. CLASS METHODS are methods whose call is not associated with a
specific object of the class, and thus do not take an object as
an implicit parameter.
b. Such methods are declared static in C++
Example: TRANSPARENCY - transaction.h (Looked at earlier)
c. Class methods are called by the syntax CLASSNAME::METHODNAME(ARGS)
instead of OBJECT.METHODNAME(ARGS)
Example: TRANSPARENCY - session.cc
call to makeTransaction() in doSessionUseCase()
d. Class methods cannot access the individual fields of an object or
ordinary class methods without explicitly naming an object, since
there is no implicit first parameter.
D. When a class declaration includes one or more class variables (fields
declared as static) the implementation file must initialize them.
Example: TRANSPARENCY - transaction.cc
IV. Encapsulation and Design Issues
-- ------------- --- ------ ------
A. The class is the basic mechanism used for encapsulation in C++. Correct
use of public: and private: is an important part of this.
1. As a rule of thumb, the methods of a class should be public and
the fields should be private. However, there can be exceptions to
this.
2. Sometimes, a method is needed only to facilitate the implementation of
other methods - i.e. it is not intended to be called directly from the
outside. In this case, it should be private.
Example: In our ATM system the Bank class serves to model our
interface to the Bank. The physical connection to the bank
may be a communications link, in which case the various
methods will be implemented like this:
Construct a communications packet from the parameters
Send it to the bank over the network
Wait for a response packet from the bank
Convert the response packet into value(s) to return to
the caller.
The class Bank may have private methods like constructPacket(),
sendPacket(), and getResponse()
3. What about making a data member public?
a. This is never necessary - one can make full access to a data
member possible - while preserving the wall of abstraction - by
creating a modifer and accessor method.
Example: A class foo has an integer data member called _number.
If necessary, we can include methods:
void setNumber(int value);
-- body: { _number = value; }
int number();
-- body: { return _number; }
b. However, some would argue that this leads to run-time inefficiency,
since there is the overhead of a procedure call involved instead
of simple access to a field in a structure.
To address, this, C++ allows the use of INLINE methods:
i. The implementation of an inline method is included in the class
HEADER (.h) file - preceeded by the keyword inline.
Example:
inline void foo::setNumber(int value)
{ _number = value; }
inline int foo::number()
{ return _number; }
ii. The compiler substitutes the body of method for a call to the
method, making the result as efficient as accessing the data
member directly.
iii. Actually, as an alternative C++ allows inline methods to have
both declaration and body in the class declaration - e.g.
class foo
{
...
inline void setNumber(int value) { _number = value; }
inline int number() { return _number; }
...
}
However, this tends to clutter the interface and so we will
avoid it.
c. Someone may argue that since giving the inline code for a method
in the header file breaches abstraction, we might as well go all
the way and have public data members, thus avoiding the work of
writing the inline methods. The best response to this is that
using inline methods allows us to change the implementation at a
later date without requiring a lot of code to be rewritten.
Example: It becomes necessary to ensure that the value of _number
never exceeds 100. If an attempt is made to set it to
a higher value, it must be just set to 100. This is
easily done by just changing the one body of the inline
method and then recompiling.
4. We have seen that C++ provides two alternatives for specifying how
a member of a class can be accessed - private, which restricts
access only to operation of the class, and public, which grants
access to everyone. (Actually, there is a third kind of access -
protected - which applies when a class has subclasses.)
a. Sometimes, one wants to grant some other class privileged access,
without granting that access to everyone.
b. Example: Suppose we need to maintain a record of all transactions
performed at our ATM, so we create a TransactionLog class.
class TransactionLog
{
public:
TransactionLog(); // Make an empty log
void add(Transaction * t); // Add a transaction
void print(); // Print the log
...
Suppose further that we decide to do this using a linked list
implementation. To support this, we add a _link field to the
declaration of Transaction. Should this field be private or public?
ASK
Answer: Neither is really right. If it is private, then
TransactionLog::add and Transaction::print cannot access
it. If it is public, everyone can access it - which
violates encapsulation.
c. To handle cases like this, C++ allows one class to declare another
class to be a friend. Methods of a friend class can access
private members of the class just like methods of the class
itself.
Example:
We could add
friend class TransactionLog;
to the declaration of Transaction. This would allow all the
methods of TransactionLog to access the private members of
Transaction.
d. In addition to declaring a class as a friend, it is also
possible to declare a specific global function or a specific
method of another class to be a friend.
Example: In this case, if add and print were the only operations of
TransactionLog needing to access the _link of Transaction,
we could put the following in Transaction:
friend void TransactionLog::add(Transaction *);
friend void TransactionLog::print();
e. The friend feature of C++ has been criticized as providing too
large of a hole in encapsulation - e.g. in this case TransactionLog
would have access to other private members of Transaction besides
the one (_link) it needs. It can usually be avoided by careful
design, though (as Stroustrup points out) there are a few places
where it is the only way to do something in a clean, elegant way.
B. Naming conventions. One important choice in designing a class is the
NAMES that are used for classes, objects, methods, data members, etc.
1. Many conventions are in common use
2. Ones I will use are those recommended in our book - actually based on
Smalltalk conventions. (All examples from ATM simulation code.)
a. Class names begin with uppercase - then lower case except first
letter of new word
ex: Session, Bank, ATM (abbreviation for AutomatedTellerMachine),
CardReader
b. Other user-defined types - ditto
ex: ApprovalCode
c. Method names begin with lowercase - then lower case except first
letter of new word
ex: doSessionUseCase()
d. Field names begin with underscore - then lower case except first
letter of new word
ex: _state
(Note: _PIN is an exception because it is a standard abbreviation)
e. Constant names (e.g. enum values) all uppercase
ex: RUNNING
f. Also: variables other than fields: begin with lower case, then
lower case except first letter of new word
ex: theATM, theBank
C. Our text makes a number of design suggestions in the chapters you were
assigned to read.
1. ASK
2. TRANSPARENCY - DESIGN SUGGESTIONS
ASK CLASS WHAT EACH MEANS
D. Here are some additional general principles, taken from a book by
McGregor and Sykes:
TRANSPARENCY
Go over each
Copyright ©1998 - Russell C. Bjork