• No results found

// forward reference so that

N/A
N/A
Protected

Academic year: 2021

Share "// forward reference so that"

Copied!
19
0
0

Loading.... (view fulltext now)

Full text

(1)

_______________________________________________________________________________________________

Classes and Abstract Data Types

Earlier, we approached the problem of setting up a database by specifying the data for the problem and the operations to be performed on the data. Although we ultimately had to look at how the data would be stored and how the operations would be implemented, we began by just describing the data and what we would do with it. This logical view of data – what is represented and what operations are performed – form an abstract data type. The differences between the traditional data types such as int, float, char and the user-defined data types is minimal. The principal concept used in C++ to attain this abstraction is the class.

1. The Class

We'll introduce the class by showing the implementation of the recent linked list example as a class. The first component is the interface description, usually placed in a header file called, say, List.h.

struct Cust; // forward reference so that

typedef Cust* CustPtr; // pointer type can be declared struct Cust

{

char name[30]; int partysize; CustPtr next; };

class LinkedList {

public:

void Initialise(); void Add(char[],int); bool Remove(char[],int&); private:

CustPtr Head; };

Next is the implementation component, in a file called List.cpp. void LinkedList::Initialise()

{

Head = 0; }

void LinkedList::Add(char name[], int psize) {

CustPtr tmp, curr;

// create a new customer and store the information tmp = new Cust;

strcpy(tmp->name,name); tmp->partysize = psize; tmp->next = 0;

// add to the end of the list if the list is not empty if (Head != 0)

{

curr = Head;

while (curr->next) // is this the end? curr = curr->next;

curr->next = tmp; }

else

Head = tmp; }

(2)

bool LinkedList::Remove(char name[], int& psize) {

CustPtr tmp; if (Head == 0)

return false; strcpy(name,Head->name); psize = Head->partysize;

tmp = Head; // make a copy of the pointer to current head Head = Head->next; // change the head

delete tmp; // delete the old head's memory

return true; }

Of course, the implementation file will also need

#include "List.h"

Let's now look at the construct in detail. There are similarities between the class and the struct – more than will be mentioned in this subject. The class principally has two sections as shown in the following:

class classname {

public:

assorted user-accessible functions, etc private:

data (and functions) hidden from the user };

The interface section of the declaration is what the user needs to know about – and what the compiler needs to know so that the user's use of the type can be compiled. The user can call the functions in the public section, but cannot reference the private section. In this way the data is protected. In fact, the declaration of the struct Cust and its associated pointer type can be placed in the private part of the class as well, since it is not required except within the class.

The implementation section is the actual code for the functions. Note the use of the prefix LinkedList:: to indicate what class these functions refer to, and how these functions just refer to the data in the private part as if the identifiers were local.

Just as in the case of structs, the class directive declares a type not a variable. Once the type has been declared, instances of that type can be declared. These instances are called class objects.

To declare a variable of the type LinkedList called list, we write LinkedList list;

This requests the C++ compiler to set aside enough memory for the data structure LinkedList, linking the name list to that location. How much memory does it require? The amount needed for the (public and private) data.

When it is created the private data item Head for this particular instance of the LinkedList is not initialised. To do that we call

list.Initialise();

Note the use of the prefix varname., just as we have seen it for structs and for functions associated with streams. It just reminds us that streams are in fact instances of classes.

It turns out that C++ provides a facility for initialising the private data whenever such a variable is declared. A special public function, called a constructor, can be part of the class. Its name is the

(3)

_______________________________________________________________________________________________ name of the class and it has no type (not even void) nor any arguments. (Other constructors, with arguments, are discussed later.)

So our class interface would contain LinkedList();

and our implementation would incorporate LinkedList::LinkedList() {

Head = 0; }

We do not implicitly call the constructor – C++ generates the call where needed. We no longer require the separate Initialise() call. We'll keep it to enable reinitialisation – although, if we re-initialise a list containing existing items, we would cause a memory leak. This would also occur if we declared an instance of the class as an automatic variable.

For example, void A() {

LinkedList Alist; ...

... }

Once we return from the function A, we no longer have access to the variable Alist or its associated data. Of course the space for the data within the class is free'd but any dynamically allocated data linked to that Head is set adrift. Obviously that is undesirable.

