1. Trang chủ
  2. » Công Nghệ Thông Tin

Core C++ A Software Engineering Approach phần 10 potx

108 289 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 108
Dung lượng 2,04 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Here, each instantiation of class Stack e.g., for type float has access to details of each instantiation of class Node e.g., of class Point.. Node *next; // Node *next' is also OK Nodec

Trang 1

to be consistent across different functions of the template class Here is another stack function where I use different names for the type parameter and for the expression parameter.

{ cout << "Out of memory\n"; exit(1); }

for (int i=0; i < size; i++) // copy existing stack

p[i] = items[i];

delete [] items; // return heap memory

items = p;

size *= 2; // update stack size

cout << "New size: " << size << endl;

items[top++] = c; } } // push symbol on top

Similar to type parameters, expression parameters have to be listed both in the template parameter list and in the class name prefix for all member functions Even if an expression parameter is not used within the body of the function, it still has to be listed

template <class Type, int sz>

Type Stack<Type,sz>::pop() // parameters are not used

{ return items[¡Xtop]; }

template <class Tp, int s>

bool Stack<Tp,s>::isEmpty() const // parameters are not used

Stack<int,4> s; // stack object

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 2

Stack<int,8> s1; // incompatible stack object

Consider, for example, a global function DebugPrint(). It has the parameter of class

Stack<int,4>. Notice that the parameter object is passed by reference¡Xthe private declaration of the Stack<Type,sz> copy constructor prevents passing stack objects by value Also notice that the parameter is not labeled as const because it changes (even temporarily) during function execution

void DebugPrint(Stack<int,4>& s) // no const modifier

{ Stack<int,4> temp;

cout << "Debugging print: ";

while (!s.isEmpty()) // pop until stack is empty

{ int x = s.pop(); temp.push(x); // save in temporary stack

cout << x << " "; } // print each component

cout << endl;

while (!temp.isEmpty()) // pop until stack is empty

{ s.push(temp.pop()); } } // restore initial state

The stack objects s and s1, are of different types Object s can be passed as a parameter to

DebugPrint(). An attempt to do so with object s1 causes a syntax error

const int length = 4;

Stack<int,length> s2; // compatible with Stack<int,4>

As far as templates with type parameters only are concerned, all instantiations with the same actual type arguments are of the same type, and one object can be used instead of another object Here we reconsider the template class with one type parameter

Trang 3

Type *items; // stack of items of type Type

int top, size; // current top, total size

Stack(const Stack& = 100);

operator = (const Stack&);

public:

Stack(int); // conversion constructor

void push(const Type&); // push on top of stack

Type pop(); // pop the top symbol

bool isEmpty() const; // is stack empty?

~Stack(); // return heap memory

Relations Between Instantiations of Template Classes

Template instantiations can be used as actual type arguments to instantiate other template classes For example, you can create a stack of dictionary entries with the following declaration

Stack<DictEntry<Point,char*> > stackOfEntries; // 100 entries

Notice an extra space between the two greater than signs If you do not insert this space, the

compiler will misunderstand the code and will shower you with a deluge of irrelevant error

messages None of these messages indicates that you need an extra space This is the second place where C++ is not space blind Another place where space is important is defining the default

parameter value for a function parameter of a pointer type where the name of the parameter is not used

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 4

In the declaration above, the Stack instantiation prompts the DictEntry instantiation An

optimized compiler might cache object code for reuse in future compilations If the compiler does not do that, compilation and link time can grow significantly

Template instantiations for different actual types (and expression values) are separate and have no relation or access to each other

For example, DictEntry<int,int> and DictEntry<float,record> are two independent distinct classes So are instantiations for Stack<int> and Stack<float>. Objects of these types cannot be used one instead of another

A class can declare all its template instantiations to have a common base non-template class:

Template Classes as Friends

A non-template class (or function) can be declared as a friend of all instantiations of a template class, if the use of instantiated objects does not depend on their type:

Trang 5

Node *next; // Node *next' is also OK

Node(const int val) : item(val)

{ next = NULL; }

} ;

Here the Node class can support the information field and the link to the next node in the linked list

It does not need any member functions with the exception of the constructor that initializes both data fields

Each instantiation of the Stack class is a friend of the non-template class Node and has access to its non-public members This might be useful if factoring out common code decreases the size (and compile/link time) of the object code Instead of heap memory allocated at instantiation (or at array overflow), the Stack class can allocate a Node object every time data is pushed on the stack and deallocate the top Node object when the data is popped from the stack

However, this is not very useful One type Node (e.g., with the integer information field) cannot accommodate different types of objects that the client code wants to push on the stack This means that class Node has to be a template as well

This also means that class Node should be defined as a template class Then different types of

Stack objects can instantiate and access different types of the Node object with different types of the item field

Node<Type> *next; // Node *next' is also OK

Node(const Type& val) : item(val)

{ next = NULL; }

} ;

The technical term for this use of templates is unbounded types The parameter Type is independent

of parameter T, and each parameter can accept any actual values independently of each other

Come to think of it, this is more than you need Here, each instantiation of class Stack (e.g., for type float) has access to details of each instantiation of class Node (e.g., of class Point) This code does not implement a realistic model of the real world

We need to enforce a one-to-one mapping between related instantiations, so that a stack of integers

becomes a friend of an integer node only, not of a node with other types of the item field To

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 6

achieve that mapping, we can bind a friend (client) template class (in this case, Stack) to the same type(s) as the template class that provides services (in this case, Node).

Node<Type> *next; // Node *next' is also OK

Node(const Type& val) : item(val)

{ next = NULL; }

} ;

Here, for each instantiation of Node to a specific type (e.g., of class Point,) the Stack instantiation

to the same type (class Point) is made a friend to this instantiation of class Node.

Class Stack now has a data member of class Node pointer that is initialized to zero in the Stack

constructor When the next node is pushed on the stack, this pointer points to the new node (and the new node points to the node that used to be the first node in the list) The member function

isEmpty() checks whether this pointer is NULL or points to a node This means that function pop()

has to set this pointer to NULL when the last node is removed from the stack

int isEmpty() const

