You now have the tools needed for the ATM simulation. The program should allow the user to enter three quantities: the maximum queue size, the number of hours the program will simulate, and the average number of customers per hour. The program should use a loop in which each cycle represents one minute. During each minute cycle, the program should do the following:
1. Determine whether a new customer has arrived. If so, add the customer to the queue if there is room; otherwise, turn the customer away.
2. If no one is being processed, take the first person from the queue. Determine how long the person has been waiting and set a wait_time
counter to the processing time that the new customer will need.
3. If a customer is being processed, decrement the wait_time
counter by one minute.
4. Track various quantities, such as the number of customers served, the number of customers turned away, cumulative time spent waiting in line, and cumulative queue length.
When the simulation cycle is finished, the program should report various statistical findings.
An interesting matter is how the program determines whether a new customer has arrived. Suppose that on average, 10 customers arrive per hour. That amounts to a customer every 6 minutes. The program computes and stores that value in the variable min_per_cust
. However, having a customer show up exactly every 6 minutes is unrealistic. What you really want (at least most of the time) is a more random process that averages to a customer every 6 minutes. The program uses this function to determine whether a customer shows up during a cycle:
bool newcustomer(double x)
{
return (std::rand() * x / RAND_MAX < 1);
}
Here’s how it works. The value RAND_MAX
is defined in the cstdlib
file (formerly stdlib.h
) and represents the largest value the rand()
function can return (0
is the lowest value). Suppose that x
, the average time between customers, is 6
. Then the value of rand() * x / RAND_MAX
will be somewhere between 0
and 6
. In particular, it will be less than 1 one-sixth of the time, on average. However, it’s possible that this function might yield two customers spaced 1 minute apart one time and two customers 20 minutes apart another time. This behavior leads to the clumpiness that often distinguishes real processes from the clocklike regularity of exactly one customer every 6 minutes. This particular method breaks down if the average time between arrivals drops below 1 minute, but the simulation is not intended to handle that scenario. If you did need to deal with such a case, you’d use a finer time resolution, perhaps letting each cycle represent 10 seconds.
Listing 12.12 presents the details of the simulation. Running the simulation for a long time period provides insight into long-term averages, and running it for short times provides insight into short-term variations.
// bank.cpp -- using the Queue interface
// compile with queue.cpp
#include <iostream>
#include <cstdlib> // for rand() and srand()
#include <ctime> // for time()
#include "queue.h"
const int MIN_PER_HR = 60;
bool newcustomer(double x); // is there a new customer?
int main()
{
using std::cin;
using std::cout;
using std::endl;
using std::ios_base;
// setting things up
std::srand(std::time(0)); // random initializing of rand()
cout << "Case Study: Bank of Heather Automatic Teller
";
cout << "Enter maximum size of queue: ";
int qs;
cin >> qs;
Queue line(qs); // line queue holds up to qs people
cout << "Enter the number of simulation hours: ";
int hours; // hours of simulation
cin >> hours;
// simulation will run 1 cycle per minute
long cyclelimit = MIN_PER_HR * hours; // # of cycles
cout << "Enter the average number of customers per hour: ";
double perhour; // average # of arrival per hour
cin >> perhour;
double min_per_cust; // average time between arrivals
min_per_cust = MIN_PER_HR / perhour;
Item temp; // new customer data
long turnaways = 0; // turned away by full queue
long customers = 0; // joined the queue
long served = 0; // served during the simulation
long sum_line = 0; // cumulative line length
int wait_time = 0; // time until autoteller is free
long line_wait = 0; // cumulative time in line
// running the simulation
for (int cycle = 0; cycle < cyclelimit; cycle++)
{
if (newcustomer(min_per_cust)) // have newcomer
{
if (line.isfull())
turnaways++;
else
{
customers++;
temp.set(cycle); // cycle = time of arrival
line.enqueue(temp); // add newcomer to line
}
}
if (wait_time <= 0 && !line.isempty())
{
line.dequeue (temp); // attend next customer
wait_time = temp.ptime(); // for wait_time minutes
line_wait += cycle - temp.when();
served++;
}
if (wait_time > 0)
wait_time--;
sum_line += line.queuecount();
}
// reporting results
if (customers > 0)
{
cout << "customers accepted: " << customers << endl;
cout << " customers served: " << served << endl;
cout << " turnaways: " << turnaways << endl;
cout << "average queue size: ";
cout.precision(2);
cout.setf(ios_base::fixed, ios_base::floatfield);
cout << (double) sum_line / cyclelimit << endl;
cout << " average wait time: "
<< (double) line_wait / served << " minutes
";
}
else
cout << "No customers!
";
cout << "Done!
";
return 0;
}
// x = average time, in minutes, between customers
// return value is true if customer shows up this minute
bool newcustomer(double x)
{
return (std::rand() * x / RAND_MAX < 1);
}
You might have a compiler that has not implemented bool
. In that case, you can use int
instead of bool
, 0
instead of false
, and 1
instead of true
. You might also have to use stdlib.h
and time.h
instead of the newer cstdlib
and ctime
. You might have to define RAND_MAX
yourself.
Here are a few sample runs of the program built from Listings 12.10, 12.11, and 12.12:
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 10
Enter the number of simulation hours: 100
Enter the average number of customers per hour: 15
customers accepted: 1485
customers served: 1485
turnaways: 0
average queue size: 0.15
average wait time: 0.63 minutes
Done!
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 10
Enter the number of simulation hours: 100
Enter the average number of customers per hour: 30
customers accepted: 2896
customers served: 2888
turnaways: 101
average queue size: 4.64
average wait time: 9.63 minutes
Done!
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 20
Enter the number of simulation hours: 100
Enter the average number of customers per hour: 30
customers accepted: 2943
customers served: 2943
turnaways: 93
average queue size: 13.06
average wait time: 26.63 minutes
Done!
Note that going from 15 customers per hour to 30 customers per hour doesn’t double the average wait time; it increases it by about a factor of 15. Allowing a longer queue just makes matters worse. However, the simulation doesn’t allow for the fact that many customers, frustrated with a long wait, would simply leave the queue.
Here are a few more sample runs of the program in Listing 12.12; they illustrate the short-term variations you might see, even though the average number of customers per hour is kept constant:
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 10
Enter the number of simulation hours: 4
Enter the average number of customers per hour: 30
customers accepted: 114
customers served: 110
turnaways: 0
average queue size: 2.15
average wait time: 4.52 minutes
Done!
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 10
Enter the number of simulation hours: 4
Enter the average number of customers per hour: 30
customers accepted: 121
customers served: 116
turnaways: 5
average queue size: 5.28
average wait time: 10.72 minutes
Done!
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: 10
Enter the number of simulation hours: 4
Enter the average number of customers per hour: 30
customers accepted: 112
customers served: 109
turnaways: 0
average queue size: 2.41
average wait time: 5.16 minutes
Done!
This chapter covers many important aspects of defining and using classes. Several of these aspects are subtle—even difficult—concepts. If some of them seem obscure or unusually complex to you, don’t feel bad; they affect most newcomers to C++ that way. Often the way you come to really appreciate concepts such as copy constructors is through getting into trouble by ignoring them. So some of the material in this chapter may seem vague to you until your own experiences enrich your understanding.
You can use new
in a class constructor to allocate memory for data and then assign the address of the memory to a class member. This enables a class, for example, to handle strings of various sizes without committing the class design in advance to a fixed array size. Using new
in class constructors also raises potential problems when an object expires. If an object has member pointers pointing to memory allocated by new
, freeing the memory used to hold the object does not automatically free the memory pointed to by the object member pointers. Therefore, if you use new
in a class constructor to allocate memory, you should use delete
in the class destructor to free that memory. That way, the demise of an object automatically triggers the deletion of pointed-to memory.
Objects that have members pointing to memory allocated by new
also have problems with initializing one object to another or assigning one object to another. By default, C++ uses memberwise initialization and assignment, which means that the initialized or the assigned-to object winds up with exact copies of the original object’s members. If an original member points to a block of data, the copy member points to the same block. When the program eventually deletes the two objects, the class destructor attempts to delete the same block of memory twice, which is an error. The solution is to define a special copy constructor that redefines initialization and to overload the assignment operator. In each case, the new definition should create duplicates of any pointed-to data and have the new object point to the copies. That way, both the old and the new objects refer to separate but identical data, with no overlap. The same reasoning applies to defining an assignment operator. In each case, the goal is to make a deep copy—that is, to copy the real data and not just pointers to the data.
When an object has automatic storage or external storage, the destructor for that object is called automatically when the object ceases to exist. If you allocate storage for an object by using new
and assign its address to a pointer, the destructor for that object is called automatically when you apply delete
to the pointer. However, if you allocate storage for class objects by using placement new
instead of regular new
, you also take on the responsibility of calling the destructor for that object explicitly by invoking the destructor method with a pointer to the object. C++ allows you to place structure, class, and enumeration definitions inside a class. Such nested types have class scope, meaning that they are local to the class and don’t conflict with structures, classes, and enumerations of the same name that are defined elsewhere.
C++ provides a special syntax for class constructors that can be used to initialize data members. This syntax consists of a colon followed by a comma-separated list of initializers. This is placed after the closing parenthesis of the constructor arguments and before the opening brace of the function body. Each initializer consists of the name of the member being initialized followed by parentheses containing the initialization value. Conceptually, these initializations take place when the object is created and before any statements in the function body are executed. The syntax looks like this:
queue(int qs) : qsize(qs), items(0), front(NULL), rear(NULL) { }
This form is obligatory if the data member is a nonstatic const
member or a reference, except that C++11 in-class initialization can be used for nonstatic const
members.
C++11 allows in-class initialization (that is, initialization in the class definition):
class Queue
{
private:
...
Node * front = NULL;
enum {Q_SIZE = 10};
Node * rear = NULL;
int items = 0;
const int qsize = Q_SIZE;
...
};
This is equivalent to using a member initialization list. However, any constructor using a membership initialization list will override the corresponding in-class initializations.
As you might have noticed, classes require much more care and attention to detail than do simple C-style structures. In return, they do much more for you.
1. Suppose a String
class has the following private members:
class String
{
private:
char * str; // points to string allocated by new
int len; // holds length of string
//...
};
a. What’s wrong with this default constructor?
String::String() {}
b. What’s wrong with this constructor?
String::String(const char * s)
{
str = s;
len = strlen(s);
}
c. What’s wrong with this constructor?
String::String(const char * s)
{
strcpy(str, s);
len = strlen(s);
}
2. Name three problems that may arise if you define a class in which a pointer member is initialized by using new
. Indicate how they can be remedied.
3. What class methods does the compiler generate automatically if you don’t provide them explicitly? Describe how these implicitly generated functions behave.
4. Identify and correct the errors in the following class declaration:
class nifty
{
// data
char personality[];
int talents;
// methods
nifty();
nifty(char * s);
ostream & operator<<(ostream & os, nifty & n);
}
nifty:nifty()
{
personality = NULL;
talents = 0;
}
nifty:nifty(char * s)
{
personality = new char [strlen(s)];
personality = s;
talents = 0;
}
ostream & nifty:operator<<(ostream & os, nifty & n)
{
os << n;
}
5. Consider the following class declaration:
class Golfer
{
private:
char * fullname; // points to string containing golfer's name
int games; // holds number of golf games played
int * scores; // points to first element of array of golf scores
public:
Golfer();
Golfer(const char * name, int g= 0);
// creates empty dynamic array of g elements if g > 0
Golfer(const Golfer & g);
~Golfer();
};
a. What class methods would be invoked by each of the following statements?
Golfer nancy; // #1
Golfer lulu("Little Lulu"); // #2
Golfer roy("Roy Hobbs", 12); // #3
Golfer * par = new Golfer; // #4
Golfer next = lulu; // #5
Golfer hazzard = "Weed Thwacker"; // #6
*par = nancy; // #7
nancy = "Nancy Putter"; // #8
b. Clearly, the class requires several more methods to make it useful. What additional method does it require to protect against data corruption?
1. Consider the following class declaration:
class Cow {
char name[20];
char * hobby;
double weight;
public:
Cow();
Cow(const char * nm, const char * ho, double wt);
Cow(const Cow c&);
~Cow();
Cow & operator=(const Cow & c);
void ShowCow() const; // display all cow data
};
Provide the implementation for this class and write a short program that uses all the member functions.
2. Enhance the String
class declaration (that is, upgrade string1.h
to string2.h
) by doing the following:
a. Overload the +
operator to allow you to join two strings into one.
b. Provide a stringlow()
member function that converts all alphabetic characters in a string to lowercase. (Don’t forget the cctype
family of character functions.)
c. Provide a stringup()
member function that converts all alphabetic characters in a string to uppercase.
d. Provide a member function that takes a char
argument and returns the number of times the character appears in the string.
Test your work in the following program:
// pe12_2.cpp
#include <iostream>
using namespace std;
#include "string2.h"
int main()
{
String s1(" and I am a C++ student.");
String s2 = "Please enter your name: ";
String s3;
cout << s2; // overloaded << operator
cin >> s3; // overloaded >> operator
s2 = "My name is " + s3; // overloaded =, + operators
cout << s2 << ".
";
s2 = s2 + s1;
s2.stringup(); // converts string to uppercase
cout << "The string
" << s2 << "
contains " << s2.has('A')
<< " 'A' characters in it.
";
s1 = "red"; // String(const char *),
// then String & operator=(const String&)
String rgb[3] = { String(s1), String("green"), String("blue")};
cout << "Enter the name of a primary color for mixing light: ";
String ans;
bool success = false;
while (cin >> ans)
{
ans.stringlow(); // converts string to lowercase
for (int i = 0; i < 3; i++)
{
if (ans == rgb[i]) // overloaded == operator
{
cout << "That's right!
";
success = true;
break;
}
}
if (success)
break;
else
cout << "Try again!
";
}
cout << "Bye
";
return 0;
}
Your output should look like this sample run:
Please enter your name: Fretta Farbo
My name is Fretta Farbo.
The string
MY NAME IS FRETTA FARBO AND I AM A C++ STUDENT.
contains 6 'A' characters in it.
Enter the name of a primary color for mixing light: yellow
Try again!
BLUE
That's right!
Bye
3. Rewrite the Stock
class, as described in Listings 10.7 and 10.8 in Chapter 10 so that it uses dynamically allocated memory directly instead of using string
class objects to hold the stock names. Also replace the show()
member function with an overloaded operator<<()
definition. Test the new definition program in Listing 10.9.
4. Consider the following variation of the Stack
class defined in Listing 10.10:
// stack.h -- class declaration for the stack ADT
typedef unsigned long Item;
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Item * pitems; // holds stack items
int size; // number of elements in stack
int top; // index for top stack item
public:
Stack(int n = MAX); // creates stack with n elements
Stack(const Stack & st);
~Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
bool push(const Item & item); // add item to stack
// pop() returns false if stack already is empty, true otherwise
bool pop(Item & item); // pop top into item
Stack & operator=(const Stack & st);
};
As the private members suggest, this class uses a dynamically allocated array to hold the stack items. Rewrite the methods to fit this new representation and write a program that demonstrates all the methods, including the copy constructor and assignment operator.
5. The Bank of Heather has performed a study showing that ATM customers won’t wait more than one minute in line. Using the simulation from Listing 12.10, find a value for number of customers per hour that leads to an average wait time of one minute. (Use at least a 100-hour trial period.)
6. The Bank of Heather would like to know what would happen if it added a second ATM. Modify the simulation in this chapter so that it has two queues. Assume that a customer will join the first queue if it has fewer people in it than the second queue and that the customer will join the second queue otherwise. Again, find a value for number of customers per hour that leads to an average wait time of one minute. (Note: This is a nonlinear problem in that doubling the number of ATMs doesn’t double the number of customers who can be handled per hour with a one-minute wait maximum.)
18.220.53.93