C++ provides the complement of a constructor – a destructor – to enable us to provide a function to clean up any such dynamic memory. We do not have to (and in fact cannot) clean up the space needed for the class itself. A destructor's name is the name of the class preceded by the symbol ~ (called tilde) as in

~LinkedList(); with implementation

LinkedList::~LinkedList() {

CustPtr tmp; while (Head != 0) {

tmp = Head; // copy the pointer to current head Head = Head->next; // change the head delete tmp; // delete the old head's memory }

}

This function will now be called automatically any time an instance of the class is destroyed. So let's look how this can be used.

Consider a supermarket with n checkouts. Each checkout has its own queue. We don't want to limit the number of checkouts so we'll use dynamic allocation to create the queues.

LinkedList *checkout; int n;

(4)

cout << "How many checkouts? "; cin >> n;

checkout = new LinkedList[n]; // now have an array of n lists

... // all initialised

...

delete [] checkout;

Here the constructor is called for each linked list created in the array. And the destructor is called for each of the linked lists when delete is invoked.

In this latter example, we might not want to utilise all the parts of the contents of the linked list, namely the customer's name, nor would we use the int as the party size but rather the number of items in the trolley. We might like to generalise the construct to all sorts of application areas. C++ provides a construct for creating such generalisations, as we shall see later.

2. Another Example

We said earlier that the class provides the facility to define our own abstract datatypes. Let's look at a simpler example. Consider the case of an ADT to represent time.

First, what will we call it? TimeType

What do we want to represent?

time of day in the form of hours, minutes and seconds What do we want to be able to do with it?

set the time print the time

increment the time by a second compare two times for equality

determine if one time comes before another

Note that this description does not say how these requirements are to be implemented. To the user who needs such an ADT it doesn't really matter. The interface for the class is the only information that the user needs.

Here is a suitable declaration. class TimeType {

public:

void Set(int,int,int); void Increment(); void Write() const;

bool Equals(const TimeType&) const; bool IsBefore(const TimeType&) const; private:

int hours, minutes, seconds; };

Before we even look at the implementation, let's comment on a few things. 2.1 Referencing the data

The three functions Set, Increment and Write do not have an argument indicating what we are setting, incrementing or writing. They are actually manipulating the values of the data (currently, and usually, all private) in the class.

(5)

_______________________________________________________________________________________________ How does the program know on which instance of the class to perform these actions?

Because the name of the identifier precedes the function. An object of the above type is declared as TimeType now;

This creates the data area for the three components of the variable now. Those three members are now.hours, now.minutes and now.seconds. This compares with the struct members with one major difference. Although we declared the identifier now, we cannot access the data members of the class because they are private. Only functions within the class can access them. Thus a call of the form

now.Set(13,10,0);

is needed to set the three components of the object to the time 13 hours, 10 minutes, 0 seconds. The call

now.Increment();

would then modify the data so that the seconds member would be incremented by 1 – with possible adjustments to all three members. Obviously, the call

now.Write();

would output the time in some chosen format – without changing the data.

It might be useful at this time to introduce some terminology. If a member function modifies the value of the data of an instance it is called a mutator function. The functions Set and Increment are examples. Functions which merely access the data without changing the values are called observer functions or accessor functions. Write is one such function.

2.2 Protecting the data

What is the keyword const doing after the argument list on some of the functions? A class function has full access to the private data without the need for the values to be referred to by arguments, so there is no place to protect the data being referenced from being altered (either by passing by value or passing by reference with a preceding const). Thus class functions can be followed by the keyword const to ensure that changes to the private data is guarded by the compiler. If there are public data members they are also guarded. Assessor functions generally have this associated keyword.

But what if we want to reference the private data of another instance? 2.3 Access to private data of another object

The fourth and fifth functions in this example illustrate how to pass information about another instance to a class function. To compare two instances, we pass the information about the two in two different ways.

TimeType time1, time2; if (time1.Equals(time2))

cout << "time1 equals time2\n";

The function Equals() gets the first time to be compared from its invocation, that is the identifier appearing before the dot, while the second time is in the argument – here passed by const-protected reference. This is somewhat like a binary operator (in this case ==) where the left operand is the object before the dot and the right operand is the argument of the function. We will see more about operators very soon.

Why can the private data of one instance be accessed by another? Because they are of the same class. 2.4 Implementation

Now that we have described the interface or specification of the class – located in a header file called say TimeType.h – we can proceed with the implementation in TimeType.cpp.

void TimeType::Set(int hrs, int mins, int secs) {

hours = hrs; // setting the private data members

minutes = mins;

(6)

seconds = secs; }

Hey! Isn't this just some fancy data hiding? Surely we can do better than that. Can't we ensure that the time is a valid representation? What if hrs is passed as a negative number or a number larger than 23? What should we do?

There are varying levels of involvement:

a) whatever is passed is stored (as above) – assume the user knows best and will not pass incorrect values;

