C
C
C
Part I
(1) What is C++?
(2) What is C++ in relation to C?
(3) The simple new features of C++:
I/O
Comments
Variables
Constants
Managing heap objects
Referencing objects
(4) Designing classes
(5) A Student class
(6) Working with objects
(7) C++ Student class implementation (STUDENT.H)
(8) Student class functionality (STUDENT.CPP)
(9) Code to test the Student class (STEST.CPP)
(10) Syntax when coding classes
(11) Hiding data
(12) Revised Student code to support encapsulation
(13) Summary of OO terminology
(14) A C implementation of a C++ class
(15) C Student class implementation (CSTUDENT.H)
(16) CSTUDENT.C
(17) Code to test the C Student class (STEST.C)
(18) Programming operators
(19) Operator member function syntaxx
(20) Defining an operator
(21) Employee stock code (ESTOCK.H,ESTOCK.CPP,ESTEST.CPP)
(22) Overloading functions/operators
(23) Default parameters
(24) Protecting data
(25) Selected exercises
Part II
(26) Copy constructor
(27) When to supply a copy constructor
(28) Assignment operator
(29) Using the this pointer
(30) Destructor
(31) Static class members
(32) Revised EmployeeStock code (ESTOCK.H)
(33) Revised ESTOCK.CPP
(34) Test code (ESTEST.CPP)
(35) Friend functions
(36) FRIENDEX.CPP
(37) Friend classes
(38) Providing streamable support
(39) Programming the display/input functions
(40) Revised personn class (FRIENDEX2.CPP)
(41) File input/output using streams
(42) Writing class file i/o support
(43) Inline functions
(44) Making an outline function inline
(45) Selected exercises
Part III
(46) Deriving classes
(47) General derivation syntax
(48) Deriving UniversityStudent from Student
(49) Revised Student class (STUDENT2.H)
(50) STUDENT2.CPP
(51) UniversityStudent definition (USTUDENT.H)
(52) USTUDENT.CPP
(53) UTEST.CPP
(54) Dynamic binding
(55) Virtual functions
(56) A virtual display() function
(57) Revised STUDENT2.H
(58) Revised USTUDENT.H
(59) Revised USTUDENT.CPP
(60) Test code (DBIND.CPP)
(61) A musical instrument derivation hierarchy
(62) Multiple inheritance
(63) Memory map of multiple inheritance
(64) Inheritance versus embedding
(65) Virtual base classes
(66) Pointers to member functions
(67) Assertions
(68) Exceptions
(69) Sample exception code (EXCEPT.CPP)
(70) When to code exceptions
(71) Selected exercises
Part IV
(72) C++ data structures
(73) An abstract base class
(74) An abstract Object definition
(75) C++ linked list code (OBJECT.H, LL.H)
(76) LL.CPP
(77) LLTEST.CPP
(78) Templates
(79) Template header file definition
(80) A template array definition (ARRAY.H)
(81) ARRAYTST.CPP
(82) Multiple template types
(83) Templates in general...
(84) A linked list template implementation
(85) LinkedList template definition (LLT.H)
(86) LLTTEST.CPP
(87) Selected exercises
What is C++?
C++ is an extension of the C language providing objectoriented support to programmers,
namely:
. Encapsulation (Data hiding/protection)
. Overloading (Multiple function instances)
. Inheritance (Reusability)
. Polymorphism (Runtime type resolution, dynamic binding)
. Templates (Type parameterizable classes)
The above techniques provide programmers with increased power with the minimum of syntax,
hence validating C++'s popularity in the world of objectoriented technology.
As a result, C++ is widely replacing C as a worldwide industry standard.
What is C++ in relation to C?
C is actually a subset of C++, meaning that all C programs will run under a C++ compiler, but
not viceverse.
| |
| |
| | C | C++ |
| | printf() | cout |
| | malloc() | new |
| | main() | virtual |
| | static | class |
| | #define | public |
| | ... | inline |
| const |
| ... |
| |
All C++ programs should be suffixed with .CPP.
The compiler does not recognize C++ keywords in a .C file.
The simple new features of C++
(1) I/O
Stream classes have been created to replace the standard C file streams. The objects cin, cout,
and cerr are the equivalents of the C stdin, stdout, stderr file streams.
#include <iostream.h>
void main()
{
int g;
float f;
cout << "Enter variables g,f ";
cin >> g >> f;
cout << "You entered " << g << "," << f << endl;
}
Output
Enter variables g,f 54 78.9
You entered 54,78.9
(2) Comments
An additional commenting method has been added to the language to support quick commenting
of lines.
The rule is the compiler ignores any text after the sequence // up until the next carriage return.
// A program to print hello twice
/* The C printf() is called and the cout
object simulates the result */
#include <iostream.h>
#include <stdio.h>
void main()
{
printf("Hello! C functions supported\n");
cout << "Hello! the C++ way" << endl;
// cout << "This line of code is commented out\n";
}
(3) Variables
Recall that C restricts variables to being declared at the beginning of a function.
In C++, variables can be declared anywhere within the enclosing braces {..} of a function.
This gives scope for grouping variables near the code they are used in for better program
readability.
#include <iostream.h>
void main()
{
cout << "My program is executing" << endl;
int x = 6; //!! we can declare x flush to the
// for loop where it is used
for (int i = 7; i + x > 5; i)
cout << "[" << i << "]" << endl;
}
(4) Constants
Recall that C provides macro substitution of constants or computed constants at compile time via
the #define directive.
#define G 9.8
...
f = m * G; /* C compiler subs in 9.8 for G */
Note however that no type is allotted for G.
C++ provides the const keyword to typedefine a constant:
const <type> <variable name> = <value>;
A physical cell is allocated in memory of size depending on <type> and const guarantees that no
one can change the value of <variable name>.
The compiler can perform typechecking can be done on the variable as it is passed into
functions.
#include <iostream.h>
const int MASS = 10;
const float G = 9.8;
float force(int mass,float acceleration)
{ return (mass*acceleration); }
void main()
{
cout << "Force is " << force(MASS,G) << " newtons " << endl;
}
(5) Managing heap objects
The keywords new and delete replace the C malloc() and free() calls.
The syntax is:
<Object pointer> = new <Object type>
...
delete <Object pointer>;
void main()
{
int* heapArrayOfInts = new int[55];
heapArrayOfInts[0] = 56;
heapArrayOfInts[1] = 52;
//...
delete heapArrayOfInts;
}
(6) Referencing objects
One complaint C programmers had was that the pass by reference pointer syntax was ugly and
prone to compile errors.
For example, if we wanted to write a C swap function, we would have to code:
void swap(int *a,int *b)
{
int temp = *a;
*a = *b; /* Too many *'s! */
*b = temp;
}
C++ provides an easier mechanism for modifying arguments via the reference operator (&):
void swap(int &a,int &b) // Pass a,b by reference
{
int temp = a;
a = b; // We are modifying the calling
b = temp; // parameters, not local copies
}
Functionally, both code segments are the same; behind the scenes, C++ translates the above logic
into the equivalent C swap code. But syntactically we can refer to the reference (or pointer to) a
and b without all the *'s.
Note that referencing and pointer syntax are interchangeable:
int A,*a=&A; // A is an integer, a points to A
int &aReference = *a; // Creates aReference to A
aReference = 9; // Changes A to 9
*a = 10; // Changes A to 10
Designing classes
The power of C++ resides in its ability to create classes. A class is a definition of data with
operations on that data. The definition rings somewhat similar to that of a data structure; in
fact they are one and the same.
| CLASS |
| <has> DATA |
| <has> FUNCTIONALITY |
When we create data structures in C we must carefully package the unit via a .H header file
describing the data and a corresponding .C file describing the operations.
We can simulate this packaging much the same way but with greater ease, effect and power using
the C++ class keyword.
A Student class
Consider a simple implementation of a data structure, or class, that supports data on students.
Suppose a student "has a" name and two marks. We can represent this pictorially by a class data
diagram:
|Student |
|> name : string
|> mark1 : integer ">" denotes "has a" condition
|> mark2 : integer
We can extend this definition by adding on the things that we would like to do with a student.
We want to initialize a student, edit a student, and calculate their average:
|Student |
name : string <=|
mark1 : integer | DATA PART
mark2 : integer <=|
init() <=|
edit() | FUNCTIONAL PART
average() <=|
Having the complete class diagram, we are ready to translate the sketch into C++ code.
Working with objects
Many objects of various classes can be sitting in memory and we can talk to them by sending
messages to them via function calls or by reading/writing to their data members.
We always think of the object first, meaning that the object must exist (in memory) before we can
access it. This makes sense. That is why syntactically, C++ demands the name of the object
first. Without knowing the name of the object, we can't perform any request.
Once we have the object, then we can suffix the object name specifically with the request, being
the data we want to grab or the message we want to send.
To access an object method, we code:
<object name>.<function name>(<argument list>)
To access a data member of an object, we could code
<object name>.<data member name>
If object name is a pointer to an object, we use the > operator:
<object name>>[request]
C++ Student class implementation (STUDENT.H)
#ifndef STUDENTH
#define STUDENTH
const int MAX_STUDENT_NAME_CHARS = 40;
class Student
{
public:
char name[MAX_STUDENT_NAME_CHARS]; /* DATA MEMBERS */
int mark1,mark2;
Student(); /* FUNCTION MEMBERS */
Student(char*,int,int);
void edit(char *,int,int);
void display();
int average();
};
#endif
Student class functionality (STUDENT.CPP)
#include "student.h"
#include <string.h>
#include <iostream.h>
Student::Student() /* DEFAULT CONSTRUCTOR */
{
name[0] = NULL;
mark1 = 0;
mark2 = 0;
}
Student::Student(char *_name,int _mark1,int _mark2) /* SPECIFIC CONSTRUCTOR */
{
edit(_name,_mark1,_mark2);
}
void Student::edit(char *_name,int _mark1,int _mark2) /* MUTATOR */
{
strcpy(name,_name);
mark1 = _mark1;
mark2 = _mark2;
}
void Student::display()
{
cout << name << "," << mark1 << "," << mark2 << endl;
}
int Student::average()
{
return ((mark1+mark2)/2);
}
Code to test the student class (STEST.CPP)
#include "student.h"
#include <iostream.h>
void main()
{
Student s1("Martha",67,78); // (Method1) create a local object instance,
// deallocated automatically
s1.display();
cout << s1.name << "'s average is " << s1.average() << endl;
Student *s2 = new Student; // (Method2) create a heap object instance
s2>edit("Joey",78,55);
s2>display();
cout << s2>name << "'s average is " << s2>average() << endl;
delete s2; // free the heap object
}
Output
Martha,67,78
Martha's average is 72
Joey,78,55
Joey's average is 66
Memory map
Stack:
s1 | Martha |
| 67 |
| 78 |
Heap:
s2> | Joey |
| 78 |
| 55 |
Syntax when coding classes
The header file (.H) contains the class definition: a description of the data members and
prototypes of the member functions.
The actual details of the functions are described in a corresponding (.C) file.
To bind a member function to a particular class, the scope operator :: is used. The syntax is:
<return type> <Class name>::<function name>(<argument list>)
{
...(function body)
}
Without a :: class scope specifier, we are back into writing C code. The following describes a
floating global C function:
void display()
{
cout << name << "," << mark1 << "," << mark2 << endl;
}
Not to mention that the compiler won't recognize the symbols name, mark1, and mark2 because
they are not declared. They are only recognized within the class as class members:
void Student::display()
{
// This works becase name, mark1, mark2 are declared
// as members of the Student class
cout << name << "," << mark1 << "," << mark2 << endl;
}
Hiding data
As it stands, there are some flaws with the Student class. Note that the data members are
declared as public, meaning that users of the class can directly access them and possibly write to
them maliciously:
Student s1;
strcpy(s1.name,"....80 characters");
// crashes! because s1.name has only space for 40 chars
We should design the class to protect the integrity of the data members. We can do this by
declaring our data members private, thus hiding or encapsulating the core data from users, and
by providing better internal error checking in our edit() mutator.
Of course we will have to supply accessor functions to allow read access to name, mark1 and
mark2.
const char* Student::getName() { return name; }
We can protype the getName() accessor with const to force the compiler to prevent the class user
from inadvertantly changing the contents of the name array.
Our getMark1() and getMark2() accessors return merely a copy of the data, so const protection is
unnecessary:
int Student::getMark1() { return mark1; }
int Student::getMark2() { return mark2; }
Revised Student code to support encapsulation
STUDENT.H
class Student
{
private:
char name[MAX_STUDENT_NAME_CHARS]; /* DATA MEMBERS */
int mark1,mark2;
public:
Student(); /* FUNCTION MEMBERS */
Student(char*,int,int);
void edit(char *,int,int);
void display();
int average();
const char* getName();
int getMark1();
int getMark2();
};
STUDENT.CPP
#define min(a,b) (a < b ? a : b)
...
void Student::edit(char *_name,int _mark1,int _mark2) /* MUTATOR */
{
strncpy(name,_name,
min(strlen(_name)+1,MAX_STUDENT_NAME_CHARS));
mark1 = _mark1;
mark2 = _mark2;
}
const char* Student::getName() { return name; }
int Student::getMark1() { return mark1; }
int Student::getMark2() { return mark2; }
STEST.CPP
void main()
{
Student s1("Martha",67,78); // (Method1) create a local object,
// deallocated automatically
s1.display();
// strcpy(s1.name,"Joeee"); // Gives error: cannot access private member
// strcpy(s1.getName(),"joeey"); // Gives error: cannot convert const char* to char*
cout << s1.getName() << "'s average is " << s1.average() << endl;
Student *s2 = new Student; // (Method2) create a heap Student object
s2>edit("Joey",78,55);
s2>display();
cout << s2>getName() << "'s average is " << s2>average() << endl;
delete s2; // free the heap object
}
Summary of OO terminology
Class definition:
A definition or template of a data set and operations that work particularly on that data set.
Object instance:
An object is any physical instance or manifestation of a class that is sitting in memory. A String
is an object. A Console is an object, even an integer can be considered on object.
Class member:
Any entity of the class, a piece of data or a function. A class can contain both data members and
function members.
Encapsulation:
The ability of a class to "hide" its internal details from the outside world.
Constructor:
A special function that initializes an object of a particular class to a certain state.
Accessor:
A special function that accesses a piece of data from an object for readaccess only and returns it
to a caller from the outside world.
Mutator:
A special function called by a caller from the outside world that sets an objects internal piece(s)
of data to particular value(s).
Method:
Any class member function.
A C implementation of a C++ class
Recall that in C to do the same job we had define a structure of the data involved then write
floating C functions to do the work. We even had to be careful to choose the names so they
wouldn't conflict with other possible common names in other data structures.
A C++ class looks strikingly similar to a C struct with some functions embedding.
In fact we could simulate the student class version in C. In fact, this is what kind of code the
compiler ultimately generates, not in C, but in native code.
Some earlier C++ compilers were in fact merely C translaters. They took C++ code, generated
C code, and invoked a C compiler to get the final native .EXE.
A true C++ compiler translates C++ code directly into native machine code.
C Student class implementation (CSTUDENT.H)
#ifndef CSTUDENTH
#define CSTUDENTH
#define MAX_STUDENT_NAME_CHARS 40
typedef struct student
{
/* Data members */
char name[MAX_STUDENT_NAME_CHARS];
int mark1,mark2;
/* Function members */
void (*init1)(struct student*);
void (*init2)(struct student*,char *,int,int);
void (*edit)(struct student*,char *,int,int);
void (*display)(struct student*);
int (*average)(struct student*);
} Student;
/* Global student instance initializer */
extern void StudentInstance(Student *s);
#endif
CSTUDENT.C
#include "cstudent.h"
#include <stdio.h>
#include <string.h>
void cstudent_init1(Student *s) /* DEFAULT CONSTRUCTOR */
{
s>name[0] = 0;
s>mark1 = 0;
s>mark2 = 0;
}
void cstudent_edit(Student *s,char *_name,int _mark1,int _mark2) /* MUTATOR */
{
strcpy(s>name,_name);
s>mark1 = _mark1;
s>mark2 = _mark2;
}
void cstudent_init2(Student *s,char *_name,int _mark1,int _mark2) /* SPECIFIC CONSTRUCTOR */
{
cstudent_edit(s,_name,_mark1,_mark2);
}
void cstudent_display(Student *s)
{
printf("%s,%d,%d\n",s>name,s>mark1,s>mark2);
}
int cstudent_average(Student *s)
{
return ((s>mark1+s>mark2)/2);
}
void StudentInstance(Student *s)
{
/* initialize function pointers for object <s> */
s>init1 = cstudent_init1;
s>init2 = cstudent_init2;
s>edit = cstudent_edit;
s>display = cstudent_display;
s>average = cstudent_average;
}
Code to test the C Student class (STEST.C)
#include "cstudent.h"
#include <stdio.h>
void main()
{
Student S1,*s1=&S1;
StudentInstance(s1); /* Set up <S1> for operations */
(*S1.init1)(s1); /* Invoke Default constructor */
(*S1.edit)(s1,"Joe",56,78); /* S1.edit() */
(*S1.display)(s1); /* S1.display() */
printf("%s's average is %d\n",
S1.name,(*S1.average)(s1)); /* S1.average() */
/* Note: in C, it is not possible to protect
S1.name from possible outside corruption */
}
Output
Joe,56,78
Joe's average is 67
Programming operators
C++ provides the opportunity to create operators on classes.
These work in the same way as member functions, yet are more powerful syntactically to users of
your class.
The following operators are programmable:
+ * / % ^ & | ~ ! , = < > <= >= ++ << >> == !=
&& || += = /= %= ^= &= |= *= <<= >>= [] () > >*
new delete
Operator member function syntax
The syntax for coding a member function is:
// .H file
class <Class name>
{
...
<return type> operator<operatorName>(<arg list>); // prototype
...
};
// .CPP file
<return type> <Class name>::operator<operatorName>(<arg list>)
{
... function body
}
// TEST.CPP for invoking the operator
...
<Class name> a,b;
result = a <operatorName> b; // same as a.funcName(b);
Defining an operator
Suppose a company records the number of shares each employee has in a database with the
following records data:
EmployeeStock
|> departmentID
|> employeeID
|> numberOfShares
They may decide to get statistics on employees from various departments, namely, what is the
total number of shares that employees in department X have?
We could define how to add two employee stocks together: by defining the + (add) operator on
two objects of EmployeeStock to return an Employee Stock object containing the sum of
numberOfShares.
ESTOCK.H
#ifndef EMPLOYEESTOCKH
#define EMPLOYEESTOCKH
class EmployeeStock
{
public:
EmployeeStock();
EmployeeStock(int,int,int);
EmployeeStock operator+(EmployeeStock&);
int EmployeeStock::getNShares();
private:
int deptID,employeeID,nShares;
};
#endif
ESTOCK.CPP
#include "estock.h"
// Constructors can initialize data members via an : initializer list
EmployeeStock::EmployeeStock() : deptID(0),employeeID(0),nShares(0)
{}
EmployeeStock::EmployeeStock(int _deptID,int _employeeID,int _nShares)
: deptID(_deptID),employeeID(_employeeID),nShares(_nShares)
{}
EmployeeStock EmployeeStock::operator+(EmployeeStock &es)
{
EmployeeStock result;
result.nShares = nShares + es.nShares;
return result;
}
int EmployeeStock::getNShares() { return nShares; }
ESTEST.CPP
#include "estock.h"
#include <iostream.h>
void main()
{
// Simulate two employees from department 54
// with 100 and 10 shares respectively
EmployeeStock e1(54,12,100),e2(54,11,10),result;
result = e1 + e2; // invoke operator+() to
cout << "Dept 54's total stock is " << result.getNShares() << endl;
}
Output
Dept 54's total stock is 110
Overloading functions/operators
Sometimes we may want to code multiple instances of a function. C++ allows functions to have
several forms. We have already seen this with constructors. Rarely do we have only one way to
construct an object. Sometimes we may wish to set none of the data, sometimes portions,
sometimes all.
Let's define how to compare EmployeeStock, say by comparing their nShares fields.
We can overload the < operator to handle two types of comparisons via two operator<()
functions:
EmployeeStock a(..),b(..)
if (a < b) // if a's nShares is less than b's
...
if (b < 60) // if b's nShares is less than 60
...
int EmployeeStock::operator<(const EmployeeStock &e) // const promises that
{ // the function will not
return (nShares < e.nShares); // change the parameter e
}
int EmployeeStock::operator<(int _nShares)
{
return (nShares < _nShares);
}
Note that the above two functions vary in signature, namely in their argument list part. One
takes an EmployeeStock object the other an integer. As long as the compiler has no trouble
distinguishing between function instances, there are no resolution problems. Two identical
signatures result in a "multiply defined" compiler error.
Note that return types do not count as part of the change in signature, so void f(int a); and int
f(int a); are not allowed.
Default parameters
Often, especially in long parameter lists, it is nice to provide default arguments, to give the class
user extra convenience.
Consider an Inventory Record with four fields:
InventoryRecord
|> description : string
|> # of units : integer
|> distributor : string
|> targetMarket : integer
In the header file, we can define default arguments for the distributor and targetMarket fields
because let's say 90% of the time they will be "ACE Limited" and CANADA respectively.
enum {CANADA,USA,EUROPE,WORLDWIDE};
class InventoryRecord
{
private:
char *description;
int nUnits;
char *distributor;
int targetMarket;
public:
InventoryRecord(char *_description,int _nUnits,
char *_distributor = "ACE Limited",
int _targetMarket = CANADA);
};
...
InventoryRecord::InventoryRecord(char *_description,int _nUnits,
char *_distributor,int _targetMarket)
: description(_description),nUnits(_nUnits),
distributor(_distributor),targetMarket(_targetMarket)
{}
...
void main()
{
InventoryRecord ir1("Skates",400); // defaults <distributor>,<targetMarket> fields
InventoryRecord ir2("Skiis",110,"Lowdry Inc."); // defaults <distributor>
InventoryRecord ir3("Skiis",110,"Venture Enterprises",EUROPE); // overrides defaults
...
}
Note the order of default parameters is important. If we wish to let the distributor default, then
also we must let the targetMarket default. The following is illegal:
InventoryRecord ir4("Gloves",25,,WORLDWIDE);
Protecting data
We can protect internal data from class users by declaring a class's data members as private, but
that does not protect the data from accidental misuse by the class members themselves.
The compiler can catch unwanted writes to data members if the class designer tags a const
keyword after the prototype:
class EmployeeStock
{
private:
int deptID; ...
...
int getNShares() const; // function promises not to modify <deptId> or other members
};
int EmployeeStock::getNShares() const
{
deptID = 6; // illegal access, compiler error
return nShares;
}
When creating a member function we should decide whether a function changes internal
members or not. If it doesn't then supply the const keyword.
This is good practice for three reasons:
(1) To catch firstround implementation errors
(2) For increased readability to class users scanning the .H file
(3) To make upgraders of your class (or meddlers) think twice about altering a member function
to modify data members when the original definition was readonly. There must have been some
reason.
Part I Exercises
(1) Why is it better to make class data members private instead of public?
(2) Design a vector class to store x,y components as real numbers. Define the operators +,,
==,<,>,<=,>= and * as the dot product of two vectors. Recall the dot product definition
v1 = (x1,y1), v2 = (x2,y2)
v1 * v2 = (x1*x2,y1*y2)
Create a function to return the unit vector perpendicular to a vector.
Vector& normal();
Provide a mainline to test your class. Document your class.
(3) Revise the student class to support the following definition:
Student
|> name
|> phone#
|> list of marks
Write functions average() to compute the average of <list of marks> and median() to return the
"middle" mark in the list.
(4) Use the EmployeeStock definition to create a simulation of a company of employees holding
shares. Allow the user to type in various employees from different departments. Your program
stores EmployeeStock objects in an array.
Allow the user to enter a departmentID and the program tallies shares totals for employees of that
department.
If you want to make your program more userfriendly, encode a lookup table of department
names. This way the user can type in a department name instead of an ID.
Use the + operator to create your total.
Copy constructor
The default copy scenario is a bitwisecopy. All the members from one object are copied
directly to the corresponding members of another object.
All classes in C++ support a default copy constructor. No code is required.
A copy of an object can be created in three possible ways in C++. Two are via a construction:
EmployeeStock e1,e2=e1,e3(e2); // e2 is an exact copy of e1,
// e3 is an exact copy of e2
The third is an indirect method when an object is passed into a function. In the following
example, e is not passed by reference which informs the compiler to create a local copy of the
incoming object and name it e:
char* lookupEmployee(EmployeeStock e)
{
char *employeeName = employeeTable[e.id];
return (employeeName);
}
...
EmployeeStock es(45,13,12);
char *name = lookupEmployee(es);
Tip It is better to pass e by reference to avoid the bitwise
copy operation and save time.
When to supply a copy constructor
There are times when we have to write code to override the default copying mechanism.
Particularly This occurs when an object has a complex makeup.
Suppose we wanted to tag stock options onto an EmployeeStock. We could add an options
member acting as an array. Over the lifetime of the object, options can be added.
Option
|> dateOfIssue :
|> optionType : integer
EmployeeStock
|> employeeId
|> departmentId
|> nShares
|> options : array of options
|> nOptions : total # of options
|> addOption()
Suppose that heap Option objects are added to the array options by a call to addOption().
Resorting to the default bitwise copying method results in a copy from a source object
SOURCE
| departmentID : 55
| employeeID : 32 0 1 2 MAX_OPTIONS
| nShares : 14
| options :> | | | | ... | |
|
| | | |
|
| |05|97 | |06|97 | |12|97 |
| |SINGLE| |DOUBLE| |TRIPLE |
|
|
COPY |
|
| departmentID : 55 |
| employeeID : 32 |
| nShares : 14 |
| options : >
If we delete an option from copy, then we also delete an option from source. Definitely not
desirable. COPY should have its own copy of the three pieces of option data.
To enact this, we must create our own copy routine that make copies of each of the elements in
the options array.
Copy constructor syntax:
<Class Name>::<Class Name>(const <Class Name> & source)
{
// code to copy source's members to this object
}
Assignment operator
Suppose we want to assign one object's data to another:
EmployeeStock e1(...),e2,e3;
e2 = e1; // This does not call copy constructor!
e3 = (e2 = e1); // chained assignment e2 < e1 then e3 < e2
Again, if a bitwise assignment of members is undesirable, we will have to overload the
assignment operator to provide a welldefined assignment, code similar to our copy constructor
code.
Assignment operator syntax:
<ClassName>& <Class Name>::<Class Name>(const <Class Name> & source)
{
// code to assign source's members to this object
}
Using the this pointer
To write the chained assignment operator we have to return the result of the current assignment
being done. This requires use of the this pointer.
This refers to the object "being worked on" or the object bound to the operation:
Object X begin worked on
this >
| departmentID : 55 |
| employeeID : 32 |
| nShares : 14 |
| options : > | | | .... | | |
| |
| || || |
| \/ \/ |
| ... ... |
When the computer executes the statement a = b, the object bound to the operation is a, the
operation is =, and the argument is b.
The completed assignment operator:
EmployeeStock& EmployeeStock::operator=(const EmployeeStock &e)
{
doCopy(e); // Execute code to do the assignment
return *this; // Because this is a pointer to the object
// being worked on, we have to dereference it
// to pass the actual object
}
Destructor
When an object goes out of scope, a destruction process occurs deallocating all class members,
so the memory can be reused for other objects.
If the construction of class members is simple, like ints, floats, and char buffer types, we can rely
on the compiler to free them up. This is because the compiler knows the exact size of the data at
compile time.
But if with the new command we allocate any embedded objects or structures over the lifetime of
the object, we have to write a function responsible enough to deallocate or wrap up the extra
allocation done.
Such a function is deemed a destructor. As a general rule, if we have provided copy constructor
and/or assignment operators, we must provide a destructor. If we don't provide the destructor, we
create a memory leak in the system and our program hogs memory that should be given up to the
system, possibly resulting in an out of memory runtime error.
Destructor syntax:
<Class Name>::~<Class Name>()
{
// code to wrap up this object
}
Static class members
Sometimes we want to have variables common to all objects of a particular class. We can do this
by prefixing them with the static keyword.
This is useful in our Option class when it comes time to display the type field. Displaying 1, 2 or
3 is inferior to outputting SINGLE, DOUBLE, or TRIPLE to the user. We can create a lookup
table of type descriptions to use in conjunction with display().
The point is, we want only one copy of the array to exist in memory, not one for each object
instance.
Sure, we could create a static array of type descriptions in our .CPP class code file, but for
readability sake, it is nice to embed the array within the Option class:
const int NTYPES = 3;
class Option
{
private:
...
static char* types[NTYPES]; // Only one copy of the
// array exists for all
// objects
};
Initialize static members using the scope operator:
<type> <ClassName>::<variable name> = <value>;
char* Option::types[] = {"SINGLE","DOUBLE","TRIPLE"};
Revised EmployeeStock code
ESTOCK.H
#ifndef EMPLOYEESTOCKH
#define EMPLOYEESTOCKH
#define MAX_OPTIONS 10
enum {SINGLE,DOUBLE,TRIPLE};
class Option
{
public:
int issueYear,issueMonth,type;
static char *types[3]; // fixed type descriptions for all Option objects
Option();
Option(int _issueYear,int _issueMonth,int issueType);
void display();
};
class EmployeeStock
{
public:
EmployeeStock();
EmployeeStock(const EmployeeStock &e); // Copy constructor
EmployeeStock(int,int,int);
EmployeeStock operator+(const EmployeeStock&);
EmployeeStock& operator=(const EmployeeStock&);
void doCopy(const EmployeeStock&);
int EmployeeStock::getNShares();
~EmployeeStock();
void addOption(int,int,int);
Option* getOption(int index);
private:
int deptID,employeeID,nShares;
Option **options;
int nOptions;
};
#endif
Revised ESTOCK.CPP
#include "estock.h"
#include <iostream.h>
// Option class code
Option::Option() : issueYear(0),issueMonth(0),type(0)
{}
Option::Option(int _issueMonth,int _issueYear,int _type)
: issueMonth(_issueMonth),issueYear(_issueYear),type(_type)
{}
char *Option::types[] = {"SINGLE","DOUBLE","TRIPLE"};
void Option::display()
{
cout << issueMonth << "," << issueYear << "," << types[type] << endl;
}
// EmployeeStock class code
EmployeeStock::EmployeeStock() : deptID(0),employeeID(0),nShares(0),
options(0),nOptions(0)
{}
EmployeeStock::EmployeeStock(int _deptID,int _employeeID,int _nShares)
: deptID(_deptID),employeeID(_employeeID),nShares(_nShares),
options(0),nOptions(0)
{}
void EmployeeStock::addOption(int _issueMonth,int _issueYear,int _type)
{
if (!options)
options = new Option*[MAX_OPTIONS];
if (nOptions < MAX_OPTIONS)
options[nOptions++] = new Option(_issueMonth,_issueYear,_type);
}
Option* EmployeeStock::getOption(int index)
{
if ((index < 0) || (index >= MAX_OPTIONS))
return 0;
else
return options[index];
}
// Copy constructor
EmployeeStock::EmployeeStock(const EmployeeStock &e)
{
doCopy(e);
}
// Assignment operator
EmployeeStock& EmployeeStock::operator=(const EmployeeStock &e)
{
doCopy(e);
return *this;
}
void EmployeeStock::doCopy(const EmployeeStock &e)
{
if (!e.options)
options = 0;
else
{
options = new Option*[MAX_OPTIONS];
for (int i = 0; i < e.nOptions; i++)
{
options[i] = new Option(*e.options[i]);
}
nOptions = e.nOptions;
}
}
// Destructor
EmployeeStock::~EmployeeStock()
{
if (options)
{
for (int i = 0; i < nOptions; i++)
delete options[i];
delete options;
}
}
EmployeeStock EmployeeStock::operator+(const EmployeeStock &es)
{
EmployeeStock result;
result.nShares = nShares + es.nShares;
return result;
}
int EmployeeStock::getNShares() { return nShares; }
Test code (ESTEST.CPP)
#include "estock.h"
#include <iostream.h>
void main()
{
EmployeeStock e22(56,32,900);
e22.addOption(5,97,SINGLE);
e22.addOption(6,97,DOUBLE);
e22.addOption(12,97,TRIPLE);
EmployeeStock e23(e22);
cout << "E23's 2nd option type is: " << endl;
e22.getOption(1)>display();
EmployeeStock e24,e25;
e25 = (e24 = e23);
cout << "E24's 3rd option type is: " << endl;
e24.getOption(2)>display();
cout << "E25's 1st option type is: " << endl;
e25.getOption(0)>display();
}
Output
E23's 2nd option type is:
6,97,DOUBLE
E24's 3rd option type is:
12,97,TRIPLE
E25's 1st option type is:
5,97,SINGLE
Friend functions
It is possible to allow functions outside of a class to have read/write access to a class's private
variables. The class designer must give authorization for such access.
Such functions are deemed friend functions. The label "friend" is given with the meaning that
such a function is friendly to a class, promising not to corrupt its internal variables.
Some may argue that such priviledges betray the objectoriented philosophy of encapsulation, but
we will see that there is a special need for friend functions in at least one common case in the
C++ language.
Let's study an example that allows a C function verifyAge() to modify the private variable
inTheInClub of the Person class.
As you can see from the output only Gretel and Geronimo are part of the "in club" because they
are between the ages of 25 and 35.
FRIENDEX.CPP
#include <iostream.h>
#include <string.h>
const int TRUE = 1,FALSE = 0;
class Person
{
private:
int age;
char name[80];
int inTheInClub;
public:
Person(char*,int);
void display();
friend void verifyAge(Person&); // The class designer gives permission
// for the function verifyAge() to
// read/write to the private variables
};
Person::Person(char *_name,int _age) : age(_age),inTheInClub(FALSE)
{
strcpy(name,_name);
}
void Person::display()
{
cout << name << "," << age << "," << inTheInClub << endl;
}
static void verifyAge(Person &p)
{
if ((p.age <= 35) && (p.age >= 25))
p.inTheInClub = TRUE;
else
p.inTheInClub = FALSE;
}
void main()
{
Person p1("Harry",56),p2("Gretel",34),p3("Vina",16),p4("Geronimo",29);
verifyAge(p1); // decide who can be in the inclub
verifyAge(p2);
verifyAge(p3);
verifyAge(p4);
p1.display(); // display the results
p2.display();
p3.display();
p4.display();
}
Output
Harry,56,0
Gretel,34,1
Vina,16,0
Geronimo,29,1
Friend classes
C++ goes a step further and allows whole classes to be friends of another class. This means to
say that if class B is a friend of A then all the member functions of B can access the private
members of A.
The friend class relationship should only occur if there is by definition a strong link between
classes A and B, that is that B has its fingers in A a great amount of time and that B's fingers are
trustworthy.
A B
| private: | | public: |
| x,y,z <========== | f1() |
/\ | f2() |
|| | f3() |
|| | ... |
||
Strong link
The number of friend class declarations should be kept in check because it can weaken the
definition of OO encapsulation.
This feature is great for testing or prototyping a system without having rigorous protection
mechanisms and accessor/mutator methods in place. Once the code has been hammertested
with "loose" friend class relationship(s), it can be tightened by eliminating friend classes and
shipped for protection.
For example, suppose we have one class A that is linked heavily to another class B. Class A
wishes to give permission to class B to access its private members but no one else!
class A
{
friend class B; // A gives permission for B's member functions
// to access A's private variables
private:
int x,y,z;
...
};
class B
{
public:
void f1(A &a)
{
a.z = 56; // this is ok
}
...
};
Providing streamable support
The last program was a weak "reallife" example of the use of friend functions. We could have
just made verifyAge() a member function of the Person class and avoided the whole "friend"
fiasco! When do we really need friends?
Suppose we want to use the cout and cin objects to display Persons and input them from the
keyboard.
It would be nice to code:
Person p;
cin >> p; // ask the user to input Person data
cout << p; // display person p's input data
In order for the cin and cout objects to do their job, they must have access to the private members
of p. The only one who can do this is the class designer of Person, and that is us.
Specifically, we must supply the functionality to do so by overloading the << and >>
streamable operators for the cout and cin objects taking a Person parameter as input. We must
label such functions as friends.
Programming the display function
cin is an instance of the class istream.
cout is an instance of the class ostream.
The syntax for the input and display functions is somewhat cryptic. They can be better
understood by understanding what is the bound object, the operation, and the input parameter in
the usage of cin and cout to perform the i/o.
Bound object operation parameter
cout << p
Return Function Bound Input
argument name object parameter
|| || || ||
\/ \/ \/ \/
friend ostream& operator<<(ostream &o,Person &p)
{
// Do the code to pipe to object o (cout) the details
// of Person p...
// Return o so that chained cout << p1 << p2 << ...
// can occur
...
return(o);
}
Programming the input function
Bound object operation parameter
cin >> p
Return Function Bound Input
argument name object parameter
|| || || ||
\/ \/ \/ \/
friend istream& operator>>(istream &i,Person &p)
{
// Do the code to allow object i (cin) to input
// Person p's details from the keyboard
// Return i so that chained cin >> p1 >> p2 >> ...
// can occur
...
return(i);
}
Revised Person class code (FRIENDX2.CPP)
#include <iostream.h>
#include <string.h>
const int TRUE = 1,FALSE = 0;
class Person
{
private:
int age;
char name[80];
int inTheInClub;
public:
Person();
Person(char*,int);
friend ostream& operator<<(ostream&,Person&); // prototype for display function
friend istream& operator>>(istream&,Person&); // prototype for input function
};
Person::Person() : age(0),inTheInClub(FALSE)
{
name[0] = 0;
}
Person::Person(char *_name,int _age) : age(_age),inTheInClub(FALSE)
{
strcpy(name,_name);
}
ostream& operator<<(ostream &o,Person &p)
{
o << p.name << "," << p.age << "," << p.inTheInClub;
return o;
}
istream& operator>>(istream &i,Person &p)
{
i >> p.name >> p.age;
return i;
}
void main()
{
Person p;
cout << "Enter Person p's <name> <age>: ";
cin >> p;
cout << p << endl;
}
Output
Enter Person p's <name> <age>: Craig 89
Craig,89,0
File input/output using streams
Reading and writing to disk files works the same way as the standard streaming using cin and
cout.
The ofstream class handles output to files, the ifstream class input from files.
Sample usage:
// Program to create an ascii file consisting of personal data
// and read it back in
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
const char *TEST_FILE = "C:\\TEST.DAT";
void main()
{
ofstream o;
o.open(TEST_FILE); // Careful!, overwrites <TEST_FILE>
if (!o)
{
// Incorrect path, write protect or drive door open or
// other error
cout << "Can't open " << TEST_FILE << " for output" << endl;
exit(0);
}
o << "Joe " << 56 << endl; // pipe some data to output file <o>
o.close();
// Reread the same file
ifstream i;
i.open(TEST_FILE);
if (!i)
{
cout << "Can't open " << TEST_FILE << " for input" << endl;
exit(0);
}
char name[80];
int age;
i >> name >> age;
i.close();
cout << name << "," << age;
}
TEST.DAT
Joe 56
Writing class file i/o support
If we wanted to write specific functions, it makes sense to embed them (or make them part of)
the class we are trying to stream, or pipe to/from a file.
Again we can create friend equivalents of operator<< and operator>> functions for the
ofstream and ifstream classes.
Sample code:
#include <iostream.h>
#include <fstream.h>
...
class Person
{
public:
...
friend ofstream& operator<<(ofstream&,Person&); // prototype for file output function
friend ifstream& operator>>(ifstream&,Person&); // prototype for file input function
};
...
// File input/output operations
ifstream& operator>>(ifstream &i,Person &p)
{
i >> p.name >> p.age >> p.inTheInClub;
return i;
}
ofstream& operator<<(ofstream &o,Person &p)
{
// Important to write out spaces as delimiters not commas!
o << p.name << " " << p.age << " " << p.inTheInClub;
return o;
}
const char *TEST_FILE = "C:\\TEST.DAT";
void main()
{
// Write out some Persons to <TEST_FILE>
ofstream o;
o.open(TEST_FILE);
if (!o)
{
cout << "Can't open " << TEST_FILE << " for output" << endl;
exit(0);
}
Person p1("Janie",23),p2("Jeff",24),p3("Granny",21);
o << p1 << endl;
o << p2 << endl;
o << p3 << endl;
o.close();
// Read back all persons in <TEST_FILE> and display to terminal
Person tempPerson;
ifstream i;
i.open(TEST_FILE);
if (!i)
{
cout << "Can't open " << TEST_FILE << " for input" << endl;
exit(0);
}
i >> tempPerson;
while (!i.eof())
{
cout << tempPerson << endl;
i >> tempPerson;
}
i.close();
}
TEST.DAT
Janie 23 0
Jeff 24 0
Granny 21 0
Inline functions
When we want to optimize our code for speed, we can make our member functions inline. This
means that whenever a function call is made, the compiler does not generate a native JSR
instruction (jump to subroutine) and an expensive copy of arguments onto the stack. These can
be too time consuming for our tastes. The actual function code is substituted or embedded inline.
We should not overuse the inline capability as it can make the executable large. Remember that
performance can really only be increased by increasing the speed of commonly used functions.
Deciding upon whether an function should be inline may require some timeanalysis study of
many functions to determine those (if any) that cumulatively eatup an appreciable percentage of
overall run time.
Class accessors and mutators are generally coded inline as they are generally small oneliners
that don't cost us anything in terms of executable size.
A function is automatically loaded inline if the codification appears inside the class header file:
class Person
{
public:
// automatic inline functions
int getInClubStatus() { return inTheInClub; }
const char* getName() { return name; }
int getAge() { return age; }
};
Making an out-line function inline
We can also force outline functions (those in the .CPP file) to be inline by prefixing the inline
keyword in the function prototype.
// Another inline demonstration
// ..a function that converts last name, first name delimiters
// from white space to underscore characters.
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
...
class Person
{
public:
...
inline void checkForNameSpaces();
};
...
void Person::checkForNameSpaces()
{
int len = strlen(name);
for (int i = 0; i < len; i++)
if (isspace(name[i]))
name[i] = '_';
}
void main()
{
Person p("Jody Charon",40);
p.checkForNameSpaces(); // <== The above code is substitued here
cout << p << endl;
}
Output
Jody_Charon,40,0
Part II Exercises
(1) Design a ComplexNumber class. Recall the definition and operations on complex numbers:
Let c be a complex number, then
c = a + bi, i = square root of(1), a,b elements of R
If C1 and C2 are complex numbers,
C1 = a1 + b1i, C2 = a2 + b2i then
C1 + C2 = (a1 + a2) + (b1 + b2)i
C1 C2 = (a1 a2) + (b1 + b2)i
C1 * C2 = (a1*a2 b1*b2) + (a1*b2 + b1*a2)i
C1 / C2 = (a1*a2 + b1*b2)/d + (b1*a2 a1*b2)i/d
where d = a22 + b22
. Provide default and full constructors for your class
. Overload the +,,*,/ operators
. Allow chained operations like the following:
Complex c1(4.1,5.7),c2(2.0,9.8),c3 = c1 + c1 + c2*2;
. Allow mixed operations of complex numbers with a real:
Complex c1(4.0,5.6),c3 = c1+2.1 + c1*2.2 + c1/2.1 + c12.0
. Allow complex numbers to be piped to and from standard input
and file streams
. Test your class
(2) If the class designer supplies a copy constructor is it necessary to provide an assignment
operator? When is it necessary to code a destructor?
(3) Design a Dstring class which provides dynamic string management. A dynamic string can
grow or shrink through the operations provided on the class.
Dstring s1("abc"),s2("def),s3(s1),s4;
s3 = s1 + s2 : concatenates s1 to s2 and returns a result
s3 = s1.mid(4,6); : extracts 6 characters from s1 starting from the 5th character
s3 = s1.left(4); : extracts the leftmost 4 characters of s1
s3 = s1.right(2); : extracts the rightmost 2 characters of s3
cout << s1[2]; : extracts the 3rd character of sd
s4 = s2 = s1 : assigns s4 = s1 and s3 = s2
. You will need to a code constructor(s), a copy constructor, an assignment operator, and a
destructor.
. Allow complex numbers to be piped to/from standard input
and file streams
. Test your class
(4) Design an Input class, one that allows "tokens" to be read from standard input or a file. A
token is a series of characters delimited by white space, (or a userdefined delimiter).
Here is some sample code which describes the usage of your class:
// Usage 1
Input i1(","); // define a keyboard input source that sees commas as delimiters
Dstring firstName,lastName;
cout << "Enter your first name,last name" << endl;
i1 >> firstName >> lastName;
cout << lastName << "," << firstName << endl;
...
// Usage 2
const char *INPUT_FILE = "test.dat";
ifstream inputFile(INPUT_FILE);
if (!inputFile) exit(0);
Input i2(inputFile); // define a file input source that defaults to white space as delimiters
Dstring token;
i2 >> token;
while (!i2) // write out each token in <inputFile> until the end of file
{
cout << token << ",";
i2 >> token;
}
...
(5) Users of the Student class wish you as its designer to
extend its capabilites. Recording a series of marks and calculating the average mark is not
enough details on a student's performance. They wish to record a series of subject descriptions
and corresponding grades for each subject
and have the class calculate the average internally. As well,
the class should describe the final state of a student's
performance based on the overall average. The possibilities are:
0 49 Failure
50 55 Academic probation
56 65 Pass
66 75 Median
76 89 Honours
90 100 First class honours
Students may take up to ten courses each semester. Study the
following theoretical output of a student's data.
Martha jones Age: 18 Overall avg: 82% Bottom line: honours
Physics 82
English 77
Geography 91
Latin 82
Revise the Student class to support this functionality. Decide upon how a Student object should
be constructed. Suggested:
Student::Student(name,age);
Student::addASubject(description,mark);
Deriving classes
Once we have designed a class, we can derive another class from it to create a composite or
subclass, something similar but with additional data and/or functionality. A composite describes
an "IS A" relation with the source or base class.
For example, we already have a Student class defined. A university student IS A student but with
some extra fields: nYears, typeOfProgram (honours/general), fieldOfStudy (English,
Mathematics,...), tuition, and lastYearsAverage:
| Student | | UniversityStudent |
| name | < | year |
| marks[] | | typeOfProgram |
| fieldOfStudy |
| tuition |
| lastYearsAverage |
A UniversityStudent IS A Student.
A UniversityStudent HAS A year,typeOfProgram,fieldOfStudy,...
General derivation syntax:
The C++ syntax for creating a derived class is:
class CompositeClass : <scope> BaseClass
{ // ...definition };
where there are three <scope> possibilities : public, private, or protected :
public, (most commonly used), means public members of base class become public members of
derived class and protected members of the base class becom protected members of the derived
class.
The other two scopes are rarely used; the IS A condition is lost:
protected means public and protected members of base class become protected members of the
derived class.
private means public and protected members of the base class become private members of the
derived class.
* Class members declared as protected like private, remain hidden to outside users of the class
with the exception of class designers who derive from the class.
Deriving UniversityStudent from Student
We can create a special UniversityStudent instance description of Student which maintains all the
data and functionality at the Student level by coding:
class UniversityStudent : public Student
{
private:
<UniversityStudent fields>
public:
<universityStudent user functions>
};
How is the base class Student part initialized?
A/ Via UniversityStudent() constructors. They can propogate fields down into the Student class
by calling a Student constructor in the initializer list:
UniversityStudent(...) : Student(...),... {}
OR by simply omitting a Student(..) constructor and let the compiler call the default Student()
constructor. Of course this is only possible if a default Student() constructor exists:
UniversityStudent(...) : ... {}
Revised Student class (STUDENT2.H)
#ifndef STUDENT2H
#define STUDENT2H
#include <iostream.h>
#include <string.h>
const int MAX_STUDENT_NAME_CHARS = 40;
const int MAX_STUDENT_MARKS = 20;
const int NPROGRAMS = 2;
class Student
{
private:
char name[MAX_STUDENT_NAME_CHARS];
int marks[MAX_STUDENT_MARKS];
int nMarks;
public:
Student();
Student(char*,int*,int);
void edit(char *,int*,int);
int average();
const char* getName();
const int* getMarks();
friend ostream& operator<<(ostream& o,Student&);
};
#endif
STUDENT2.CPP
#include "student2.h"
#include <string.h>
#include <iostream.h>
#define min(a,b) (a < b ? a : b)
Student::Student()
{
name[0] = NULL;
nMarks = 0;
}
Student::Student(char *_name,int *_marks,int _nMarks)
{
edit(_name,_marks,_nMarks);
}
void Student::edit(char *_name,int *_marks,int _nMarks)
{
strncpy(name,_name,min(strlen(_name)+1,MAX_STUDENT_NAME_CHARS));
for (int i = 0; i < _nMarks; i++)
marks[i] = _marks[i];
nMarks = _nMarks;
}
ostream& operator<<(ostream& o,Student& s)
{
o << "Name " << s.name << endl;
o << "Marks ";
for (int i = 0; i < s.nMarks; i++)
o << s.marks[i] << " ";
return o;
}
const char* Student::getName()
{
return name;
}
const int* Student::getMarks()
{
return marks;
}
int Student::average()
{
long int total = 0;
for (int i = 0; i < nMarks; i++)
total += marks[i];
return (total/nMarks);
}
UniversityStudent definition (USTUDENT.H)
#ifndef USTUDENTH
#define USTUDENTH
#include "student2.h"
enum {FIRST_YEAR=1,SECOND_YEAR,THIRD_YEAR,FOURTH_YEAR};
enum {HONOURS,GENERAL};
#define MAX_DESC_CHARS 30
#define STANDARD_LAST_YEARS_WEIGHTING 0.3
class UniversityStudent : public Student
{
private:
int year;
int typeOfProgram;
char fieldOfStudy[MAX_DESC_CHARS];
int tuition;
int lastYearsAverage;
static char *programs[NPROGRAMS];
public:
UniversityStudent();
UniversityStudent(char*,int*,int,
int,int,char*,int,int);
friend ostream& operator<<(ostream&,UniversityStudent&);
int average(float lastYearsWeight = STANDARD_LAST_YEARS_WEIGHTING);
};
#endif
USTUDENT.CPP
#include "ustudent.h"
// automatically calls default constructor for Student
UniversityStudent::UniversityStudent()
: year(FIRST_YEAR),typeOfProgram(GENERAL),tuition(2500),lastYearsAverage(0)
{
strcpy(fieldOfStudy,"unknown");
}
// In this constructor, we explicitly invoke the base class Student(..) constructor
UniversityStudent::UniversityStudent(char *_name,int *_marks,int _nMarks,
int _year,int _typeOfProgram,char *_fieldOfStudy,int _tuition,int _lastYearsAverage)
: Student(_name,_marks,_nMarks),
year(_year),typeOfProgram(_typeOfProgram),tuition(_tuition),
lastYearsAverage(_lastYearsAverage)
{
strcpy(fieldOfStudy,_fieldOfStudy);
}
// Compute a weighted average of lastYearsAverage
// Use a default weighted average of <lastYearsWeight>
// in favour of <lastYearsAverage>
int UniversityStudent::average(float lastYearsWeight)
{
int currentAverage = Student::average();
float overallAverage = (1lastYearsWeight)*(float)currentAverage
+ lastYearsWeight*(float)lastYearsAverage;
int result = (int)overallAverage;
return (result);
}
ostream& operator<<(ostream& o,UniversityStudent &u)
{
// To display the Student part of u, we must cast u to an object of type Student
// and invoke operator(ostream&,Student&)
o << *(Student*)&u << endl;
// Display <u>'s particulars
o << "Year " << u.year
<< "," << u.programs[u.typeOfProgram]
<< "," << u.fieldOfStudy
<< "," << "Tuition " << u.tuition
<< "," << "last years avg " << u.lastYearsAverage;
return o;
}
char* UniversityStudent::programs[] = {"Honours","General"};
UTEST.CPP
#include <iostream.h>
#include "ustudent.h"
void main()
{
int kellysMarks[] = {56,78,65,67,77};
UniversityStudent s1("Kelly",kellysMarks,5,
2,HONOURS,"Accounting",2200,50);
cout << s1 << endl;
cout << s1.getName() << "'s weighted Average(default) =" << s1.average() << endl;
cout << s1.getName() << "'s weighted Average(20%,80%) =" << s1.average(0.2) << endl;
}
Output
Name Kelly
Marks 56 78 65 67 77
Year 2,Honours,Accounting,Tuition 2200,last years avg 50
Kelly's weighted Average(default) =62
Kelly's weighted Average(20%,80%) =64
Dynamic binding
If we have a bunch of Students and UniversityStudents in a list, how can we process them without
having to decode the type? This is a good question, for if we execute the following code, we
don't get the results we would have hoped for!
#include <iostream.h>
#include "ustudent.h"
const int NSTUDENTS = 2;
void main()
{
int mannysMarks[] = {88,90,52,55};
Student s1("Manny",mannysMarks,4);
int verasMarks[] = {70,71,85,88,87,86};
UniversityStudent u1("Vera",verasMarks,6,
3,GENERAL,"Biology",1900,90);
Student *sa[] = {&s1,&u1};
for (int i = 0; i < NSTUDENTS; i++)
{
Student *s = sa[i];
cout << *s << endl;
cout << s>getName()
<< "'s weighted Average(default) =" << s>average()
<< endl << endl;
}
}
Output
Name Manny
Marks 88 90 52 55
Manny's weighted Average(default) =71
Name Vera
Marks 70 71 85 88 87 86
Vera's weighted Average(default) =81
Note that Vera's average was calculated the simple "Student" way without a weighted average,
even though she IS A university student!
Virtual functions
There are several problems with the last example. Firstly, Students are all cast implicitly to
Student *s so that when we call a member function of s, (namely average and operator<<), the
default mechanism is to call the function at the exact object type level of s which is obviously
Student.
We can override the default behaviour by prefixing functions with the virtual keyword. This
tells the compiler to get the exact type of the object at run time. Then it calls the function at this
level if it exists. If not, is searches one derived level down working towards the base class to find
a matching function. Such action is called dynamic binding.
| Student |
| virtual average() |
/ \
/ \
| UniversityStudent | | NightSchoolStudent |
| average() | | |
/ \
/ \
| GraduateStudent | | PHDStudent |
| average() | | average() |
In the above derivation chart, all classes have there own specific average() calculation except a
NightSchoolStudent who shares the average() calculation with a regular Student.
Also note that for virtual binding to work, the signatures of the common function must be exactly
the same as declared in the base class. For this reason we must remove the default parameter
lastYearsWeight in UniversityStudent::average() for the dynamic binding to work.
A virtual display() function
We can't rely on the operator<< function to display virtually any type of student because only
member functions can be declared virtual, and friend ostream& operator<<(..) is not a member
function.
We have to write a virtual display() function to get the right output. friend ostream& and
display() can share the same code, we just have two interfaces now:
| Student |
| virtual void display(ostream&); |
| friend ostream& operator<<(..); |
| ... |
| UniversityStudent |
| virtual void display(ostream&); |
| friend ostream& operator<<(..); |
| ... |
Revised STUDENT2.H
#ifndef STUDENT2H
#define STUDENT2H
#include <iostream.h>
#include <string.h>
const int MAX_STUDENT_NAME_CHARS = 40;
const int MAX_STUDENT_MARKS = 20;
const int NPROGRAMS = 2;
class Student
{
protected: // We make this protected so that derived class UniversityStudent
// can access the private members of Student
char name[MAX_STUDENT_NAME_CHARS];
int marks[MAX_STUDENT_MARKS];
int nMarks;
public:
Student();
Student(char*,int*,int);
void edit(char *,int*,int);
virtual int average();
virtual void display(ostream&);
const char* getName();
const int* getMarks();
friend ostream& operator<<(ostream& o,Student&);
};
#endif
STUDENT2.CPP
...
ostream& operator<<(ostream& o,Student& s)
{
s.display(o);
return (o);
}
void Student::display(ostream& o)
{
o << "Name " << name << endl;
o << "Marks ";
for (int i = 0; i < nMarks; i++)
o << marks[i] << " ";
}
int Student::average()
{
long int total = 0;
for (int i = 0; i < nMarks; i++)
total += marks[i];
return (total/nMarks);
}
Revised USTUDENTH
#ifndef USTUDENTH
#define USTUDENTH
#include "student2.h"
...
class UniversityStudent : public Student
{
private:
int year;
int typeOfProgram;
char fieldOfStudy[MAX_DESC_CHARS];
int tuition;
int lastYearsAverage;
static char *programs[NPROGRAMS];
public:
void display(ostream&);
UniversityStudent();
UniversityStudent(char*,int*,int,
int,int,char*,int,int);
friend ostream& operator<<(ostream&,UniversityStudent&);
int average();
};
#endif
Revised USTUDENT.CPP
ostream& operator<<(ostream& o,UniversityStudent &u)
{
u.display(o);
return o;
}
void UniversityStudent::display(ostream &o)
{
// o << *(Student*)this << endl; // This causes a stack overflow
// because via virtual, it reinvokes
// UniversityStudent::display(ostream&)
// To avoid repeating the Student::display() code,
// we have to create yet another version
// of Student::display, call it Student::displayCoreFields()
// with no virtual keyword and call that from here!
o << "Name:" << getName() << endl;
o << "Marks:" << endl;
for (int i = 0; i < nMarks; i++)
o << marks[i] << " ";
o << endl;
o << "Year " << year
<< "," << programs[typeOfProgram]
<< "," << fieldOfStudy
<< "," << "Tuition " << tuition
<< "," << "last years avg " << lastYearsAverage;
int UniversityStudent::average()
{
float lastYearsWeight = STANDARD_LAST_YEARS_WEIGHTING;
int currentAverage = Student::average();
float overallAverage= (1lastYearsWeight)*(float)currentAverage
+ lastYearsWeight*(float)lastYearsAverage;
int result = (int)overallAverage;
return (result);
}
Test code (DBIND.CPP)
#include <iostream.h>
#include "ustudent.h"
// Simulate processing a family of Student objects loaded into an array
const int NSTUDENTS = 2;
void main()
{
int mannysMarks[] = {88,90,52,55};
Student s1("Manny",mannysMarks,4);
int verasMarks[] = {70,71,85,88,87,86};
UniversityStudent u1("Vera",verasMarks,6,
3,GENERAL,"Biology",1900,90);
Student *sa[] = {&s1,&u1};
for (int i = 0; i < NSTUDENTS; i++)
{
Student *s = sa[i];
cout << *s << endl;
cout << s>getName() << "'s weighted Average(default) ="
<< s>average() << endl << endl;
}
}
Output
Name Manny
Marks 88 90 52 55
Manny's weighted Average(default) =71
Name:Vera
Marks:
70 71 85 88 87 86
Year 3,General,Biology,Tuition 1900,last years avg 90
Vera's weighted Average(default) =83
A musical instrument derivation hierachy
| Musical Instrument |
/ | \
Woodwind Brass String
| \ / \ / \
| \ / \ / \
Clarinet Saxsophone Trumpet Tuba Guitar Violin
/ \
/ \
Electric Acoustic
| |
Ibanez Samick
Multiple inheritance
In more complex programs, we may wish to derive a class from more than one counterpart. An
AND condition describes multiple inheritance.
Theoretically:
| X | | Y |
\ / Z is an X and a Y
\ /
| Z |
Practically:
| Student | | Person | | Person |
\ / OR |
\ / |
| UniversityStudent | | Student |
|
|
| UniversityStudent |
In both cases, UniversityStudent is a Student and a Person
Actually, the second derivation chart may be a more realistic model since we have more concise
definition of Student as Student being a Person.
Regardless, they are both examples of multiple inheritance.
Memory map of multiple inheritance
To create an AND derived relation in C++, we simple append the extra class(es) after the colon
operator separated by commas. We could code the first model as:
class UniversityStudent : public Student,Person
{ ... };
Suppose a Person has a birthDate and a countryOfBirth. Then creating an object u of type
UniversityStudent yields something like the following in memory:
u
"Person" | birthDate |
part | countryOfBirth |
| |
| name |
"Student" | marks[] |
part | |
All data and functional members of Student and Person are available at the UniversityStudent
level. We can write:
UniversityStudent u(...);
cout << u.getName() << u.average() << u.getBirthdate() << endl;
* Note if we opt for this design, we may decide to move the name field from the Student class to
the Person class since it makes more sense groupwise to say that every Person has a name,
birthdate and countryOfBirth.
Inheritance versus embedding
Another way to achieve the same results is to embed instances of our derived classes inside the
subclass.
class UniversityStudent
{
public:
Student s; // Here, we are saying that a UniversityStudent
Person p; // has Student and Person counterparts
...
};
This works but the syntax is clumsier. Suppose we want to access the Student or Person
components of u, a UniversityStudent object,
UniversityStudent u(..);
cout << u.s.getName() << u.p.getBirthdate() << endl;
IS A is a stronger relation than HAS A, so if we recognize an IS A relation, we should not kludge
it with HAS A condition(s).
Virtual base classes
Sometimes we may wish to avoid loading in a . In some multiple inheritance schemas we may
accidentally include two copies of a base class. Consider a chess piece organization which
derives queen properties from both the bishop and rook properties:
Chess piece derivation chart
| ChessPiece |
/ | | | \
/ | | | \
Pawn Bishop Rook Knight King
\ /
\ /
Queen
The Queen has two routes back to her base class ChessPiece which means that two copies of
ChessPiece members would exist! Really, we don't want this we only want one instance of
ChessPiece loaded.
The solution is to inherit a base class that has danger of being multiply included as virtual:
class Bishop : public virtual ChessPiece {...};
class Rook : public virtual ChessPiece {...};
class Queen : public Rook,Bishop {...};
Queen(nonvirtual Rook/Bishop) Queen(virtual Rook/Bishop)
|RookPart ChessPiece| ==> | RookPart ChessPiece|
|BishopPart ChessPiece| | BishopPart |
Pointers to member functions
Pointers to C++ member functions work the same way as C function pointers except that we can
only access the function pointer through a specific object of that class.
Suppose we wanted to create a pointer to the UniversityStudent::display(..) function and invoke it
indirectly via the pointer. We can code:
UniversityStudent u1(...);
void (UniversityStudent::*pf)(ostream&) = u1.display;
(u1.*pf)(cout);
or we could initialize the pointer without having an actual object handle using the scope operator:
UniversityStudent u1(...);
void (UniversityStudent::*pf)(ostream&) = &UniversityStudent::display;
(u1.*pf)(cout);
To load an array of member functions...
First we must have a consistency in member function signature. Suppose we have generic class
member functions on class X: input(), edit(), and display(), all returning no argument and
accepting no arguments. Then we can create an array of pointers to the three member functions.
A typedef simplifies the syntax:
const int NFUNCTIONS = 3;
enum {INPUT,EDIT,DISPLAY};
typedef void (X:*PVF)();
X x;
PFV fa[NFUNCTIONS] = {&X::input,&X::edit,&X::display};
(x.*fa[INPUT])(); // invoke x.input()
(x.*fa[EDIT])(); // invoke x.edit()
(x.*fa[DISPLAY])(); // invoke x.display()
Assertions
In building code for the first time or attempting to track down bugs, sometimes it is useful to
insert assertion patches into a program.
If a particular condition isn't met which should have been, a program can unconditionally abort
with a message describing a failed condition, a source file and a line number.
Suppose we should never expect that the name field of a record should be blank when processing
a Record. We can guarantee this statement with an assert clause, assert(<condition>), which
says: if <condition> isn't met, then terminate.
#include <assert.h>
#include <string.h>
void processRecord(Record r)
{
assert (strlen(r.name) != 0);
...
}
Output (on the error condition of a blank name field)
Assertion failed: strlen(r.name) != 0, file assert.h, line 6
Exceptions
C++ provides the ability for programmers to provide their own exception handling. An
exception, or program error, is said to be thrown by a function and caught by the calling code.
The three C++ keywords necessary to create and trap exceptions are throw, try and catch. Here
is an example:
try
{
// some dubious code
f();
}
catch (ExceptionType1 &e1) { // error handler for error type 1 }
...
catch (ExceptionTypeX &eX) { // error handler for error type x }
void f()
{
...
if (error condition of type 1)
throw ExceptionType1;
...
else if (error condition of type x)
throw ExceptionTypeX;
}
ExceptionType can be as simple as a string description of the problem to a specialized exception
class.
Sample exception code (EXCEPT.CPP)
#include <iostream.h>
class DivideByZeroException
{
public:
DivideByZeroException() : description("Division by zero error") {}
friend ostream& operator<<(ostream &o,DivideByZeroException &e)
{
o << e.description;
return(o);
}
char *description;
};
// Function that throws two possible exceptions:
// (1) a simple char* exception and
// (2) a specialized DivideByZeroException
int f(int index,int denominator)
{
if ((index >= 10) || (index < 0))
throw "index out of range";
else if (denominator == 0)
throw DivideByZeroException();
return (index/denominator);
}
void main()
{
// Test const char* exception...
try
{
cout << f(9,3) << endl; // succeeds
cout << f(10,2) << endl; // generates a const char* exception
}
catch (const char* s) { cout << s << endl; }
catch (DivideByZeroException &e) { cout << e << endl; }
// Test DivideByZeroException...
try
{
cout << f(9,0) << endl; // generates as DivideByZeroException
cout << f(10,4) << endl; // never is executed
}
catch (const char* s) { cout << s << endl; }
catch (DivideByZeroException &e) { cout << e << endl; }
}
Output
3
index out of range
Division by zero error
When to use exceptions
(1) When it does not make sense for the function to handle its own error(s).
(2) To maintain uniform error processing in team projects.
(3) For widelyused code such as that found in libraries. It is easier for a user to put a try block
around a series of library calls rather than checking the return code of each function for possible
error.
Tips:
Don't go overboard on exceptions. You, or some other programmer, has to ultimately provide the
error handling code to process them when they are thrown!
Decide when exception processing is overkill. Perhaps a simple int error code suffices.
Part III Exercises
(1) What is the difference between an IS A relation and a HAS A relation? How do we describe
each relation syntactically in C++?
(2) Various shapes can be said to derive from a generic Shape class. Every Shape consists of a
Color and a Centre, a Color being an integer, a Centre a Point. Consider Circle as being a
special instance of an Oval, Square a special instance of a Rectangle. Simulate virtual draw()
functions for all shapes in the derivation tree below. Each draw function should display the
geometry attributes of that shape. For example, Square::draw() should display the length of a
side as well as the core Shape information.
Shape
/ \
/ \
Oval Rectangle
| |
| |
Circle Square
(3) Create a N x M character Screen object on which Shapes can be drawn. Revise your virtual
draw() functions to display directly onto a Screen object, the syntax being:
Screen screen(80,50); // An 80 x 50 character screen
Square square(Centre(40,25),SQUARE_LENGTH,GREEN);
square.draw(screen); // writes '*' characters to screen
...
screen.display(); // outputs the screen matrix to standard output
ofstream of("Screen.out");
screen.display(of); // outputs the screen matrix to file stream <of>
Generate an OffScreen exception if a draw() command writes outside the screen boundaries. Test
your exception trigger by catching it from your test mainline.
(4a) A middleman appliance retailer wishes to have a computer
system that will record and report on their inventory. Analysts
have come up with the following inheritance diagram:
Appliance
|>Name
|>numberOfUnits
|>Wholesale cost
|>Retail price
|>Distributor
|>Date purchased
/\ /\
|| ||
HouseholdAppliance OfficeAppliance
markup() markup()
Household appliances are items such as fridges, stoves, and
toaster ovens, etc. Office appliances are items such as tables,
phones, and computers, etc.
The calculation for retail price depends on the Appliance type
and when the appliance was purchased. Office appliances have a
2% markup over and above the household appliance markup of 20%.
The markup goes down a percentage for every month the appliance
has been sitting in stock, with a bottom limit of an 12%
markdown.
Construct the classes.
Code default constructors and full constructors for your classes.
Populate an array with some hypothetical inventory.
Write a function which produces a report somewhat like:
Date: 1997 07 18
Item # units wholesale retail markup purchaseDate overhead
Fridge 4 $ 400 $ 600 20% 1997 07 08 $1600
Computer 2 $1200 $1428 19% 1997 04 01 $2400
Desk 10 $ 300 $ 450 15% 1997 02 21 $3000
Total retail overhead $ 9756
Total wholesale overhead $ 7000
(4b) Allow the user to edit the markup percentages and time factor markup depreciation ramp
and rerun the report.
Suggestion: create a helper class which allows editing of the parameters pertinent to the
application.
(5) The game of chess involves pieces of various types that have
various movement patterns. The pieces are
{PAWN,KNIGHT,BISHOP,KING,ROOK, QUEEN}. All these pieces can be
derived from an abstract class called Piece. Each piece moves
differently and can be queried for its squares of movement by
calling the function getMoves(). Here is a sample board:
8 | BR BN BK | W = white, B = Black
7 | BP BB BB | K = king, P = pawn, B = bishop
6 | BP | N = knight, R = Rook, Q= queen
5 | BP BR BP |
4 | WP BP | For ex, WN at Position(B,3)
3 | WN WR WP WP WP | has the following squares to
2 | WB | move to:
1 | WK WQ | (A,1),(C,1),(D,4),(D,2),(C,5)
(A,5)
A B C D E F G H
* Note that a queen's movement can be monitored by sending a
getMoves() message to a Bishop object and a Rook object of the
same color at the same square.
Class diagram:
Piece < Rook
|name Position[] getMoves()
|position < Bishop
|colour Position[] getMoves()
...
Position
|row
|column
Color: [WHITE,BLACK]
Board
|8x8 array of pieces
Instantiate a board and populate it with the above sample setup.
Display the board, then let the user ask where any piece on the
board can be moved to:
Sample run:
Enter piece position => B3
White knight at B3 can move to {A1, C1, D4, D2, C5, A5}
Enter piece position => H1
?? No piece at H1
Enter piece position =>
C++ Data structures
Having learned the basics of C++ we are ready to tackle building our own data structure. Let's
start by converting our Clinked list implementation to a C++ OO implementation.
The messy part of the C implementation of the linked list is the need to pass a pointer to a
comparison function. We can eliminate this ugliness in our C++ version by recommending
objects to provide their own comparison feature.
Specifically a user can pass an object to add to the data structure, and the data structure code can
ask this object to compare itself with another.
To do this though, we will have to create an object base or abstract base class from which
addable objects to our data structures can derive from. In so doing, we can maintain a family of
addable objects and can rely on the virtual mechanism of the language to call the comparison
function of the type of object the data structure is working on.
DATA STRUCTURE APPLICATION CODE
| . > ObjectA |
| | | compare() |
| | | |
| . > ObjectB |
| | | compare() |
| | | |
| ... | | ... |
| | | |
| | | |
| . > ObjectX |
| | | compare() |
An abstract base class
An abstract base class cannot be instantiated; it is as described, abstract, it exists only in theory.
The purpose of such a beast is to provide a template or shell description of objects that must
follow a certain guideline.
In a nutshell, as designers of the linked list data structure, we are going to say:
Dear application designer:
If you wish to add objects to my data structure, you must derive from my abstract base
class and I'm going to force you to code an comparison function otherwise your class won't
compile.
In general, an abstract base class can define any number of data or functional members, but
functional members that are not defined are labelled as null via an = 0 declaration:
class AbstractBaseClass
{
public:
void f1() = 0; // The deriver promises to define
}; // f1()
| X |
| |
| void f1() {...} |
/
/
|AbstractBaseClass |
| |
\
\
| Y |
| |
| void f1() {...} |
An abstract Object definition
class Object
{
public:
virtual int compare(const Object&) = 0;
};
Suppose a user wants to populate our data structure with InventoryRecords. Then the user
derives from Object and provides a compare function describing how to compare
InventoryRecords.
class InventoryRecord : public Object
{
public:
...
int compare(const Object& o) {...}
};
| Object | < | InventoryRecord |
| | | int compare(...) |
OBJECT.H
#ifndef OBJECTH
#define OBJECTH
class Object
{
public:
virtual int compare(const Object&) = 0;
};
#endif
LL.H
#ifndef LLH
#define LLH
#include "std.h"
#include "object.h"
/* forward declare the LLNode class for the recursive definition that follows */
class LLNode;
/* LLNode storage class */
class LLNode
{
public:
LLNode(Object *_data,LLNode *_next)
{ data = _data; next = _next; }
LLNode()
{ data = 0; next = 0; }
Object *data;
LLNode *next;
};
class LinkedList
{
public:
LinkedList(int ownsElements = FALSE);
~LinkedList();
void begin();
LLNode *next();
LLNode *cur();
void add(Object &o,LLNode *insertAfterNode=0);
LLNode *search(Object &o);
int remove(Object &o);
void setOwnership(int _ownsElements)
{ ownsElements = _ownsElements; }
int getOwnership() { return (ownsElements); }
private:
LLNode *head;
LLNode *current;
int ownsElements;
LLNode* find(Object &data,LLNode **previousNode);
};
#endif
LL.CPP
#include <stdlib.h>
#include "ll.h"
LinkedList::LinkedList(int _ownsElements)
{
head = 0;
current = head;
ownsElements = _ownsElements;
}
LinkedList::~LinkedList()
{
LLNode *node,*nextNode;
node = head;
while (node)
{
if (ownsElements)
delete node>data;
/* important to get next node before freeing <node>! */
nextNode = node>next;
delete node;
node = nextNode;
}
}
void LinkedList::begin()
{
current = 0;
}
LLNode* LinkedList::cur()
{
return(current);
}
LLNode* LinkedList::next()
{
/* CASE 1: get first element in list */
if (!current)
{
current = head;
return(current);
}
/* CASE 2: get subsequent elements in list */
current = current>next;
return(current);
}
void LinkedList::add(Object &data,LLNode *insertAfterNode)
{
LLNode *newNode;
/* Create the new node */
newNode = new LLNode;
newNode>data = &data;
if (!insertAfterNode)
{
/* CASE 1: insert element at start of list */
newNode>next = head;
head = newNode;
}
else
{
/* CASE 2: adding after the first element */
newNode>next = insertAfterNode>next;
insertAfterNode>next = newNode;
}
/* Make the added node the current one */
current = newNode;
}
LLNode* LinkedList::find(Object &data,LLNode **previousNode)
{
LLNode *currentNode;
begin();
*previousNode = 0;
while (currentNode = next())
{
if (data.compare(*currentNode>data) == EQUAL)
{
current = currentNode;
return(currentNode);
}
*previousNode = currentNode;
}
return(0);
}
LLNode* LinkedList::search(Object &data)
{
LLNode *previousNode;
return(find(data,&previousNode));
}
int LinkedList::remove(Object &data)
{
LLNode *nodeToDelete,*previousNode;
nodeToDelete = find(data,&previousNode);
if (!nodeToDelete)
return(FALSE);
if (!previousNode)
{
/* CASE 1: delete element at start of list, update the head pointer */
head = nodeToDelete>next;
}
else
{
/* CASE 2: delete an element after the first element,
patch a previous pointer */
previousNode>next = nodeToDelete>next;
}
if (ownsElements)
delete nodeToDelete>data;
delete nodeToDelete;
/* make the previous node current */
current = previousNode;
return(TRUE);
}
LLTEST.CPP
#include "ll.h"
#include <stdio.h>
#include <stdlib.h>
#include <iostream.h>
class Data : public Object
{
public:
int a;
Data(int _a = 0) :a(_a) {}
int compare(const Object &o)
{
Data &data = *(Data*)&o;
if (data.a == a)
return(EQUAL);
else if (data.a < a)
return (LESS_THAN);
else
return (GREATER_THAN);
}
friend ostream& operator<<(ostream &o,Data &d)
{
o << d.a;
return (o);
}
};
void displayData(LinkedList &llist)
{
LLNode *node;
Data *element;
llist.begin();
while (node = llist.next())
{
element = (Data*)node>data;
cout << *element << " ";
}
cout << endl;
}
#define NELEMENTS 6
void main()
{
int i,data[NELEMENTS] = {9,4,3,2,99,88};
LLNode *node;
// Turn ownership of elements on for <llist>
// This means <llist> performs garbage collection explicitly
// when items are removed from <list>
LinkedList llist;
llist.setOwnership(TRUE);
for (i = 0; i < NELEMENTS; i++)
{
Data &d = *new Data(data[i]);
llist.add(d);
displayData(llist);
}
Data three(3),minusFour(4),eightyEight(88);
Data& oneHundred = *new Data(100);
node = llist.search(three);
if (node)
cout << "Element < " << *(Data*)node>data << " > found" << endl;
llist.add(oneHundred,node);
cout << "Element < " << oneHundred << " > added after < "
<< three << " >" << endl;
displayData(llist);
node = llist.search(minusFour);
if (!node)
cout << "Element 4 not found" << endl;
if (llist.remove(eightyEight))
cout << "Element < " << eightyEight << " > removed" << endl;
displayData(llist);
if (llist.remove(three))
cout << "Element < " << three << " > removed" << endl;
displayData(llist);
}
Output
9
4 9
3 4 9
2 3 4 9
99 2 3 4 9
88 99 2 3 4 9
Element < 3 > found
Element < 100 > added after < 3 >
88 99 2 3 100 4 9
Element 4 not found
Element < 88 > removed
99 2 3 100 4 9
Element < 3 > removed
99 2 100 4 9
Templates
C++ provides the ability to parameterize data types at compile time. This is a useful if a class
wishes to work on any user defined type without having to code internally a generic void* data
type and perform cumbersome casts.
For example, suppose we wanted to create a class which would store an array of data of arbitrary
type. Normally we would have to code something like:
class Array
{
private:
int size;
void **arrayData;
public:
Array(int _size = 10) {...};
void* operator[](index) { return(arrayData[index]); }
...
};
We can replace void* with a type variable inside the angle brackets.
Inline template definition (template.cpp)
#include <iostream.h>
#include <stdlib.h>
template<class Type>
class Array
{
private:
int size;
Type *arrayData;
public:
Array(int _size = 10)
{
size = _size;
arrayData = new Type[size];
}
Type& operator[](int index) { return(arrayData[index]); }
int getSize() { return (size); }
~Array() { delete arrayData; }
};
void main()
{
Array<int> a(3);
a[0] = 67;
a[1] = 43;
a[2] = 102;
for (int i = 0; i < a.getSize(); i++)
cout << a[i] << " ";
cout << endl;
}
Template header file definition
The complete template definition must appear in the header file. This includes function
definitions outside the class. The reason being that the functions are generated at compiletime
when a type is bound to the array via:
Array<OfWhateverType> a(SIZE);
Only when the compiler has loaded a definition of the functions via #include array.h can it
generate the functions. If they are not there, no functions are generated and undefined symbol
errors occur at link time.
The syntax for outofline template functions for class X with a parametrized type T is:
template<class T>
[returnType] X<T>::[functionName](parameters) {...}
ARRAY.H
#ifndef ARRAYH
#define ARRAYH
template<class Type>
class Array
{
private:
int size;
Type *arrayData;
public:
Array(int _size = 10);
Type& operator[](int index);
int getSize();
~Array();
};
template<class Type>
Array<Type>::Array(int _size)
{
size = _size;
arrayData = new Type[size];
}
template <class Type>
Type& Array<Type>::operator[](int index)
{
return(arrayData[index]);
}
}
template <class Type>
int Array<Type>::getSize()
{
return (size);
}
template <class Type>
Array<Type>::~Array()
{
delete arrayData;
}
#endif
ARRAYTST.CPP
#include <iostream.h>
#include <string.h>
#include "array.h"
class InventoryRecord
{
private:
int key;
int nUnits;
char description[10];
public:
InventoryRecord() {}
InventoryRecord(int _key,int _nUnits,char *_description)
: key(_key),nUnits(_nUnits)
{
strcpy(description,_description);
}
friend ostream& operator<<(ostream &o,InventoryRecord &r)
{
o << r.key << "," << r.nUnits << "," << r.description;
return o;
}
};
void main()
{
InventoryRecord inv1(1,10,"pail"),inv2(2,4,"shovel"),inv3(3,40,"trowel");
Array<InventoryRecord> a(3);
a[0] = inv1;
a[1] = inv2;
a[2] = inv3;
for (int i = 0; i < a.getSize(); i++)
cout << a[i] << endl;
}
Multiple template types
C++ does not restrict us to parametrizing a class upon a single type. We can list as many class
parameters as we want inside the angle brackets, separated by commas.
Here is an example of a class that builds itself upon three types A,B,C:
template <class A,class B,class C>
class X
{
private:
A a;
B b;
C c;
public:
...
};
X x<A,B,C>; // Creates instance x based on the three types A,B,C
Templates in general...
Advantages:
. provides ability to create typegeneric code
. provides compact clean syntax
. avoids the necessity to create an abstract base class
from which the user must derive
. avoids the necessity to cast back from the void* type
Disadvantages:
. higher executable size overhead as the compiler generates code
for each type occurrence
For example if we code:
Array<int> a(3);
Array<InventoryRecord> ir(10);
then we have two instances of all the Array functions, a copy for the int type and a copy for the
InventoryRecord type.
At least with our void* implementation we had only one copy of the class functions for all
possible user types.
Linked list template implementation
This is an interesting data structure to attempt because we have two classes to template, the
LLNode class and the LinkedList class, both parameterized by a user <Type>.
template<class Type>
class LLNode {..};
template<class Type>
class LinkedList {...};
To minimize code changes required from the original C linked list version we will keep the
LLNode members public access even though it is better to make them private.
Incidentally, we could still have LLNode private members and avoid the fiasco of creating the
necessary public accessors and mutators getData(), setData(), getNext(), setNext() by making
LinkedList a friend of LLNode:
template<class Type>
class LinkedList
{
friend class LinkedList<Type>;
...
};
Recall that member functions of friend classes have access to the private members of their
friends.
LinkedList template (LLT.H)
#ifndef LLTH
#define LLTH
#include "std.h"
/* forward declare the LLNode class for the recursive definition */
template<class Type>
class LLNode;
/* LLNode storage class */
template<class Type>
class LLNode
{
public:
LLNode(Type *_data,LLNode *_next)
{ data = _data; next = _next; }
LLNode()
{ data = 0; next = 0; }
Type *data;
LLNode *next;
};
template<class Type>
class LinkedList
{
public:
LinkedList(int ownsElements = FALSE);
~LinkedList();
void begin();
LLNode<Type> *next();
LLNode<Type> *cur();
void add(Type &o,LLNode<Type> *insertAfterNode=0);
LLNode<Type> *search(Type &o);
int remove(Type &o);
void setOwnership(int _ownsElements)
{ ownsElements = _ownsElements; }
int getOwnership() { return (ownsElements); }
private:
LLNode<Type> *head;
LLNode<Type> *current;
int ownsElements;
LLNode<Type>* find(Type &data,LLNode<Type> **previousNode);
};
template<class Type>
LinkedList<Type>::LinkedList(int _ownsElements)
{
head = 0;
current = head;
ownsElements = _ownsElements;
}
template<class Type>
LinkedList<Type>::~LinkedList()
{
LLNode<Type> *node,*nextNode;
node = head;
while (node)
{
if (ownsElements)
delete node>data;
/* important to get next node before freeing <node>! */
nextNode = node>next;
delete node;
node = nextNode;
}
}
template<class Type>
void LinkedList<Type>::begin()
{ current = 0; }
template<class Type>
LLNode<Type>* LinkedList<Type>::cur()
{ return(current); }
template<class Type>
LLNode<Type>* LinkedList<Type>::next()
{
/* CASE 1: get first element in list */
if (!current)
{
current = head;
return(current);
}
/* CASE 2: get subsequent elements in list */
current = current>next;
return(current);
}
template<class Type>
void LinkedList<Type>::add(Type &data,LLNode<Type> *insertAfterNode)
{
LLNode<Type> *newNode;
/* Create the new node */
newNode = new LLNode<Type>;
newNode>data = &data;
if (!insertAfterNode)
{
/* CASE 1: insert element at start of list */
newNode>next = head;
head = newNode;
}
else
{
/* CASE 2: adding after the first element */
newNode>next = insertAfterNode>next;
insertAfterNode>next = newNode;
}
/* Make the added node the current one */
current = newNode;
}
template<class Type>
LLNode<Type>* LinkedList<Type>::find(Type &data,LLNode<Type> **previousNode)
{
LLNode<Type> *currentNode;
begin();
*previousNode = 0;
while (currentNode = next())
{
Type *nodeData = (Type*)currentNode>data;
if (data.compare(*nodeData) == EQUAL)
{
current = currentNode;
return(currentNode);
}
*previousNode = currentNode;
}
return(0);
}
template<class Type>
LLNode<Type>* LinkedList<Type>::search(Type &data)
{
LLNode<Type> *previousNode;
return(find(data,&previousNode));
}
template<class Type>
int LinkedList<Type>::remove(Type &data)
{
LLNode<Type> *nodeToDelete,*previousNode;
nodeToDelete = find(data,&previousNode);
if (!nodeToDelete)
return(FALSE);
if (!previousNode)
{
/* CASE 1: delete element at start of list, update the head pointer */
head = nodeToDelete>next;
}
else
{
/* CASE 2: delete an element after the first element,
patch a previous pointer */
previousNode>next = nodeToDelete>next;
}
if (ownsElements)
delete nodeToDelete>data;
delete nodeToDelete;
/* make the previous node current */
current = previousNode;
return(TRUE);
}
#endif
Code to test the templated link list (LLTTEST.CPP)
#include "llt.h"
#include <stdio.h>
#include <stdlib.h>
#include <iostream.h>
class Data
{
public:
int a;
Data(int _a = 0) :a(_a) {}
int compare(const Data &o)
{
Data &data = *(Data*)&o;
if (data.a == a)
return(EQUAL);
else if (data.a < a)
return (LESS_THAN);
else
return (GREATER_THAN);
}
friend ostream& operator<<(ostream &o,Data &d)
{
o << d.a;
return (o);
}
};
void displayData(LinkedList<Data> &llist)
{
LLNode<Data> *node;
Data *element;
llist.begin();
while (node = llist.next())
{
element = node>data;
cout << *element << " ";
}
cout << endl;
}
#define NELEMENTS 6
void main()
{
int i,data[NELEMENTS] = {9,4,3,2,99,88};
LLNode<Data> *node;
// Turn ownership of elements on for <llist>
// This means <llist> performs garbage collection explicitly
// when items are removed from <list>
LinkedList<Data> llist;
llist.setOwnership(TRUE);
for (i = 0; i < NELEMENTS; i++)
{
Data &d = *new Data(data[i]);
llist.add(d);
displayData(llist);
}
Data three(3),minusFour(4),eightyEight(88);
Data& oneHundred = *new Data(100);
node = llist.search(three);
if (node)
cout << "Element < " << *(Data*)node>data << " > found" << endl;
llist.add(oneHundred,node);
cout << "Element < " << oneHundred << " > added after < "
<< three << " >" << endl;
displayData(llist);
node = llist.search(minusFour);
if (!node)
cout << "Element 4 not found" << endl;
if (llist.remove(eightyEight))
cout << "Element < " << eightyEight << " > removed" << endl;
displayData(llist);
if (llist.remove(three))
cout << "Element < " << three << " > removed" << endl;
displayData(llist);
}
Part IV Exercises
(1a) Create a Stack class, that stores arbitrary objects on a stack. Recall that a stack works on a
LIFO basis (last infirst out). If we push() the elements A, B, C on the stack and pop() them out
one at a time, we get them back in the reverse order: C, B, A.
Here are the stack operations you should supply:
Stack();
void push(void*);
void* pop();
int isEmpty();
(b) Create a templated version of your class.
(c) Test your templated version by creating and populating a Stack of LotteryTickets from an
input file.
Stack<LotteryTickets> s;
Lottery tickets have a number, a purchaseDate and purchasePlace. Once your data structure is
loaded, try various combinations of push() and pop() commands to test its functioning.
(2a) Create a priority queue class. Recall that a queue operates on a FIFO, firstin firstout basis.
A priority queue can insert elements of higher priority ahead of others. For example, below we
have 4 elements of priorities 10,8,3, and 1, 1 being the highest priority and specifically the next
element to be popped off the queue. If we add an element of priority 5 then it should be inserted
at the appropriate place in the queue, namely between elements of priority 8 and 3:
tail> | 10 | <> | 8 | <> | 3 | <> | 1 | < head
/\
||
| 5 |
Implement your queue using a doublylinked list as the underlying data structure.
(2b) Test your PriorityQueue by simulating jobs queued for printing. The master scheduler
assigns priorities to print jobs based on user administration groupid. Managers and vice
presidents have highest priority. Regular employees have lowest priority. Superusers can dictate
their own priority to the master scheduler. Assume lookup tables of userid to admin group and
admin group to priority exists.
Here is a sample OO mainline. You can fill in the details of what a PrintJob, AssociationTable
and MasterScheduler should look like:
AssociationTable userLookup;
userLookup.add("Ned","superuser");
userLookup.add("Frank,"vice");
userLookup.add("Belinda","payroll");
...
PriorityQueue<PrintJob> printQueue;
MasterScheduler masterSheduler;
masterScheduler.assignUsers(userLookup);
PrintJob printJob1("monthlyReport.txt");
masterScheduler.addJob("Frank",printJob1
PrintJob printJob2("daemons.out");
masterScheduler.addJob("ned",printJob2,3);
PrintJob printjob3("doc.draft");
jobHandle lindasJobHandle = masterScheduler.addJob("linda");
...
masterScheduler.displayJobs();
masterScheduler.cancelJob(lindasJobHandle);
masterScheduler.displayJobs();
...
(3) Convert the following C data structure implementations over to C++ templatebased
implementations.
. DArray
. HashTable
. BinaryTree
(4a) Your Student class software from exercise 5 Part II, has gained popularity amongst many
institutions. Requests have been made for a Student manager package that will provide users to
manage all the students in the institution. Here is a list of the following requests:
Add a student
Delete a student
Search for a student
Edit a students marks
Display a students details
Display all students details
class StudentManager
{
public:
... add(...) {...}
... search(...) {...}
... delete(...) {...}
... edit(...) {...}
... display(...) {...}
... displayAll(...) {...}
private:
LinkedList<Student> sList;
// (or other data structure of your choice)
};
Design your class on paper and walk through the logic with a
series of test cases, before attempting the code. Write an
application that uses the StudentManager class to simulate the
tool described above. Test your code with a list of at least ten
students.
(4b) A second request has been asked of your StudentManager
package, that is to provide a record of attendance for each
student. It suffices to list only the student's absences:
Martha Jones ...
...
4 Absences:
02 22 97
02 27 97
03 02 97
03 10 97
Suggested class design:
class Student
{
...
private:
AttendanceRecord ar;
};
class AttendanceRecord
{
private:
LinkedList<Absence> aList[];
};
class Absence
{
...
};