PART 5
Interaction between
Classes
Header files
Inline functions
Function templates
Friends
Polymorphism
condit4.cpp
// CONDIT4.CPP: Inline functions
// But this approach may not work with some compilers
#include <iostream>
using namespace std;
inline int max (int a, int b) { return a > b ? a : b; }
main()
{
int x, y, z;
x = 1;
y = 5;
z = 0;
cout < "x = " < x < ", y = " < y;
cout < ", z = " < z < endl;
z = max(x,y);
cout < "x = " < x < ", y = " < y;
cout < ", z = " < z < endl;
return 0;
}
condit5.cpp
// CONDIT5.CPP: Inline functions defined in a header file
#include <iostream>
using namespace std;
#include "condit5.h"
main()
{
int x = 1, y = 5, z = 0;
cout < "x = " < x < ", y = " < y;
cout < ", z = " < z < endl;
z = max(x,y);
cout < "x = " < x < ", y = " < y;
cout < ", z = " < z < endl;
z = max(++x,++y);
cout < "x = " < x < ", y = " < y;
cout < ", z = " < z < endl;
return 0;
}
condit5.h
// CONDIT5.H: Inline function header file
#ifndef condit5
#define condit5
inline int max (int a, int b) { return a > b ? a : b; }
#endif
condit6.h
/* CONDIT6.H: Inline function header file for various
data types */
#ifndef condit6
#define condit6
inline int max (const int a, const int b)
{ return a > b ? a : b; }
inline double max (const double & a, const double & b)
{ return a > b ? a : b; }
inline long int max (const long int a, const long int b)
{ return a > b ? a : b; }
inline short int max (short int a, short int b)
{ return a > b ? a : b; }
// ...
#endif
condit7.h
// CONDIT7.H: Inline function template for all types
#ifndef condit7
#define condit7
template<class T> inline T max (const T & a, const T & b)
{ return a > b ? a : b; }
// works for any class that supports operator >
#endif
INLINE FUNCTIONS
C++ allows functions to be declared as "in-line" functions
In-line functions generate in-line code like a macro. This eliminates the "overhead" of a function call for short functions. Of course, the code is "duplicated" at each point of function call.
In-line functions in C++ can be overloaded, just like any other function.
In-line functions can only be defined in header files, not program files. This means that it is not only desirable but necessary to create separate header files.
Unlike macros, in-line functions require the specification of parameter types and results. This can lead to a long list of functions for the "standard" types.
Since version 3.0, C++ allows the creation of function templates so that a function is defined for every data type. Operator overloading can handle the behavior of user-defined types/classes. Alternatively, the exceptions to the pattern of the template can be called out with specific functions overloading the template.
enum.cpp
// ENUM.CPP: Examine the enumerated data type in C++
#include <iostream>
using namespace std;
main()
{
enum Gender { female, male, unknown = 4, couple };
// female=0, male=1, unknown=4, couple=5
enum Customer { yes, no };
// adding "unknown" would be an error
class Fruit
{
public:
enum Color { red, yellow, unknown };
Color fruitcolor;
};
Gender mary = female, mike = male;
Gender pat, ted_and_alice;
Fruit banana, apple;
pat = unknown;
ted_and_alice = couple;
banana.fruitcolor = Fruit::yellow;
apple.fruitcolor = Fruit::unknown;
cout < "Mary's gender = " < mary < endl;
cout < "Pat's gender = " < pat < endl;
cout < "Banana's color = " < banana.fruitcolor
< endl;
cout < "Apple's color = " < apple.fruitcolor
< endl;
return 0;
}
ENUMERATIONS
The global enumerated data type is treated as an integer data type, with, in effect, a series of #define's used to create the (integer) constants within the enumeration. No input/output facility is available for these enumeration other than the integer data handling. Different types of enumeration can be freely mixed. As a result, their use is limited to providing documentation, and many programmers advocate avoiding global enumerated data types.
However, enumerations can be made part of a class. This allows checking for proper usage of the enumeration instance. It also allows for the "duplicate" use of an enumeration name. (The scope qualifier resolves any ambiguity.)
The use of enumerations in C++ has been used to clarify the "magic" parameters that go into C++ library functions, particularly input/output functions.
enumout.cpp
/* ENUMOUT.CPP: Examine enumerated data type output */
#include <iostream>
using namespace std;
class Fruit
{
public:
enum Color { red, yellow, unknown };
Color fruitcolor;
};
ostream& operator< ( ostream& c, Fruit f )
{
switch (f.fruitcolor)
{
case Fruit::red:
c < "red"; break;
case Fruit::yellow:
c < "yellow"; break;
case Fruit::unknown:
c < "???"; break;
default:
c < "ERROR"; break;
}
return c;
}
main()
{
Fruit apple, banana;
banana.fruitcolor = Fruit::yellow;
apple.fruitcolor = Fruit::unknown;
cout < "Banana's color = " < banana
< endl;
cout < "Apple's color = " < apple
< endl;
return 0;
}
OVERLOADING THE < OPERATOR
Combining an enumerated data type together with overloading the insertion operator allows us to provide customized input/output.
The < operator is already overloaded in the ios class. It provides for an ostream on the left and any standard type on the right.
To provide customized output, all we need to do is write another operator overload with our new data type/class on the right.
However, this overloaded operator is in neither the ios class nor the new class (Fruit, in this case).
This is no problem in the code on the facing page, since the members of the Fruit class are all public. In general, of course, this is not what we want.
The main function also allows (requires?) direct access into the public data element of the Fruit class.
enumout2.cpp
/* ENUMOUT2.CPP: Friends for overloading < for enums */
#include <iostream>
using namespace std;
class Fruit
{
public:
enum Color { red, yellow, unknown };
private:
Color fruitcolor;
public:
Fruit operator= ( Color );
friend ostream& operator< ( ostream& c, Fruit f );
};
Fruit Fruit::operator= ( Color col )
{
fruitcolor = col;
return *this;
}
ostream& operator< ( ostream& c, Fruit f )
{
switch (f.fruitcolor)
{
case Fruit::red:
c < "red"; break;
case Fruit::yellow:
c < "yellow"; break;
case Fruit::unknown:
c < "???"; break;
}
return c;
}
main()
{
Fruit apple, banana;
banana = Fruit::yellow;
apple = Fruit::unknown;
cout < "Banana's color = " < banana < endl;
cout < "Apple's color = " < apple < endl;
return 0;
}
FRIENDS
We need a way for a function to access the members of a class without making the class members public.
We need to do that without making the function part of the class in three situations:
1. A function needs to access data from two or more classes. In this case, the function is not a member of either class. It must be declared as a friend of both if they both have needed private data.
2. A function in one class needs access to data in another class. It must be declared as a friend of the other class from which the private data is needed.
3. Member notation needs to be avoided. This is handled like case 1.
When a function is declared as a friend of another function, the friend function is allowed to access the private members of the class.
The use of friends is not always the best solution. Careful object-oriented design can yield class hierarchies that eliminate or reduce the use of friends.
The use of friends tends to compromise the whole idea of data encapsulation. As a result, their use is controversial. While not as objectionable as global variables, it ranks near it.
enumout3.cpp
/* ENUMOUT3.CPP: < is already a friend of ostream */
#include <iostream>
using namespace std;
class Fruit
{
public:
enum Color { red, yellow, unknown };
private:
Color fruitcolor;
public:
Fruit operator= ( Color );
void print ( ostream& c) const;
};
Fruit Fruit::operator= ( Color col )
{
fruitcolor = col;
return *this;
}
void Fruit::print (ostream& c) const
{
switch (fruitcolor)
{
case Fruit::red: c < "red"; break;
case Fruit::yellow: c < "yellow"; break;
case Fruit::unknown: c < "???"; break;
}
}
ostream& operator< ( ostream& c, Fruit f )
{
f.print (c);
return c;
}
main()
{
Fruit apple, banana;
banana = Fruit::yellow;
apple = Fruit::unknown;
cout < "Banana's color = " < banana < endl;
cout < "Apple's color = " < apple < endl;
return 0;
}
SUPPORTING < WITHOUT FRIENDS
operator < has already been declared a friend function in the ostream class. For a function to access members of two classes it is only necessary that one of the classes declare a friend function
- Since friend functions are to be avoided, we wish to support operator < without declaring it to be a friend to our Fruit class. However, there is a problem since it is an ostream object that appears to the left of the operator.
- We can use the non-class instance overloading of the operator to get around the problem of which object type is to the left and which object type is to the right of the operator. However, we still need a way to actually insert the proper text into the output stream. This is done with a separate print( ) helper function.
- The print( ) function is declared to be const to indicate that it does not modify any member variable in the class. Thus, it can be used on a const member. In general, things that can be declared const should for efficiency and coding safety.
scope.cpp
/* SCOPE.CPP: Check the scope of variables */
#include <iostream>
using namespace std;
int i = 10;
class Test
{
public:
int i;
Test (int j = 0);
void print ();
};
Test::Test (int j)
{
i = j;
}
void Test::print ()
{
cout < "Class Test: Class i = " < i < endl;
cout < "Class Test: Global i = " < ::i < endl;
int i = 50;
cout < "Class Test: Local i = " < i < endl;
cout < "Class Test: Class i = " < Test::i < endl;
}
main()
{
int i = 20;
Test j(30);
cout < "local i = " < i < endl;
cout < "global i = " < ::i < endl;
cout < "class i = " < j.i < endl;
{
int i = 40; // local to block
cout < "inner block i = " < i < endl;
}
cout < "back to local i = " < i < endl;
j.print();
return 0;
}
VARIABLE SCOPE
The scope of a variable is the area within a program when the value of the variable will be available.
Global variables (which should be avoided) are always in scope whenever another variable of the same name is not in scope.
A global variable can also be accessed in preference to another variable of the same name by using the :: operator
A local variable overrides any global variable of the same name or any other local variable of the same name at an outer lever in the same function.
Local variables at outer levels are automatically brought back in scope when the inner level is exited.
Within member functions, class members are in scope unless overridden by local variables in the functions.
Class members can be brought back into scope by using the :: operator, preceding by the class name.
union2.cpp
/* UNION2.CPP: Simple application of a union */
#include <iostream>
#include <string>
using namespace std;
struct EMPLOYEE
{
char name[25];
int age;
char job_status; // P = permanent, C = contract
union
{
char employee_no[10]; // employee number
int svc_time; // months of service
} job_info;
};
void display (EMPLOYEE *);
main()
{
EMPLOYEE joe,irene;
(void) strcpy(joe.name, "JOSEPH");
joe.age = 21;
joe.job_status = 'P';
(void) strcpy(joe.job_info.employee_no, "654321");
(void) strcpy(irene.name, "IRENE");
irene.age = 16;
irene.job_status = 'C';
irene.job_info.svc_time = 1;
display(&joe);
display(&irene);
return 0;
}
UNIONS FOR VARIANT RECORDS
The union in C++ is similar to the variant record in Pascal or the redefines clause in COBOL.
Generally, the C++ code must keep track of which part of the union is currently active so that garbage data isn't brought back through the misuse of the union. In the facing program, this is done via a character variable that stores a code for the type of record being stored.
This results in a lot of error-prone code, which is not easily extensible if we decide to add, for example, student employees, part-time employees, volunteers, etc., all with a slightly different twist on what information we have to store. The code quickly becomes unmanageable. It was kept in C++ to be compatible with C, but is rarely used.
union2.cpp (Cont.)
void display(EMPLOYEE *x)
// display information in employee structure
{
cout < x->name < endl;
cout < "age = " < x->age < " years\n";
if (x->job_status == 'P')
{
cout < "Permanent employee\n";
cout < "Employee number = "
< x->job_info.employee_no < endl;
}
else if (x->job_status == 'C')
{
cout < "Contract employee\n";
cout < "Service time = "
< x->job_info.svc_time < " months\n";
}
cout < endl;
}
DERIVED CLASSES
Instead of using the union approach, C++ provides a whole new way of handling "slight" variation. A class that differs in some respects from another, but represents a “subset” or “refinement” of the class, can be derived from the original (base) class.
The whole concept of object-oriented design and programming hinges upon the ability to properly construct a set of classes the have the appropriate sharing of data and functions.
In this example, it seems obvious that we can extract the common information, name and age, into a base class. Then, we will construct two derived classes, one of which will add the employee number, and the other will add the service time.
We don't wish to write three sets of code, however. We wish to write one set of code for the common information, and then one set of code for each set of unique information.
The class hierarchy is graphed as follows:
Person
PermanentContract
union3.h
/* Adapts a Union to C++ derived classes */
#ifndef union3
#define union3
#include <iostream>
#include <string>
using namespace std;
class Person
{
protected:
char* name;
int age;
public:
Person (char* s, int years);
Person (Person & a);
void display();
~Person();
};
Person::Person (char* s, int years)
: name (new char[strlen(s) + 1]), age (years)
{
strcpy (name, s);
}
Person::Person (Person & a)
: name (new char[strlen(a.name) + 1]), age (a.age)
{
strcpy (name, a.name);
}
Person::~Person()
{
delete [] name;
}
void Person::display()
{
cout < "Name: " < name < endl;
cout < "Age: " < age < endl;
}
class Permanent : public Person
{
protected:
char* employee_no;
public:
Permanent (char* s, int old, char* num);
union3.h (Continued)
Permanent (Permanent & a);
void display();
~Permanent();
};
Permanent::Permanent (char* s, int old, char* num) :
Person (s, old), employee_no (new char[strlen(num)+1])
{
strcpy (employee_no, num);
}
Permanent::Permanent (Permanent & a) : Person (a)
{
employee_no = new char [strlen(a.employee_no)+1];
strcpy (employee_no, a.employee_no);
}
Permanent::~Permanent()
{
delete [] employee_no;
}
void Permanent::display()
{
Person::display();
cout < "Employee no: " < employee_no < endl;
}
class Contract : public Person
{
protected:
int svc_time; // months of service
public:
Contract (char* s, int old, int months);
Contract (Contract & a);
void display();
};
Contract::Contract(char* s, int old, int months)
: Person (s, old), svc_time (months) {}
Contract::Contract (Contract & a)
: Person (a), svc_time (a.svc_time) {}
void Contract::display()
{
Person::display();
cout < "Service months: " < svc_time < endl;
}
#endif
union3.cpp
/* UNION3.CPP: Test Derived classes */
#include "union3.h"
main()
{
Person pat("Pat",41);
Permanent joe("JOSEPH",21,"654321");
Contract irene("IRENE",16,1);
pat.display();
joe.display();
irene.display();
return 0;
}
IMPLEMENTING DERIVED CLASSES
A derived class is obtained by defining a class to be derived from a specific base class. In this case, both Permanent and Contract are derived from Person.
The derived class can only access (inherit) the protected and public member data and member functions of the base class.
If the derivation is public, then inherited data and functions retain their protected and public status
If the derivation is protected, then the inherited members all become protected.
If the derivation is private, then the inherited members all become private, which is the default (although the least used)
You should have constructor functions for the base class, as well as constructor functions for the derived classes, because constructor functions cannot be inherited.
shape.h
/* Program illustrates the use of structures to do
geometry of triangles. */
#ifndef shapeh
#define shapeh
#include <iostream>
#include <math>
using namespace std;
class SHAPE
{
public:
SHAPE(void);
double operator +(const SHAPE& x) const;
double operator *(const SHAPE& x) const;
virtual double perimeter(void) const = 0;
virtual double area(void) const = 0;
};
class TRIANGLE : public SHAPE
{
private:
double side1;
double side2;
double side3;
public:
TRIANGLE(double a, double b, double c);
double perimeter(void) const;
double area(void) const;
};
class RECTANGLE : public SHAPE
{
private:
double side1;
double side2;
public:
RECTANGLE(double a, double b);
double perimeter(void) const;
double area(void) const;
};
#endif
POLYMORPHISM
- The concept of polymorphism allows different objects to respond to a message in different ways. In the case of shapes, we define an abstract base class shape. From this, we derive two classes, TRIANGLE and RECTANGLE. TRIANGLE and RECTANGLE both need to respond to messages for area and perimeter, but they each need to do so in their own way.
- As much functionality should be brought back into the base class as possible to avoid duplication of effort and provide a single point of change. Thus, operator+ to add two perimeters and operator* to add two areas should be defined in the base class.
- However, the base class needs to call (the correct version of) perimeter and area in implementing operator+ and operator*. Thus, it is not sufficient to define perimeter and area in the derived classes - they must also be “defined” in the base class.
- The keyword virtual is used in front of a function declaration in a base class to indicate that every subclass will have a version of this function and that the function is to be accessed via a pointer that retains the identity of the subclass. Access to a function declared as virtual is done through a process of dynamic binding or late binding. The actual function being called is not known until run time after the object is passed as a parameter.
- The = 0 after the function declaration in the abstract base class does two things. First, it tells the linker not to look for code for the perimeter and area functions in the base class. Second, it tells the compiler that this is an abstract base class and that it is illegal to define any variables of type SHAPE.
shape.cpp
/* SHAPE.CPP: Member functions for shape geometry */
#ifndef shapecpp
#define shapecpp
#include "shape.h"
SHAPE::SHAPE(void) { };
double SHAPE::operator+ (const SHAPE& x) const
{
return perimeter() + x.perimeter();
}
double SHAPE::operator* (const SHAPE& x) const
{
return area() + x.area();
}
TRIANGLE::TRIANGLE(double a, double b, double c)
:SHAPE(), side1(a), side2(b), side3(c) {}
double TRIANGLE::perimeter(void) const
{
return side1 + side2 + side3;
}
double TRIANGLE::area(void) const
{
double s;
s = perimeter()/2.0;
return sqrt(s*(s - side1)*(s - side2)*(s - side3));
}
RECTANGLE::RECTANGLE(double a, double b)
:SHAPE(), side1(a), side2(b) {}
double RECTANGLE::perimeter(void) const
{
return 2*side1 + 2*side2;
}
double RECTANGLE::area(void) const
{