{ return top == NULL; } // does top point to a node?

~Stack();

} ;

As for any template, the use of Node outside of Node definition must be qualified with the

parameter list This is why the Stack data member top cannot be of type Node*¡Xit should be of type Node<T>*, where T is the Stack type parameter As a result of this qualification in the Stack

definition, instantiation of a Stack class object results in the automatic instantiation of a Node class data field of the same type

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 7

The Stack method push() allocates a new Node object on the heap The call to the Node

constructor initializes the item field of the Node object to the value of the push() parameter The

next field of the new Node is set to point to the node that the Stack field top is currently pointing

to, and the top field is reset to point to the new Node object

template <class T>

void Stack<T>::push (const T& val)

{ Node<T> *p = new Node<T>(val); // type Node<T>, not Node

if (p == NULL)

{ cout << "Out of memory\n"; exit(1); }

p->next = top; // point it to first node

top = p; } // point to new node

There is no need to test for array overflow in push() because there is no array in this

implementation There is still the need to test whether the allocation of the Node object is

successful Notice the type of the pointer¡Xit is not Node*; it is Node<T>*. Similarly, when the heap space is requested by the operator new, it is of type Node<T>, not of type Node, The type T

will be provided at the time of Stack instantiation

The Stack method pop() sets the local pointer (of type Node<T>, not just Node) to point to the first node of the stack, copies the information field into a local variable (of type T), moves the top field

to point to the second node, and deletes the top node because it is not needed anymore

template <class T>

T Stack<T>::pop() // return value of type T

