PART 4

Interaction between

Classes

Header files

Inline functions

Function templates

Friends

Polymorphism

condit1.c

/* Example of the conditional operator. */

#include <stdio.h>

main()

{

int x = 10, y;

y = x > 2 ? 3 : 5;

printf("x = %d, y = %d\n", x, y);

return 0;

}

condit2.c

/* Program introduces the preprocessor directive to define

macros */

#include <stdio.h>

#define max(a,b)( (a) < (b) ) ? b : a

main()

{

int x, y, z;

x = 1;

y = 5;

z = 0;

printf("x = %d, y = %d, z = %d\n", x, y, z);

z = max(x,y);

printf("x = %d, y = %d, z = %d\n", x, y, z);

return 0;

}

C CONDITIONAL EXPRESSION OPERATOR REVIEW

The conditional expression operator causes the evaluation of one out of two alternative expressions. The value of the conditional expression operator is the result of the expression evaluated.

The general format of the conditional expression operator is as follows:

condition ? expression1 : expression2

If the result of the evaluation of the condition is TRUE ( non-zero ), then expression1 is evaluated and the result of the evaluation becomes the result of the operation. If the condition evaluates FALSE ( zero ), then expression2 is evaluated and its value becomes the result of the operation.

For example:

int x, y, z;

...

z = x > 2 ? 3 : 5;

After the conditional expression is evaluated, z will have a value of 3 or 5, depending on whether x is greater than 2 or less than or equal to 2.

NOTE: The conditional expression operator is written with two operators ( ? and : ) and three operands.

condit3.c

/* Beware of possible side effects when using macros */

#include <stdio.h>

#define max(a,b)( (a) < (b) ) ? b : a

main()

{

int x, y, z;

x = 1;

y = 5;

z = 0;

printf("x = %d, y = %d, z = %d\n", x, y, z);

z = max(++x,++y);

printf("x = %d, y = %d, z = %d\n", x, y, z);

return 0;

}

RESULTS OF CONDIT1.C, CONDIT2.C, AND CONDIT3.C

condit1.c

x = / y =

condit2.c

x = / y = / z =
x = / y = / z =

condit3.c

x = / y = / z =
x = / y = / z =

SIDE EFFECTS IN PARAMETERS

The ++ operator increments a variable as a "side effect." Many function calls also produce results as side effects. (For example, when calling a function with the name of an array, any modification to the contents of the array is a side effect.)

Side effects should be avoided in the specification of parameters in a parameter list. This is because you never know for sure whether a function call is a real function call or whether it will be expanded as a macro.

In the example program condit3.c, the function max is actually implemented as a macro. The macro needs to reference the larger of the two parameters twice: once to make the comparison, and a second time to use its value as the result of the expression. Any side effect in the parameter list will occur twice for the larger value.

Moral: Keep Parameter Lists Simple

condit4.cpp

/* Inline functions - But this approach won't work */

#include <iostream.h>

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 < "z = " < z < endl;

z = max(x,y);

cout < "x = " < x < "y = " < y < "z = " < z < endl;

return 0;

}

condit5.cpp

/* Inline functions - must be in a header file */

#include <iostream.h>

#include "condit5.h"

main()

{

int x, y, z;

x = 1;

y = 5;

z = 0;

cout < "x=" < x < ", y=" < y < ", z=" < z < endl;

z = max(x,y);

cout < "x=" < x < ", y=" < y < ", z=" < z < endl;

z = max(++x,++y);

cout < "x=" < x < ", y=" < y < ", z=" < z < endl;

return 0;

}

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

/* Inline function - header file */

#ifndef condit5

#define condit5

inline int max (int a, int b)

{ return a > b ? a : b; }

inline double max (double a, double b)

{ return a > b ? a : b; }

inline long int max (long int a, long int b)

{ return a > b ? a : b; }

inline short int max (short int a, short in b)

{ return a > b ? a : b; }

// ...

#endif

condit7.h

/* Inline function - template for all types */

#ifndef condit5

#define condit5

template<class T> const T& max (const T&, const T&);

template<class T> const T& max (const T& a, const T& b)

{ return a > b ? a : b; }

#endif

INLINE FUNCTIONS

C++ allows functions to be declared as "in-line" functions

In-line functions generate in-line code like macros in C. 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 C macros, C++ in-line functions require the specification of parameter types and results. This can lead to a long list of functions for the "standard" types.

C++ version 3.0 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

/* Examine the enumerated data type in C++ */

#include <iostream.h>

main()

{

enum Gender { female, male, unknown = 4, couple };

// female=0, male=1, unknown=4, couple=5

enum Customer { yes, no };

/* adding "unknown" as a third possibility

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

In C, the 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 in C, and many C programmers advocate avoiding the enumerated data type.

In C++, 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/C++ library functions, particularly input/output functions.

enumout.cpp

/* Examine the enumerated data type in C++ */

#include <iostream.h>

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

/* Friends for overloading < for enums in C++ */

#include <iostream.h>

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 the 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

/* < is already a friend of ostream */

#include <iostream.h>

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

/* Check the scope of variables */

#include <iostream.h>

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.c

/* Program gives a simple application of a union */

#include <stdio.h>

#include <string.h>

typedef struct

{

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;

} EMPLOYEE;

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 is 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.

union2.c (Cont.)

void display(EMPLOYEE *x)

/* display information in employee structure */

{

printf("%s\n",x->name);

printf("age = %2d years\n",x->age);

if (x->job_status == 'P')

{

printf("Permanent employee\n");

printf("Employee number = %s\n",

x->job_info.employee_no);

}

else if (x->job_status == 'C')

{

printf("Contract employee\n");

printf("Service time = %d months\n",

x->job_info.svc_time);

}

printf("\n");

}

DERIVED CLASSES

Instead of using the union approach in C, 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.h>

#include <string.h>

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

/* 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.h>

#include <math.h>

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