b) adjust the values passed so that the resulting stored values are valid. For example, we could preceed the above three lines by

if (secs > 59) // assume non-negative {

mins += secs/60; secs %= 60; }

if (mins > 59) // and again {

hrs += mins/60; mins %= 60; }

hrs %= 24; // and here

c) any invalid value is rejected, with an error message printed and the program exited. That is if (secs < 0 || secs > 59 || mins < 0 || mins > 59 ||

hrs < 0 || hrs > 23) {

cerr << "Illegal time value\n.";

exit(1); // bad value exit

}

The header cstdlib should be included for the exit() function.

d) any invalid value causes an exception, just like overflows, underflows and divisions by zero in the built-in datatypes. Exception handling is a very important part of C++, but is not going to be covered here. (We could, of course, make the function return an error code.)

For the moment we will use approaches (a) - (c). Let's move on to the other functions.

void TimeType::Increment() {

seconds++;

if (seconds == 60) {

seconds = 0; minutes++;

if (minutes == 60) {

minutes = 0; hours++;

if (hours == 24) hours = 0; }

} }

bool TimeType::Equals(const TimeType& t) const {

(7)

_______________________________________________________________________________________________ return (hours == t.hours && minutes == t.minutes

&& seconds == t.seconds); }

Note how the references to the second instance of the class, t, uses the "struct-style" reference to the private members while the first just uses the names. A reminder that the second instance's private values cannot be changed because they are protected by the const keyword preceding the reference type, while the first instance's values are protected by the const keyword following the function header.

bool TimeType::IsBefore(const TimeType& t) const {

return (hours < t.hours

|| (hours == t.hours && minutes < t.minutes)

|| (hours == t.hours && minutes == t.minutes

&& seconds < t.seconds)); }

void TimeType::Write() const {

// time is output in the form HH:MM:SS if (hours < 10) cout << '0';

cout << hours << ':';

if (minutes < 10) cout << '0'; cout << minutes << ':';

if (seconds < 10) cout << '0'; cout << seconds;

}

One of the unwritten preconditions to all the functions other than Set() is that, before a reference to any instance of this class can be made, a call to Set() is required. This is because we have yet to provide a class constructor.

2.5 Adding constructors and destructors

In our previous example of a class, we discussed how constructors provide C++ with an initialisation procedure to follow when creating the memory for an instance of the linked list class. We always initialised a list to be empty. For our current example, we might like to initialise a TimeType to something other than 0, just as we often initialise an int to a non-zero value.

There are two forms of constructor. The first is the default constructor which is used whenever an instance is declared without an initial value. Such a constructor has a void argument list. Thus

TimeType::TimeType() {

hours = minutes = seconds = 0; }

is a suitable implementation. Then we can be sure that a declaration such as TimeType deadline;

would give the new variable a value of 00:00:00.

Arrays of such objects cannot be created unless there is such a default constructor. If the programmer doesn't provide any constructor, C++ will provide a default constructor (but with no initialisation). Thus, with the above default constructor, the following array could be allocated and initialised.

TimeType* starttime = new TimeType[7];

The second form of constructor has a parameter list, which is used to specify the initial value. Hence it is usually called an initialising constructor.

Thus

TimeType::TimeType(int init_hrs, int init_mins, int init_secs) {

(8)

hours = init_hrs; minutes = init_mins; seconds = init_secs; }

This would then be used as in the following declaration TimeType start(10,20,30);

to set the variable start to the value 10:20:30.

How can we have two functions with the same name?

Because C++ functions are different if they have different argument lists.

Now that we have constructors, we do not need that proviso that Set() has to be called before we use any of the other functions.