{ Node<T> *p = top; // Node of type T, not Node

T val = top->item; // get the value of type T

top = top->next; // point to the second node

delete p; // return top node to heap

Trang 8

The Stack destructor has to scan the linked list of remaining nodes and return them to the heap The local pointer p of type Node<T> with the component of type T is used again It is set to point to the first node of the list Notice that the pointer to the first node of the list, the data member top, is

of the same type as pointer p: Node<T>. In a while loop, the top pointer moves to the next node, the node pointed to by the pointer p is deleted, and pointer p moves to point to the next node as well

template <class T>

Stack<T>::~Stack()

{ Node<T> *p = top; // type Node of type T

while (top != NULL) // top is 0 when no nodes

{ top = top->next; // point to the next node

delete p; // delete the previous node

p = top; } } // traverse to the next node

Another approach is to provide each client with its own private server class If the Node definition

is nested within the private section of the client, then only that client (and its friends) can access class Node. This again raises the issue of coordination (mapping) between template definitions

Nested Template Classes

With nested design, the definition of the template class Node is nested within the definition of the container class that handles Node objects Since the Node definition is entirely within the scope of the container class (e.g., Stack,) the name Node is not visible to other potential clients (e.g., Queue

and List.) Hence, Node members can be made public, and there is no need to declare its single client (e.g., Stack) as a friend to class Node.

This is the first attempt on the design with nested classes Class Node is defined using the keyword

struct and all its members are public

template <class T>

class Stack {

template <class Type> // Is it legal? Is it needed?

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 9

struct Node {

Type item;

Node<Type> *next; // type depends on instantiation

Node(const Type& val) : item(val)

int isEmpty() const

{ return top == NULL; } // does top point to a node?

Node should be one-to-one rather than one-to-many: Class Stack needs a Node object that is

instantiated to the same actual type as class Stack itself

A good way to achieve that is to define class Stack as a template and then define class Node as a regular non-template class that uses the Stack type parameter for its data member and for its method parameter types

template <class T>

class Stack {

struct Node { // it depends on parameter type

T item; // same type as in Stack

Node *next; // Node<T> is incorrect here Node(const T& val) : item(val) // same type as in Stack

int isEmpty() const

{ return top == NULL; } // does top point to a node? ~Stack();

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 10

} ;

Each instantiation of the Stack templates generates a Node class that uses the same type as the

Stack actual type argument There is no need to qualify the type Node within the Stack definition

The same is true of Stack member functions When a local pointer is defined in a member

function, it is defined as a pointer to type Node, not a pointer to the type Node<T>. For example, method push() is almost the same as in the previous version, but the pointer p is defined

differently I commented out the previous version of the pointer definition so that you can compare both versions

template <class T>

void Stack<T>::push (const T& val)

// { Node<T> *p = new Node<T>(val); // type Node<T>, not Node

{ Node *p = new Node(val); // type Node, not Node<T>

if (p == NULL)

{ cout << "Out of memory\n"; exit(1); }

p->next = top; // point it to first node

top = p; } // point to new node

The same is true of other methods of the container class Listing 17.5 shows the implementation of the template class Stack with a nested class Node. The output of the program is shown in Figure 17-5

Figure 17.5 Output for program in Listing 17.5

Example 17.5 Example of a template class with a nested server class.

#include <iostream>

using namespace std;

template <class T>

class Stack {

struct Node { // it depends on parameter type

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 11

T item; // same type as in Stack Node *next; // Node<T> is incorrect here Node(const T& val) : item(val) // same type as in Stack { next = NULL; } } ;

Node *top; // it is not a template now public:

Stack() // default: no initial length

{ top = NULL; }

void push(const T&);

T pop();

int isEmpty() const

{ return top == NULL; } // does top point to a node? ~Stack();

} ;

template <class T>

void Stack<T>::push (const T& val)

// { Node<T> *p = new Node<T>(val); // type Node<T>, not Node { Node *p = new Node(val); // type Node, not Node<T>

if (p == NULL)

{ cout << "Out of memory\n"; exit(1); }

p->next = top; // point it to first node top = p; } // point it to new node

template <class T>

T Stack<T>::pop() // return value of type T // { Node<T> *p = top; // type Node<T>, not Node { Node *p = top; // type Node, not Node<T>

T val = top->item; // get the value of type T top = top->next; // point to the second node delete p; // return top node to heap return val; }

template <class T>

Stack<T>::~Stack()

// { Node<T> *p = top; // type Node of type T

{ Node *p = top; // type Node of type T

while (top != NULL) // top is 0 when no nodes

{ top = top->next; // point to the next node

delete p; // delete the previous node

p = top; } } // traverse to the next node

int main()

{

int data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 } ;

Stack<int> s; // stack object

int n = sizeof(data)/sizeof(int); // number of components

cout << "Initial data: ";

for (int j = 0; j < n; j++)

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 12

{ cout << data[j] << " "; } // print input data

cout << endl;

for (int i = 0; i < n; i++)

{ s.push(data[i]); } // push data on the stack

cout << "Inversed data: ";

while (!s.isEmpty()) // pop until stack is empty

Templates with Static Members

If a template class declares static data, each template instantiation will have a separate set of these static members All objects that belong to this particular instantiation will share the same static member(s), but they will have no access to static members that belong to an instantiation for a different actual type parameter

For example, class Stack can declare its top data member static This is an interesting design

alternative¡Xwith the top data member static, the data fields item and next can be moved to the class Stack itself as a non-static data member What is left in class Node then? Nothing It becomes redundant Hence, this design allows one to get rid of class Node.

In this design, class Stack combines the roles of the Stack in previous examples (calls to push(), pop(), and isEmpty() member functions) and the role of the Node class (fields item and next.) This is why it has two constructors: the default constructor and the conversion constructor

The default constructor is called when a Stack object is instantiated in the client code It does not have to do anything, but it has to be there to eliminate a syntax error The conversion constructor is called from method push(): When a new node has to be allocated, push() creates a new Stack

object rather than a new Node object The constructor initializes the item field (to the value to be stored) and the next field (to point to the top stack node)

The pop() function deletes the top node using a local pointer The type of pointer is a pointer to

Stack<T>. Since it is a pointer to an object of type Stack, the Stack destructor (instead of the

Node destructor as in previous versions) is called In the previous versions, the Stack destructor

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 13

deleted remaining stack nodes Here, this is harmful This is the reason that class Stack does not have a destructor (It has the default destructor that does nothing.)

template <class T> class Stack {

static Stack *top; // static data member

T item; // from Node

Stack *next; // from Node

public:

Stack() { } // create object in client

Stack(const T& val)

: item(val), next(top) // create new node in push()

{ top = this; }

void push(const T& val)

{ Stack<T> *p=new Stack<T>(val); } // no Node<T>, no Node

T pop()

{ Stack<T> *p = top;

T val = top->item; // no Node<T>, no Node

top = top->next; // point to second node

delete p; // delete top node: destructor

return val; }

int isEmpty() const

{ return top == NULL; }

void remove() // no call to destructor

{ Stack<T> *p = top; // trailing pointer

while (top != NULL)

{ top = top->next; // go to next node

delete p; // delete previous node

p = top; } } // catch up with next node

} ;

The absence of the destructor creates the danger of a memory leak To avoid it, class Stack

provides the remove() method that does the same thing as the Stack destructor in the previous versions The drawback of this design is that the client code has to explicitly call the remove()

method to dispose of remaining nodes on the stack (if any)

Initialization of the static member of the template class takes place not at the beginning of the program execution (as for static members of regular classes) but when a template object is

instantiated, because it is at this moment that the static member for this particular actual type is created

The syntax of the initialization statement (in a header file) should:

ϒΠ indicate that the static member belongs to template

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 14

ϒΠ specify the type of the static member

ϒΠ specify the scope of the static member

ϒΠ specify its name and the initial value

This is how the initialization statement for the stack static data member top looks: Its type is

Stack<T>*, its scope is Stack<T>, its name is top, and its initial value is NULL.

template <class T> // it belongs to template

Stack<T>* Stack<T>::top = NULL;

Consider, for example, a template class Array that contains a set of data (of a component type) and allows the client code to check whether a given element (of the component type) can be found in the collection

template <class T>

class Array {

T *data; // heap array of data

int size; // size of the array

Trang 15

Array(T items[], int n) : size(n) // conversion constructor

{ data = new T[n]; // allocate heap memory

if (data==0)

{ cout << "Out of memory\n"; exit(1); }

for (int i=0; i < n; i++) // copy input data

data[i] = items[i]; }

int find (const T& val) const

{ for (int i = 0; i < size; i++)

if (val == data[i]) return i; // return index if found

return -1; } // otherwise return -1

~Array()

{ delete [] data; }

} ;

This template class contains only a constructor, a method find(), and a destructor The

constructor allocates heap space sufficient for input data and copies the input array into the heap memory The method find() searches the heap array; if the parameter value is not found, it returns 1; if the value is found, the method returns the index of the value The destructor returns the heap memory

The client code instantiates the Array object of type int, initializes it, and prints the results of the search for the given value

int main()

{ int data1[] = { 1, 2, 3, 4, 5 } ;

int n1 = sizeof(data1)/sizeof(int); // number of components

cout << "Initial data: ";

for (int j = 0; j < n1; j++)

{ cout << data1[j] << " "; } // print input data

cout << endl;

Array<int> a1(data1,n1); // array object

int item1 = 3; int idx;

Array object is instantiated for the component of the character array type, both the constructor and the find() method have a problem

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 16

container will point to the same character array in the client space Similarly, the find() method will compare the string addresses rather than the string contents You see that for a character array

as an Array component, the general form of the template class does not work¡Xit needs copying strings in the constructor and comparing strings in find().

C++ supports the concept of specialization to deal with the type arguments that need special

treatment For each special class, a separate specialized class template should be provided The syntax for describing the specialization is a mixture of the syntax for the template class itself (with the template parameter list) and the template initialization in the client code (with the actual type list) You take the type parameter from the template parameter list and move it into the actual type list If the brackets of the template parameter list become empty as a result, this is fine For

example, the Array template class header:

template <class T> // remove class T from brackets

class Array { // append <char*> to class name

becomes

template <> // empty template parameter list

class Array<char*> { // actual type list

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 17

Array<char*> a1(data2,n2); // specialized template object

Listing 17.6 shows the complete program that contains the template Array and its specialized

template for the components of the character array type The test driver initializes the template class object a1 and the specialized template object a2 and sends messages to each object The output of the program is shown in Figure 17-6

Figure 17.6 Output for program in Listing 17.6

Example 17.6 Example of a template class specialization.

#include <iostream>

using namespace std;

template <class T>

class Array {

T *data; // heap array of data

int size; // size of the array

{ cout << "Out of memory\n"; exit(1); }

for (int i=0; i < n; i++)

data[i] = items[i]; }

int find (const T& val) const

{ for (int i = 0; i < size; i++)

Trang 18

} ;

template <> // empty template list

class Array <char *> { // type of specialization char* *data; // heap array of data

int size; // size of the array

Array(const Array&);

operator = (const Array&);

public:

Array(char* items[], int n) : size(n) // conversion

{ data = new char*[n]; // allocate heap memory

if (data==0)

{ cout << "Out of memory\n"; exit(1); }

for (int i=0; i < n; i++)

{ int len = strlen(items[i]); // special for strings only data[i] = new char[len+1];

strcpy(data[i],items[i]); } }

int find (const char*& val) const

{ for (int i = 0; i < size; i++)

if (strcmp(val,data[i])==0) // special for strings only return i;

char* data2[] = { "one", "two", "three", "four", "five" } ;

int n1 = sizeof(data1)/sizeof(int); // number of components int n2 = sizeof(data2)/sizeof(char*);

cout << "Initial data: ";

for (int j = 0; j < n1; j++)

{ cout << data1[j] << " "; } // print input data

cout << endl;

for (int i = 0; i < n2; i++)

{ cout << data2[i] << " "; } // print input data

cout << endl;

Array<int> a1(data1,n1); // array object

Array<char*> a2(data2,n2); // specialized object

int item1 = 3; int idx;

char* item2 = "three";

Trang 19

}

C++ also supports partial specializations They provide specific treatment for only one of several type parameters For example, the template class DictEntry from Listing 17.4 supports two type parameters:

For the Key type instantiated to character array, a specialized template is created by moving the Key

parameter from the template parameter list and appending the specialized type (in angle brackets)

to the class name

template <class Data> // remove the Key type

class DictEntry <char*>{ // append specialized type

char* key; // replace the Key type

template < > // remove both parameter types

class DictEntry <char*, char*>{ // append specialized types

char* key; // replace the Key type

char* info; // replace the Data type

public:

¡K} ; // the rest of the class

If only the second parameter should be treated in a special way, this is not a problem, but you have

to repeat the first type parameter in the list of actual types

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 20

template <class Key>

class DictEntry <Key, char*> {

general template class to generate the object

Using specialization is often a necessity (when one component type requires special treatment) Using specializations makes programs larger, more complex, and more difficult to understand Not all compilers support specializations well When one of the data types requires special treatment (most often, it is a character array), consider writing a separate class with a separate name, for

example, CharArray. The advantage of writing a separate class is that there is no doubt which class is used to instantiate the object The disadvantage is that there is no guarantee that similar features are treated similarly in different special classes

Sometimes, this tradeoff is difficult to make C++ specialized classes give you one way to resolve the problem Decide for yourself what you want to use

Template Functions

A stand-alone nonmember function can be defined as a template; the syntax is similar to the syntax

of template class member functions

When the function needs a prototype, it also contains the template parameter list with each class

keyword followed by a parameter

Trang 21

Both the definition and the prototype (forward declaration) start with the template keyword

followed by the formal parameter list in angle brackets; each formal parameter consists of the

keyword class followed by a programmer-defined identifier The keyword class and the

identifier are separated by commas The identifier must occur only once in the parameter list

template <class T, class T> // this is illegal

void swap(T& x, T& y)

{ T a = x; x = y; y = a; }

Each type parameter must be used in the parameter list of the template function If the type

parameter is not present in the parameter list, the compiler flags it as a syntax error

template <class T>

int isEmpty(void); // compile-time error for global function

Similar to non-template functions, template functions can be declared extern, inline, or

static; the specifier (if any) follows the template list of formal parameters and precedes the

function return type

int a=5, b=10; double c=3.0, d=4.0;

swap(a,b); // instantiation for integers

swap(c,d); // instantiation for double

Since compiler knows the types of the actual arguments a and b for the first call and c and d for the

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 22

second call, it generates code for swap(int&,int&) and swap(double&,double&).

A return value is not considered for parameter matching; conversions can be done as needed

However, implicit conversions are not used for template arguments If the compiler cannot decide which function to generate to match arguments exactly, it is a syntax error

template < >

inline void swap <char*> (char* x, char* y)

{ char* a = new char[strlen(x)+1];

char* b = new char[strlen(y)+1];

if (b==NULL) { cout << "Out of memory\n"; exit(1); }

strcpy(a,x); strcpy(b,y); // caller must assure space

Trang 23

Client code:

char x[20]="Hello!", y[20]="Hi, there!"; int a=5, b=10;

swap(a,b); // general template function is instantiated

swap(x,y); // specialized template function is instantiated

The compiler first searches for a non-template function; if one is found and parameters match exactly, templates are not considered; if more than one non-template alternative matches, it is a syntax error

If no matching non-template alternative is found, then templates are examined If there is an exact match and its instantiation already exists, it is used and no new object code is generated; otherwise, the function is instantiated; if more than one match is found, it is an error

If no matching template function is found, non-template functions are examined using implicit conversions and promotions of arguments

A template function cannot be called by or be passed as an argument to a non-template function

Summary

In this chapter, we looked at a powerful tool for code reuse: C++ templates The underlying idea is very simple and attractive: If the algorithms should be the same for different types, you should write it only once and later indicate for what actual type you want this algorithm to be used

This is the ideal, but the practical use of this idea faces a number of difficulties The syntax of C++ templates is complex The use of specializations complicates matters even more Sometimes

figuring out which specialization will be called in each case becomes a chore Sometimes, what works on one machine under one compiler will not work on another machine under a different compiler

In addition, the use of templates entails space and performance penalty This is why many C++ programmers try to avoid templates On the other hand, templates are utilized in the Standard

Template Library (STL), and you have to understand the basic principles of using templates to be able to handle the STL library correctly

It is a powerful tool Use it with care

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 24

Chapter 18 Programming with Exceptions

Topics in this Chapter

ϒΠ A Simple Example of Exception Processing

ϒΠ Syntax of C++ Exceptions

ϒΠ Exceptions with Class Objects

ϒΠ Type Cast Operators

ϒΠ Summary

In this chapter, I will deal with a relatively new C++ topic: programming with exceptions

Exception is a language mechanism that allows the programmer to separate source code that

describes the main case of processing from source code that describes exceptional situations

Exceptional situations are situations that should not occur during normal processing but from time

to time do occur Separating this exception processing from the main case makes the main case easier to read and to maintain It also makes exceptional cases easier to read and to maintain

This definition is somewhat vague, is it not? It does leave room for interpretation Indeed, what some people view as an exceptional or abnormal situation, other people perceive as a genuine part

of system operations For example, when you allocate memory on the heap, the algorithm should describe what happens if the request is satisfied Since it is possible that the computer runs out of memory, the algorithm should also specify what happens when memory is not available Is running out of memory an exception? Most people would say yes

Similarly, when the program reads data interactively from the online user, the algorithm specifies the processing of valid data What happens if the user makes a mistake and inputs data that is

invalid? Is this an exception? Most people would say no, online mistakes are a way of life, and the algorithms for processing these mistakes should be viewed as part of basic system functionality, not something that happens only rarely

Similarly, when you read data from a file in a loop, the algorithm specifies what happens when the next record is read¡Xhow different parts of the record should be processed Since it is possible that there are no more records in the file, the algorithm should define what happens when there are no more records to read Is reaching the end-of-file indeed an exception? Most people would say no, this is an event that marks the end of one stage of processing (reading file records in) and the

beginning of the next stage of processing (computations on data in memory)

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 25

Regardless of whether the programmer perceives the situation as mainline processing with some additional exceptional cases (the first example) or as a set of different cases of approximately equal importance (the second and the third examples), the issue of clogging the source code with diverse computational tasks is both real and important.

To be able to make intelligent decisions on structuring your algorithms, you should have a good understanding of the tools available in the programming language This is why I will try first and foremost to explain what exceptions (as a C++ programming technique) are, what syntax they

impose on the programmer, how to use them correctly, and what incorrect uses you should try to avoid

Initially, C++ did not support exceptions and relied on C mechanisms for exception processing by using global variables accessible to the whole program (e.g., errno) or jumps and calls to special functions whose names are fixed but whose contents might be specified by the programmer (e.g.,

setjmp and longjmp)

The C++ exception facility is a relatively new language feature Similar to C++ templates, the

exception mechanism is complex The experience in using exceptions is rather limited, and the advantages of their use for system design are not demonstrated yet In addition, the use of C++ exceptions increases the program execution time and the size of the executable program This is why I do not think you should use exceptions at every opportunity that presents itself Eventually, however, they should become a legitimate part of your programming toolbox

A Simple Example of Exception Processing

Usually, processing algorithms use C++ flow control statements, mostly the if or switch

statements, to separate normal processing of data from processing of erroneous or faulty data For multistep algorithms, the segment of source code for main algorithm and for exceptional conditions are written in alternative branches of the same source code, and this often makes code harder to read¡Xthe main line is lost in the multitude of exceptional and rare cases

When something goes wrong in a function, the function might not know what to do about the error Aborting the program might be a good solution in many situations, for example, trying to push an item onto a system stack that turns out to be full On the other hand, aborting the program might not release resources held by the program, such as opened files or database locks

Another approach is setting an error code or returning an error value for the caller to check and to take a recovery action if this action is possible For example, when the client code makes an

attempt to pop an item from an empty stack, returning an error value might be an attractive

alternative However, this is not always feasible If any return value of the given type is legal for the pop function, there may be no special value to be returned to signal an exceptional condition to

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 26

Setting a global variable, e.g., errno, to indicate an error does not work for concurrent programs

It is also hard to implement consistently for sequential programs because it requires that the client code diligently check the value of the global variable These checks clog the client code and make

it more difficult to understand

By using library functions such as setjmp and longjmp, the program can transfer control to an action that would release external resources and perform error recovery, but this would unwind the stack without calling destructors for objects created on the stack before these functions were called Hence, the resources held by these objects might not be properly released

Let us consider a simple example and review the issues that exception processing techniques should resolve Listing 18.1 shows a program that interactively prompts the user for the values of a

numerator and denominator of the fraction and computes and prints the fraction's value To

compute the result, the program uses two server functions, inverse() and fraction(). The first function returns the inverse of its argument It is called by the second function, fraction(), which multiplies its first argument by the value returned by inverse().

This is, of course, a somewhat convoluted design for such a simple computational problem A

simpler design would not let me demonstrate different options of exception processing A more complex problem would justify a more complex design but would drown me (and you) in a mire of details

In this problem, the zero value of the denominator is not acceptable and is rejected with a message

A negative value of the denominator is not acceptable either: If the fraction is negative, it is the numerator that should be made negative The negative denominator value should be rejected with a message that also prints the offending value

The input loop continues until the user enters a letter instead of numeric input data The cout

statement returns zero, and the break statement terminates the loop The sample output of the

program is shown in Figure 18-1

Figure 18-1 Output for program in Listing 18.1

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 27

Example 18.1 Example of a program with error processing in the client code.

#include <iostream>

using namespace std;

inline void inverse(long value, double& answer)

{ answer = 1.0/value; } // answer = 1/value

inline void fraction (long numer,long denom,double& result)

{ inverse(denom, result); // result = 1.0 / denom

result = numer * result; } // result = numer/denom

int main()

{

while (true) // infinite loop

{ long numer, denom; double ans; // numerator and denominator cout << "Enter numerator and positive\n"

<< "denominator (any letter to quit): ";

if ((cin >> numer >> denom) == 0) break; // enter data

if (denom > 0) { // correct input

fraction(numer,denom,ans); // compute result

cout << "Value of the fraction: " << ans <<"\n\n";

}

else if (denom == 0) // invalid result

cout << "\nZero denominator is not allowed\n\n";

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 28

Error recovery here is done by printing an error message and repeating the request for the next input data The mainline code (a call to the fraction() server function) here is not separated from error-processing code, but it does not result in serious problems.

Often, an error can be discovered only after some processing, in the server code, far from the place where the error actually originated Some of these errors could be processed at the place of their discovery, but some might require additional knowledge that might be absent in the server function that discovered the error In this case, the information about the error should be passed back to the client code for processing and, if possible, recovery I will model such a situation by moving the test of input data from client code to the server function inverse().

Listing 18.2 demonstrates this approach to error processing Function inverse() computes the inverse of its argument If the value of the argument is zero, inverse() uses the DBL_MAX constant (defined in the header file cfloat or float.h) as the inverse value Then it checks the answer to determine the validity of the result and tells the caller what happened during the call

If the answer is DBL_MAX, the inverse() function processes the error by printing an error message and returning the zero value to tell the caller about it If the argument is negative, the inverse()

function returns its value¡Xthe client will figure that out and will process the error Otherwise,

inverse() returns 1, and this will tell the caller that the value of the formal argument answer is valid

Function fraction() evaluates the return value of inverse(). If this value is 1 (the valid result),

it computes the value of the fraction If the returned value is negative (a negative denominator), it passes this value to its own client and sends to the client additional data for error processing (the message to be printed) The client code evaluates the return value of fraction(). If it is 1, the results are valid, and the main function displays the result If the return value of fraction() is negative, the client code prints this value and the message it received from fraction().

Otherwise, the client code does not do anything because the error (zero denominator) was already processed in inverse(). The result of a sample run of the program in Listing 18.2 is shown in

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 29

Figure 18-2.

Figure 18-2 Output for program in Listing 18.2

Example 18.2 Example of a program with errors discovered by the server code.

#include <iostream>

#include <cfloat>

using namespace std;

inline long inverse(long value, double& answer)

{ answer = (value) ? 1.0/value : DBL_MAX;

if (answer==DBL_MAX)

{ cout << "\nZero denominator is not allowed\n\n";

return 0; } // zero denominator else if (value < 0)

{ return value; } // negative denominator else

return 1; } // valid denominator

inline long fraction (long n,long d,double& result,char* &msg)

{ long ret = inverse(d, result); // result = 1.0 / d

if (ret == 1) // valid denominator { result = n * result; } // result = n / d

if (ret < 0)

msg = "\nNegative denominator: ";

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 30

cout << "Enter numerator and positive\n"

<< "denominator (any letter to quit): ";

if ((cin >> numer >> denom) == 0) break; // enter data

ret = fraction(numer,denom,ans,msg); // compute answer

if (ret == 1) // valid answer

cout << "Value of the fraction: " << ans <<"\n\n";

dependent on each other The client code has to abide by complex conventions on return values (in this example, returning 1 denotes the valid argument value, returning zero or a negative number denotes an invalid argument value) and behave differently for different return values This makes the client code more complex and requires additional documentation so that client programmers and server programmers use common conventions successfully

Another problem with this approach is that the server code, in this example, the inverse() and

fraction() functions, is involved not only in error discovery but also in communications with the user about the causes of the error For this simple example of three functions this is probably not a grave problem In more complex programs, it is important to make sure that each function performs one function (pun intended) only The function that computes the inverse of its argument should know how to compute the inverse of its argument and should not get into the user interface The function that is responsible for the user interface should know what to tell the user and should not

be involved in other computations These responsibilities have to be separated

Yet another problem with this approach is that the components of the user interface are spread all over the code of the program When the program has to be repackaged into French, Spanish,

Russian, or another language, there is no specific place in the program that has to be changed¡Xall program source code has to be inspected and modified This is asking for trouble

Listing 18.3 represents an attempt to eliminate the last two drawbacks It also gives you an

additional example of using static data members and static member functions All output strings used by the program are moved into a class, MSG, as a private static array of strings The class

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 31

provides a public static function, msg(), whose argument indicates the index of the string to be used If the index is incorrect, an error message is produced instead of the expected information.

You see that the server functions are not involved in the user interface anymore The code that analyzes the situation, unfortunately, stays, but there was little that could be done about this If the code is required to discover an error, the code should test some relevant values, and that makes the code more obscure

You also see that all the components of the user interface are swept into one place This facilitates not only the adaptation of the program to other languages but also maintenance of the user interface

in general If a prompt to a user has to be changed, it is only class MSG that changes If a message has to be added or removed, the static array MSG::data[] is edited, and the number of array

components in the MSG::msg() method changes accordingly To avoid this change, the number of components in the array (defined as local in msg()) can be computed as

keeping it as a literal value is not dangerous

Notice the elements of the utilization of static data and methods: the keyword static,

initialization of data outside the class boundaries, the use of the class name in the initialization statement and in the calls to the static function, the absence of object class MSG in the application, the lack of name conflict between the function MSG::msg(), and a local variable msg in the client code

The output of this version of the program is the same as the output for two previous versions of the application This is why it is not shown again

Example 18.3 Example of extensive communications between the client and the server code.

else

return data[n]; } // return valid string } ;

char* MSG::data [] = { "\nBad argument to msg()\n",

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 32

"\nZero denominator is not allowed\n\n", // depository of text

"\nNegative denominator: ",

"Enter numerator and positive\n",

"denominator (any letter to quit): ",

"Value of the fraction: "

} ;

inline long inverse(long value, double& answer, char* &msg)

{ answer = (value) ? 1.0/value : DBL_MAX;

return 1; } // valid denominator

inline long fraction (long n,long d,double& result,char* &msg)

{ long ret = inverse(d, result,msg); // result = 1.0 / d

if (ret == 1) // valid denominator

{ result = n * result; } // result = n / d

cout << MSG::msg(3) << MSG::msg(4); // prompt user for data

if ((cin >> numer >> denom) == 0) break; // enter data

ret = fraction(numer,denom,ans,msg); // compute answer

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 33

both its parameter msg and its return value to communicate with its caller (and its caller's caller).

It is the use of extra parameters, return values, and complex calling conventions that C++

exceptions help to eliminate

Syntax of C++ Exceptions

C++ exceptions allow the programmer to change the flow of control when some event occurs, for example, an error These errors happen at run time (file is not found, index is invalid, etc.); when C++ raises an exception, the program may terminate if it has no handler that knows how to deal with that exception

Exception handlers are segments of program source code that should be executed when the

exception is raised, for example, printing a message to the user, collecting information for the

analysis of the causes of the exception, or error recovery

Organizing error-related source code into exception handlers can make flow of control more

logical; instead of doing all the tests in the mainline of the algorithm and thus obscuring its

meaning, error handling is coded in a separate place The tradeoff of this approach is the possibility

of obscuring the meaning of server functions that are involved in error discovery

This separate place for error recovery can be in the same method that caused the exception, in a caller of that method, in a caller of the caller, and so on This flexibility makes the design with exceptions more difficult However, the exception mechanism allows the programmer to transfer control to recovery actions in a disciplined way

Presumably, C++ exceptions allow the programmer to isolate exceptional cases in other, remote segments of source code and streamline the base processing It should make the program more readable so that it is easier to understand I am not sure that this is often the case As I mentioned earlier, the utility of using exceptions is in eliminating extra parameters, return values, and complex calling conventions among the functions that discover the problem and the functions that try to recover from the problem

When C++ raises or "throws" an exception, it can create an object of a predefined class Exception

or of a programmer-defined class This programmer-defined class can be derived from class

Exception, or it can be an independent class Again, this flexibility makes designing with

exceptions more open ended and hence more difficult to understand

The exception might be thrown explicitly with the throw statement or implicitly as the result of an illegal or invalid action The exception is caught with the catch statement, and control is

transferred to the statement that caught the exception The catch statement (or block of statements)

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 34

performs the error recovery (if any) The return of control after the error recovery in the catch block depends on the structure of the program In general, it does not return by itself to the place where the exception occurred This is why the programmer has to structure the program in a specific way

if such return (e.g., to continue processing) is desirable

There are three operations related to exception handling:

ϒΠ throwing an exception

ϒΠ catching an exception

ϒΠ claiming an exception

Throwing an exception means specifying that certain exceptional (possibly erroneous) conditions

are discovered and that they should be processed using the C++ exception mechanism rather than common control flow techniques

Catching an exception means specifying a segment of code that is designed to respond to a

particular exceptional condition but not to other conditions

Claiming an exception means specifying what exception can be raised within this method; it helps

the compiler (and the client programmer and the maintenance programmer) know what to expect from the function and how it should be used

Throwing an Exception

To raise an exception, the keyword throw is used Its usage indicates that the server code has

discovered a condition that it does not know how to handle, and it throws the exception in the hope that someplace (among its client or its client's clients) there will be a segment of code that knows how to handle the situation

The keyword throw is used in a throw statement Its general syntactic form includes the keyword

throw with an operand that can be a value of any type to be thrown in search of the exception

Trang 35

executes the throw statement to notify the client of the problem discovered in the server code.

The throw statement can take only a single operand of any type However, some compilers do not flag the throw statement as an error if you try to throw more than one value The value of the

throw operand is used by the client code (that tries to process the exception) to retrieve information about the context of the error Often, this information is used to define the client code behavior in error recovery

Here is a modified example of the function inverse(). In Listing 18.3, this function sets up the return values or parameter values to communicate with the client code In this version, the function

inverse() throws exceptions in two cases: (1) if it discovers that the denominator is zero and (2) if

it discovers that the denominator is negative

inline void inverse(long value,double& answer) // two parameters

{ answer = (value) ? 1.0/value : DBL_MAX;

if both throw statements threw the values of the same type If both exceptions have to be processed the same way, this is not a problem If the exceptions have to be processed differently, the client code would have to figure out what really occurred in the server code that threw the exception

If you compare this function inverse() with its version in Listing 18.1, you will see that their interfaces are similar: Both functions return a void type and have only two parameters In Listing 18.1, the function inverse() did not try to discover any exceptions Neither did its client

fraction(). It was the job of the main() client code to discover both exceptions (zero

denominator, negative denominator) and process them

In Listing 18.2 and Listing 18.3, functions inverse() and fractions() were trying to discover exceptions, recover from some of them (zero denominator) and let the main() client recover from others (negative denominator) The result was greater coupling and more confusing code The last version of inverse() throws both exceptions It does have some analysis code (to decide what exception to throw, if any), but its interface is as simple as in the first version in Listing 18.1 This simplicity will be paid for by the additional code I will write to catch and to claim these exceptions

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 36

Catching an Exception

The function inverse(), which can throw two exceptions, has a direct client: function

fraction() that calls inverse() and an indirect client: function main() that calls fraction().

In general, the hierarchy of calls can be arbitrarily high If a function, in this example inverse(),

throws an exception and does not process this exception itself, one of its callers (direct or indirect) has to catch this exception

Catching an exception is a process of finding the code that can handle the error (the exception

handler); this is done by searching through a chain of function calls

One might think that catching an exception requires the keyword catch. This is true: C++ does have the keyword catch, and this keyword is used in catching the exception But this is not

enough When a function catches exceptions, it cannot catch them from an arbitrary source of

exceptions The function has to indicate from what segment of its code it will try to catch

exceptions This requires the use of yet another C++ keyword: try. This keyword should be

followed by a block that can throw exceptions

The client code that has the responsibility of catching errors encloses the code that can raise the

exception in a try statement

void foo() // function that catches exceptions

{ try // the try statement

{ statements; } // statements that throw exceptions

¡K} // the rest of foo() with catch blocks

C++ exception handlers are implemented using the keywords try and catch; the statements (or method calls) that may throw exceptions are put in the try blocks; the exception handlers

themselves are put into the catch blocks

Syntactically, one or more catch blocks should follow a try block Each catch block has a parameter

of the type that corresponds to the exception that this catch block handles

void foo() // function that catches exceptions

{ try

{ statements; } // statements that throw exceptions

catch (Type1 t1) // catch block for thrown type Type1

{ handler_for_Type1(); }

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 37

catch (Type2 t2) // catch block for thrown type Type2

The try statement must be followed by at least one catch construct (block), which provides

exception handling It is an error to use a catch block that is not preceded by a try statement (It is all right if there are other catch blocks between this one and the preceding try statement.) It is an error to use a try statement that is not followed by a catch block or blocks

Recall that the throw statement has an argument of some type¡Xa character array, a long value, or even a value of some programmer-defined class type The value of the argument usually carries some data about the context of the error In the case of the inverse() function, this data is either a string with the message to be printed or the negative denominator value to be displayed If the

throw statement throws an object of a class type, the constructor for that type should enable the object to carry some information about the problem This information might be used by the catch construct for diagnostics and error recovery

If there are several catch constructs after the try block, they have to have arguments of different types Since catch constructs do not have names, the signatures of these constructs must be unique

If the exception type thrown in the try block "matches" the argument of a catch construct, the code

in the catch construct is executed and the search stops; when the catch block terminates, the

statements that follow the catch blocks for this try statement are executed

"Matching the argument" means that the exception object that is thrown by the try block can be assigned to the parameter of the catch block, meaning the exact match, any of the standard

conversions, or any of subclasses of the parameter of the catch construct For example, a double

value can be caught by a catch block with a long parameter, and a SavingsAccount object can be caught by a catch block with an Account parameter

After the catch block terminates, the statements that follow the try block and its catch constructs are executed These statements can have other try blocks (followed by catch constructs) if necessary If the try statement did not throw any exception, the catch constructs are treated as the null

statements¡Xthey are skipped

If the exception was thrown in the middle of the try statement, the execution of the try statement is terminated, the catch construct is found and executed, and so on; the statements in the try block that follow the one that threw the exception are never executed Usually, this is only logical: The

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 38

exception was thrown because these statements could not be executed.

What happens if the code in the try block throws an exception that does not have a catch construct

of the appropriate type? Too bad¡Xthe function is terminated: Not only is the try block not executed

in its entirety, but the code that follows the catch constructs is not executed either This means that the appropriate catch block will be searched for in the client code of the function If it is found, all

is fine and well If the catch construct capable of handling the exception is not found even in

main(), the program is terminated

Let us consider, for example, the following version of the inverse() function that throws the

exceptions and tries to catch them

inline void inverse(long value,double&answer) // two parameters

{ try // start of try block

{ if (value == 0) // zero denominator

{ cout << str; } // zero denominator

catch (long val)

{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value

If the first argument has a legitimate value, the try block is executed completely, and the catch

blocks are skipped There are no statements following the catch blocks, so the function terminates

as if it had no exception handling at all

If the first argument is zero, the character array exception is thrown and the first catch block is executed Notice that the catch block is a "block"¡Xit has its own scope, and it refers to its

parameter str rather than to the variable that has actually been thrown: MSG::msg(1).

Similarly, if the first argument is negative, its value is thrown, and the second catch block is

executed Again, the name of the value to be printed is val rather than value. No matter what exception is thrown, the statement answer = 1.0/value is never executed This is reasonable because this statement should be executed only if the value passes all the tests

If the statements in the try block throw an exception that does not have a catch block to handle it in the function inverse(), the search for the exception handler continues in fraction() and then in

main().

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 39

In this version of the inverse() function, the throw statements and the catch blocks are in the same function scope Syntactically, this is legal C++ From the software engineering point of view, this is overkill¡Xthere is no need to use the exception handling mechanism if the information about exception is not passed across different functions In this case, a simple if statement within

inverse() would give the same results

Another problem with this version of exception handling is related to the execution of the rest of the program After the function inverse() terminates, its callers, fraction() and main(), have

no idea whether any exceptions were raised Meanwhile, if any exception was raised, the statement that computes the answer was not executed, and callers of inverse() should know about that

Next, let us again consider the version of inverse() that throws exceptions but does not catch them

inline void inverse(long value,double& answer) // two parameters

{ answer = (value) ? 1.0/value : DBL_MAX;

inverse(denom, result); // result = 1.0 / denom

result = numer * result; } // result = numer / denom

catch (char* str)

{ cout << str; } // zero denominator

catch (long val)

{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value

This is not better than the previous version of inverse(). The exceptions should be processed in such places in the client code where the information about the exception can be used to change the behavior of the program, in this case, skipping the display of the result of computations

Listing 18.4 demonstrates this example with exception handling in the main() function As I noted earlier, this example is somewhat artificial because main() could discover that the input is invalid

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 40

itself Assuming it cannot do that, this scheme of exception handling makes sense: inverse()

discovers the error and sends information to main() so that main() could skip the use of invalid results

In Listing 18.4, function inverse() analyzes the situation and throws two expressions for the

benefit of its callers Its immediate caller, fraction(), does not have any exception handlers

(catch constructs) because it is in the function main() where the statement to be skipped is located Since fraction does not have any catch constructs, it does not have a try statement either because it would be illegal to have a try statement without catch constructs

If inverse() does not throw exceptions, fraction() and main() continue to compute and to print the result and to request the next set of data If inverse() throws an exception, it is not processed

in inverse() because it does not have appropriate catch constructs The search propagates to

fraction(). Since fraction() does not have any exception handlers, the search propagates to

main(). If main() does not have any exception handlers either, the program terminates

When the search percolates to main(), it finds there both the try statement and the catch

constructs From the point of view of main(), it is its server function fraction() that is the

source of trouble The client main() does not care whether fraction() received the exception from one of its servers or threw it itself If fraction() throws an exception, the execution of the try block is terminated before the answer is displayed The corresponding exception handler prints

a message that uses the information generated in inverse().

Example 18.4 Example of throwing and catching exceptions.

static char* msg(int n) // public static method

{ if (n<1 || n > 5) // check index validity

return data[0];

else

return data[n]; } // return valid string

} ;

char* MSG::data [] = { "\nBad argument to msg()\n",

"\nZero denominator is not allowed\n\n", // depository of text

"\nNegative denominator: ",

"Enter numerator and positive\n",

"denominator (any letter to quit): ",

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Ngày đăng: 12/08/2014, 11:20

TỪ KHÓA LIÊN QUAN