As to destructors, we do not need to provide an explicit destructor for the TimeType class, as only static data is involved. Classes involving dynamic data need destructors. C++ automatically destroys the memory of a simple class such as TimeType.

3. Operations on classes

In the examples we've seen so far, the only things we've done with instances of the classes were declaring them, and using the public functions for the class. What else can we do?

3.1 Copying objects

Copying objects occurs in C++ in two forms: explicit copying, where the programmer specifies that a particular object's values are to be transferred to the memory of another object (assignment); and implicit copying, where the compiler is required to make a copy of the contents of an object into a new memory location of the same type. We will see that these two processes are essentially the same, but require some special consideration.

When discussing structs, we found that the assignment operator is about the only thing that works. This also applies to classes. Thus

TimeType time1, time2;

time1.Set(10,20,30); // time1 is now 10:20:30 time2 = time1; // time2 is now also 10:20:30

However, if a class contains dynamic data, only the base data is copied. This is called shallow copying. For example, suppose we declare two variables

LinkedList list1, list2;

then perform code to add a number of items to list1, and then

list2 = list1; // list2 now contains the same items as list1

That is, the head of each list is a pointer to the same item. But the nodes of each list are not duplicated – they are the same locations in memory. Thus, if we remove the head of the list in list1, the memory for that item is deleted. But list2 still points at it. This shows one of the problems in copying such complex data structures.

Sometimes the copy process as described above is acceptable – provided it does not lead to dangling pointers. Most times we would like the dynamic part of the class to be copied as well. This is called deep copying. To enable deep copying we need a class function which is called by C++ whenever the assignment operator is used in association with a class object.

When we pass an object by value to a function or have an object being the return value for a function, an implicit copy is also carried out. The difference here is that the copy is made to a new memory location.

(9)

_______________________________________________________________________________________________ These two copying processes are carried out by an (overloaded) assignment operator and a copy constructor. We know when the former will get called – we specify an assignment using the = operator. Calls to the latter will be generated by the compiler when needed. (There is also an explicit call to a copy constructor. When?) If implementations of these functions are not defined by the programmer, the compiler will perform a shallow copy. Let's deal with the second form of the copy process first.

A copy constructor has the same name as the constructors described previously. It only varies in that the argument list contains another instance of the same class. Thus it must perform all the usual tasks that a constructor does (allocate memory, initialise) but also must transfer the values of its argument to the class instance.

Thus we now have class aClass {

public: . . . . . .

aClass(); // default constructor

aClass(..,..,..); // initialising constructor

aClass(const aClass& anObject); // copy constructor . . .

. . . };

We'll see in later examples how copy constructors are used. For now let's show how our TimeType class can have a copy constructor even though one is not required.

TimeType::TimeType(const TimeType& otherTime) {

hours = otherTime.hours; minutes = otherTime.minutes; seconds = otherTime.seconds; }

Note how the contents of the instance otherTime are copied to the current instance. The assignment operator is discussed in the next section.

3.2 Other operators

Many of the built-in operators in C++ are polymorphic. That is, they designate the same general operations on different data types, even though the actual operations may vary with the types of the operands.

We can re-use almost all of the built-in operators for operations on our own ADTs. This is called overloading an operator.

For example, in the case of TimeType we could use the public function time1.Equals(time2)

to determine if the times time1 and time2 were the same. Wouldn't it be neater if we could use time1 == time2

just like we do for int, or float or char.

Before we show how this is done, it should be stated that once operator overloading is started it never stops. If a user sees that the equality operator is available, they will expect every operator usable by the built-in types to be available. Thus the creator of a new ADT must be prepared to provide all operators if they provide one.

(10)

When a binary operator, say • (representing any built-in), is encountered in the code of a C++ program, the compiler generates a call to a function named operator• with one argument being the right operand, and whose left operand is the instance preceding the function.

So, for example a + b would become a call to a.operator+(b)

with a return value as determined by the function. Similarly the expression a == b becomes

a.operator==(b)

Thus, if we want to allow TimeType variables to be compared using the operator <, we have to provide bool TimeType::operator<(const TimeType& r_op) const

{

return (hours < r_op.hours

|| (hours == r_op.hours && minutes < r_op.minutes)

|| (hours == r_op.hours && minutes == r_op.minutes

&& seconds < r_op.seconds)); }

We could similarly provide an addition operator

const TimeType TimeType::operator+(const TimeType& r_op) const {

TimeType temp = r_op; temp.hours += hours; temp.minutes += minutes; temp.seconds += seconds;

// code here to clean up the values to be within range return temp;

}

Note the need for an assignment operator here (although the shallow copy would suffice). We could, of course, pass r_op by value and use it as the temporary storage, being just a copy of the data that the function receives. We also need a copy constructor. We'll explain the reason for const on the return value later.

We could even provide another operator for adding a number of seconds to a time. const TimeType TimeType::operator+(int r_op) const

{

TimeType temp(hours,minutes,seconds+r_op);

// code here to clean up the values to be within range return temp;

}

We could then say

TimeType t1(1,2,3), t2; t2 = t1 + 10;

but could not say t2 = 10+t1;

as we have not provided the operator + where the left operand is int and the right operand is TimeType. See how complex things become!

What would we need to provide the second of the + operators? We would need an operator+ function which got its two operands a different way. C++ provides that method of operator overloading as well. We can write

(11)

_______________________________________________________________________________________________ const TimeType operator+(int, const TimeType&);

This function would need access to the internals of a TimeType, but it is not a member function of the class. C++ calls such a function a friend of the class. We have to prefix the above prototype with the keyword friend and place it within the class definition, often before the public section, as it really is neither a public nor private member. It acts like a member function but with the explicit inclusion of the type as the last argument. We do not put friend on the implementation of the function (as, at that time, we don't know what it is a friend of), nor do we prefix the function name with TimeType:: as it isn't a member function. Instead, we have to refer to the member data by the instance name.

So

const TimeType operator+(int l_op, const TimeType& r_op) {

TimeType temp(r_op.hours,r_op.minutes,r_op.seconds+l_op); // code here to clean up the values to be within range

return temp; }

We earlier indicated that statements such as t2 = t1 + 10;

assume the existence of an assignment operator (even the shallow copy generic).

We can provide the assignment operator, which is very similar to the copy constructor described in the previous section. This is of the form

TimeType& TimeType::operator=(const TimeType& from) {

// code here to copy the members of from to this object return *this;

}

Note how the value of the current instance is returned using the keyword this. The keyword is a pointer to the class instance itself. Thus *this refers to the class instance. The result of the assignment operator function is returned as a reference so that assignments can be cascaded. Note that, unlike the copying in the copy constructor, the destination of the copy is an existing instance, which may already have dynamic data associated with it. Thus, the function should ensure that any dynamic data already present in the current object is deleted. It must also check to see if the source and destination are not the same instance (a=a), in which case no copying is required.

Unary operators are also capable of being overloaded, but we'll leave that to future study. 3.3 Interacting with streams

Now that we know a little about classes, we can see that the streams we have used for input and output are just instances of classes. Apart from the default streams cin, cout and cerr we have also seen the classes ifstream, ofstream and fstream which have allowed us to declare our own file streams. In fact these three classes inherit their behaviour from two more general classes: istream for input, and ostream for output. The concept of creating classes from other classes by the process of inheritance is an integral part of C++ and will be introduced soon. Suffice to say at this stage that to interact with the three other streams we must work with the two 'parent' classes.

So how is what we now know about classes reflected in our usage of streams? We can now see that functions such as

cin.get(), cin.eof(), cout.put()

are just public functions of the class. But what is more important is that the two operators insertion (<<) and extraction (>>) are just operators defined in the classes.

Within the class ostream we have member functions defined of the form

(12)

ostream& operator<<(const type&);

or the friend equivalent, for each of the built-in types. Of course, we don't want to alter the ostream class by adding functions to it (nor should we), but we want to access the private data of another class anyway. So we'll use a friend function to our class.

For example, a function for handling TimeType could be declared as

friend ostream& operator<<(ostream&, const TimeType&); and placed just after

class TimeType {

Here is a suitable implementation.

ostream& operator<<(ostream& os, const TimeType& time) {

// time is output in the form HH:MM:SS

if (time.hours < 10) // note use of argument instance os << '0';

os << time.hours << ':'; if (time.minutes < 10)

os << '0';

os << time.minutes << ':'; if (time.seconds < 10)

os << '0'; os << time.seconds;

return os; // return value of stream

}

Now we don't need the Write() function. We can just say TimeType t(10,20,30);

cout << "The time is " << t << endl; to get the output

The time is 10:20:30

Extraction can also be handled by the operator >> for the stream istream in the form friend istream& operator>>(istream&, type&);

The function has to convert characters from the input stream into the required type. We have to specify what a legitimate input for the type is. For example, we might require TimeType values to be input in a similar form to the way we are outputting them – three integers separated by character :. So

istream& operator>>(istream& is, TimeType& time) {

char temp;

is >> time.hours;

is >> temp; // should be a : is >> time.minutes;

is >> temp; // another : is >> time.seconds; return is;

}

This is the simplest form where everything entered into a stream is a valid format with values in the correct ranges. The implementation of the extraction operator should test input for correctness and not return the stream if there is an error.

(13)

_______________________________________________________________________________________________

4. Why const return values on certain operators?

We indicated earlier that the arithmetic operator + returns a const reference. Why?

If a and b are both of type TimeType, then so is a+b. If the return value from the operator+ function were not const, then we could use (a+b) anywhere a TimeType could go. This means we can say, for example

cout << (a+b);

and make use of the insertion operator. But suppose we had a mutator function called input designed to alter the value of an instance of a TimeType.

Then we could also say (a+b).input();

which of course is an illegal operation (changing the value of an expression). So the return by const is the solution. The compiler would prevent the expression from being used where a non-const is needed.

5. Templates

We said earlier that we would like to generalise class definitions. Before we introduce the concept of parameterisation of datatypes, let's look at a more general linked list class.

// the data being stored in the list is of type DataType class LinkedList

{

public:

LinkedList();

~LinkedList(); void Initialise();

bool IsEmpty(); // true if list is empty

void Add(const DataType&); // passes pointer to data

DataType Remove(); // returns a data value

private:

struct Node;

typedef Node* NodePtr; struct Node

{

DataType data; // the data is now in the node NodePtr next;

};

NodePtr Head; };

Note that the data is in the node – a part of the list. Of course the data type could be a pointer, as we saw in the last topic.

How do we specify what DataType is?

We could insert a line in the interface file, using typedef to declare DataType. For example, if the data was of type int, we could say

typedef int DataType;

If the type was as before, that is, customers, then we could specify typedef Cust DataType;

or

typedef CustPtr DataType; to avoid too much copying.

(14)

Thus, when the implementation of the LinkedList class is compiled, the compiler will know the data type being stored in the list.

Unfortunately, changing a line in the header for the class violates much of the purpose of having generic classes. The traditional way of providing the information is to have the line

#include "DataType.h"

at the head of LinkedList.h, and providing the above header with the typedef line in it. The compiler then includes the definition into the compiled code.

C++, however, allows us to parameterise data types used within a class in a much more straightforward way. We do this by prefixing the class declaration by a template clause

template<class DataType> class ...

{

although the generic type is usually represented by a single letter such as T or U.

We can then use the type T throughout the class description to correspond to the datatype being stored. So here is a more general templated LinkedList class.

template<class T> class LinkedList {

public:

LinkedList();

~LinkedList(); void Initialise();

bool IsEmpty(); // true if list is empty void Add(const T&); // passes pointer to data

T Remove(); // returns a data value

private:

struct Node;

typedef Node* NodePtr; struct Node

{

T data; // the data is now in the node

NodePtr next; };

NodePtr Head; };

Although the word class appears in the template clause it actually refers to any datatype.

Once such a class is declared, an instance of that class must specify a type name to replace the T parameter as

LinkedList<type> varname; For example

LinkedList<int> intlist; LinkedList<float> floatlist; LinkedList<Cust> custlist;

Note that any type name, whether class, struct, user-defined alias, or in fact any base type can appear in object declarations. Note also that a template clause can involve more than one class.

(15)

_______________________________________________________________________________________________ Unfortunately, all is not quite so simple. Firstly, the C++ compiler must substitute the T with whatever type is specified – at compilation time. That is, when the compiler sees the instantiation of a templated class, it must have access to the source code. This can be achieved in several ways, corresponding to where the code is located.

5.1 Implementation in its own file

This is the standard method for implementation code. Each function must be prefixed by the template clause and each reference to the class name has a reference to T affixed to it.

Here is what the implementation part would then look like. template<class T>

LinkedList<T>::LinkedList() {

Head = 0; }

(Note the <T> following the class name, just as it does in the instancing of the class, where the actual datatype appears after the class name.)

template<class T>

LinkedList<T>::~LinkedList() {

NodePtr tmp; while (Head != 0) {

tmp = Head; // copy the pointer to head

Head = Head->next; // change the head

delete tmp; // delete the old head's memory

} }

template<class T>

void LinkedList<T>::Initialise() {

Head = 0; }

template<class T>

bool LinkedList<T>::IsEmpty() {

return (Head==0); }

template<class T>

void LinkedList<T>::Add(const T& data) {

NodePtr tmp, curr;

// create a new node and store the information tmp = new Node;

tmp->data = data; tmp->next = 0;

// add to the end of the list if the list is not empty if (Head != 0)

{

curr = Head;

while (curr->next) // is this the end? curr = curr->next;

curr->next = tmp; }

else

(16)

Head = tmp; }

template<class T>

T LinkedList<T>::Remove() {

NodePtr tmp; T data;

data = Head->data; // assume there is something to return

tmp = Head; // make a copy of the pointer to head

Head = Head->next; // change the head

delete tmp; // delete the old head's memory

return data; }

For the moment we will assume the class T is one of the base types, or a struct. This is due to the fact that our code above assumes that the type T has a constructor, a destructor and a copy constructor. When we say

NodePtr tmp; tmp = new Node; tmp->data = data;

we assume that the new will construct the T instance data within the Node, and that we can copy the type in the assignment. Similarly, when we

delete tmp;

C++ will automatically call the destructor for each of its members, including the instance of the type T. C++ provides a default constructor, destructor and copy constructor for any type without specific functions for them – including the base types.

We will see how the compiler finds this code in the lectures. For the moment, let's look at an alternative.

5.2 Implementation within header file

Here, we violate an oft-spoken rule about not placing code in header files, and incorporate the above implementation in with the interface for the class. The only consolation is that template code is not compiled as such – it is used as a template for future compilation when needed. (We should, perhaps, use a different suffix such as .hpp to indicate such header/template files, but unfortunately syntax- sensitive editors such as nedit don't recognise such extensions.) So our List.h would incorporate both the usual header content and the function code. An (even more dire) alternative would be to #include the implementation file above (don't ever do that!!!).

What does the compiler do?

#include "List.h" // including code templates

LinkedList<int> intlist; // int versions of the functions needed LinkedList<float> floatlist; // float versions of the code needed

That is, when the compiler encounters the above it would generate executable versions of the required functions.

One interesting consideration is: what if the header file is included within two source files? Won't there be conflicts between identical template function instances? The answer is no. But there may be multiple executables of each (used) function.

5.3 Using the templated class

With templates we can now have linked lists of many different types. For example, with

(17)

_______________________________________________________________________________________________ struct Customer

{

char name[20]; int number; };

and

struct Video {

char Title[41]; // 40 character string long BarCode;

short RunTime; // in minutes

char Category[11]; // comedy, adventure, action, drama, etc. short NumCopies;

}; we can create

LinkedList<Customer> ListOfCust; LinkedList<Video> ListOfVideo;

When C++ encounters the instantiation of a template class, it produces separate executable member functions for each instance of the template class. Thus there would be two versions of all these functions, one with T replaced with the type Customer and one replaced with Video.

This, of course, is possible since C++ allows function overloading – each function has the same name but differing argument/return value types. Note that although several of the functions do not have the type T in either their argument list, or in a return value, the template parameter T is an implicit argument to all member functions.

With these instances of the class LinkedList, what can we do? To add a customer to their linked list, we would need

Customer Cust;

and for each new customer, we would set the members of Cust and then call ListOfCust.Add(Cust);

To add a video to their linked list, we would need Video Vid;

and for each new video, we would call ListOfVideo.Add(Vid);

To get the first item off the lists, we would use if (!ListOfCust.IsEmpty())

Cust = ListOfCust.Remove(); and

if (!ListOfVideo.IsEmpty())

Vid = ListOfVideo.Remove();

If we are concerned about the amount of copying of complicated types such as Video above, we could store pointers to the type in the LinkedList. That is, we could say

typedef Video* VideoPtr;

LinkedList<VideoPtr> ListofVideoPtr;

We would then be required to create new instances of each Video, each with their own memory location which would be passed to the Add() function to be stored in the list.

Unfortunately, when C++ invokes the destructor for such an object, only the nodes would be deleted, leaving the memory used by the actual data no longer referenced – leakage.

(18)

6. Include Files and Preprocessor Directives

It is becoming obvious that the organisation of header files needs careful consideration. In particular, now that the header file may contain 'code', we must be careful not to include the same header file twice, either directly or indirectly, since we cannot declare identifiers more than once. There a a number of C++ preprocessor directives to handle this.

6.1 #define

The preprocessor directive

#define identifier token-string

causes the pre-processor to replace subsequent instances of the identifier with the given sequence of tokens. For example

#define MAX 23

would mean any occurrence of the identifier MAX would be replaced by the tokens 23. This does not mean MAXIMUM would become 23IMUM, nor would "MAX" be modified. This is the traditional way C programmers defined constants. In C++ the use of the const keyword is preferred. It is even possible to define inline functions in this way but, as it is not advisable, it is not described here.

A particular use is to declare flags for the inclusion of code fragments for debugging, machine-specific actions and header files. In fact, one use of the directive is for header files involving globals.

6.2 #undef

Often, we wish to change the meaning of the identifier declared in a #define. To do this, we must first undefine the defined value. Then we can redefine the meaning using another #define.

For example

#define A 5

char name[A]; // array of 5

#undef A

#define A 10

int age[A]; // array of 10

We can in fact eliminate a meaning for the identifier altogether by just not redefining it. 6.3 #if, #ifdef, #ifndef, #else, #elif, #endif

The five directives listed here provide the ability to selectively include code within a program file so that the code may or may not be compiled, depending on circumstances.

The first directive

#if constant-expression

specifies that the code contained between this line and the following #else, #elif or #endif will be compiled if the constant-expression evaluates to a non-zero value.

The directive

#elif constant-expression

provides for alternate code fragments to be selectively compiled. The directive

#ifdef identifier

specifies that the following code will be compiled if the identifier has been defined using #define (without later being undefined).

Similarly

#ifndef identifier

(19)

_______________________________________________________________________________________________ would allow the following fragment to be compiled if the identifier was undefined.

For example, using the identifier DEBUG, we could specify

#define DEBUG ... ...

#ifdef DEBUG

cout << "Reached this point in the program\n";

#endif

while testing, then remove the #define once the program was running.

Another use is to ensure that header files are included only once. For example, we could use an identifier LINKEDLIST_H to protect multiple inclusions by having the following at the top of the header file. Now that we know, this should always be done.

#ifndef LINKEDLIST_H

all the content goes here

#define LINKEDLIST_H

#endif

Often such identifiers include leading underscores.

There are even some pre-defined identifiers which may be useful: __LINE__ current line number in the C++ source file __FILE__ a string containing the name of the source file __DATE__ a string containing the date (of compilation) __TIME__ a string containing the time (of compilation)

An alternative method of avoiding multiple inclusion is the use of the directive

#pragma token-string

with the token-string being the word once. This directive is, however, implementation-dependent.

References

Related documents

–  Use Apex code to access external REST API’s –  Can be used to send data out or pull data in to/.. from an

Mackey brings the center a laparoscopic approach to liver and pancreas surgery not available at most area hospitals.. JOSHUA FORMAN, MD

As inter-speaker variability among these the two groups was minimal, ranging from 0% to 2% of lack of concord in the 21-40 group and from 41% to 46% in the 71+ generation, we

 Advises on issues related to: railway transportation of various types of cargo, route optimization, tariffs, railway station codes, routes, delivery lead times and

Agreeing to use a particular forum means waiving certain legal rights. In a permissive agreement, the parties waive their objections to litigating in a particular court.

Investigating the Relationship between Ethical Leadership and Deviant Behaviors in the Workplace: The Mediating Role of Emotional Commitment and Moral Moral, Organizational

The purpose of this study was to evaluate the diagnostic utility of real-time elastography (RTE) in differentiat- ing between reactive and metastatic cervical lymph nodes (LN)

For the measures of social interaction time, intention for a future exercise behaviour, enjoyment of the exercise session and perceived exertion, a series of mixed within