《C++PrimerPlus 6th Edition》第12章 类和动态内存分配 要点记录
本章内容
- 对类成员使用动态内存分配
- 隐式和显式复制构造函数
- 隐式和显示重载赋值运算符
- 在构造函数中使用 new 所必须完成的工作
- 使用静态类成员
- 将定位 new 运算符用于对象
- 使用指向对象的指针
- 实现队列抽象数据类型(Abstract Data Type)
本章前半部分通过对于字符串类设计方式的讨论与改进(由 StringBad 改进为 String ),从而展现了动态分配内存时的潜在问题——指针变量的浅复制(按成员复制)引发的灾难,并指出了解决方案——深度复制(定义显式复制构造函数和重载赋值运算符)
12.1 动态内存和类 (StringBad)
StringBad类的设计:
-
StringBad.h
//StringBad.h -- flawed string class definition #pragma once #include<iostream> class StringBad { private: char *str; // pointer to string int len; // length of string static int num_strings; // number of objects public: StringBad(const char* s); // constructer StringBad(); // default constructer ~StringBad(); // destructer // friend function friend std::ostream &operator<< (std::ostream &os, const StringBad &st); };
-
StringBad.cpp
#include <cstring> #include <iostream> #include "StringBad.h" using std::cout; // initializing static class member int StringBad::num_strings = 0; // class methods // construct StringBad from C string StringBad::StringBad(const char* s){ len = strlen(s); str = new char[len + 1]; std::strcpy(str, s); num_strings++; cout << num_strings << ": \"" << str << "\" object created\n"; } StringBad::StringBad(){ // default constructor len = 3; str = new char[4]; std::strcpy(str, "C++"); num_strings++; cout << num_strings << ": \"" << str << "\" object created\n"; } StringBad::~StringBad(){ // destructor cout << "\"" << str << "\" object deleted, "; --num_strings; cout << num_strings << " left\n"; delete[] str; } std::ostream& operator<< (std::ostream& os, const StringBad& st) { os << st.str; return os; }
-
testStringBad.cpp
#include <iostream> using std::cout; #include "StringBad.h" void callme1(StringBad &); // pass by reference void callme2(StringBad); // pass by value int main(){ using std::endl; { cout << "Starting an inner block.\n"; StringBad headline1("Deep Dark Fantasy."); StringBad headline2("Do you like that huh?"); StringBad sports("Ass we can ~"); cout << "headline1: " << headline1 << endl; cout << "headline2: " << headline2 << endl; cout << "sports: " << sports << endl; callme1(headline1); cout << "headline1: " << headline1 << endl; callme2(headline2); cout << "headline2: " << headline2 << endl; cout << "Initialize one object to another:\n"; StringBad sailor = sports; cout << "sailor: " << sailor << endl; cout << "Assign one object to another:\n"; StringBad knot; knot = headline1; cout << "knot: " << knot << endl; cout << "Exiting the block.\n"; } cout << "End of main()\n"; std::cin.get(); return 0; } void callme1(StringBad& rsb){ cout << "String passed by reference:\n"; cout << " \"" << rsb << "\"\n"; } void callme2(StringBad sb){ cout << "String passed by value:\n"; cout << " \"" << sb << "\"\n"; }
-
运行结果:
Starting an inner block. 1: "Deep Dark Fantasy." object created 2: "Do you like that huh?" object created 3: "Ass we can ~" object created headline1: Deep Dark Fantasy. headline2: Do you like that huh? sports: Ass we can ~ String passed by reference: "Deep Dark Fantasy." headline1: Deep Dark Fantasy. String passed by value: "Do you like that huh?" "Do you like that huh?" object deleted, 2 left headline2: pA Initialize one object to another: sailor: Ass we can ~ Assign one object to another: 3: "C++" object created knot: Deep Dark Fantasy. Exiting the block. "Deep Dark Fantasy." object deleted, 2 left "Ass we can ~" object deleted, 1 left "+" object deleted, 0 left (至此抛出异常。按理来说会到-2个对象,但在对象个数将为-1个之前中断, 机器报告保护错误(GPF),表明程序试图访问禁止它访问的内存单元。)
-
存在的问题
-
默认复制构造函数只提供浅复制,意味着如果成员变量有指针变量,那么这些不同对象中同名的指针变量共享分配的内存,这显然不符合我们的期望,因此需要显式声明复制构造函数(见12.2节
String(const String &);
)。 -
对于
kont = headline
、StringBad metoo = knot;
,前者将调用自动生成的类重载赋值运算符,后者的行为不确定(可能调用复制构造函数或赋值运算符),因此有必要显式编写类重载赋值运算符((见12.2节String &operator=(const String &);
))。
-
本节主要知识点:
-
不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。注意:静态成员变量初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字
static
。声明如下:
int StringBadd::num_strings = 0;
-
删除对象时可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,若用
new
动态分配内存,则需使用delete
释放内存。 -
使用一个对象来初始化另一个对象时,编译器将自动生成(或调用已显式定义的) ⌈ \lceil ⌈ 复制构造函数 ⌋ \rfloor ⌋。C++自动提供下面这些成员函数(如果没有定义的话):
- 默认构造函数
- 默认析构函数
- 复制构造函数:函数按值传递对象或函数返回对象时将被调用
- 赋值运算符:
- 地址运算符 (疑问:是否仅仅是指
&
? )
-
将已有的对象赋给另一个对象时,将使用重载的赋值运算符;初始化对象时,可能会使用赋值运算符,也可能会使用复制构造函数。
12.2 改进后的新 String 类 (String)
String类的设计:
-
String.h
//String.h -- fixed and augmented String class definition #pragma once #include<iostream> using std::istream; using std::ostream; class String { private: char *str; // pointer to string int len; // length of string static int num_strings; // number of objects static const int CINLIM = 80; // cin input limit public: String(const char* s); // constructer String(); // default constructer String(const String &); //copy constructor ~String(); // destructer int length() const { return len; } // overloaded operator methods String &operator=(const String &); String &operator=(const char *); char &operator[](int i); const char &operator[](int i) const; // overloaded operator friends friend bool operator<(const String &st1, const String &st2); friend bool operator>(const String &st1, const String &st2); friend bool operator==(const String &st1, const String &st2); friend ostream &operator<< (ostream &os, const String &st); friend istream &operator>>(istream &is, String &st); // static function static int HowMany(); };
-
String.cpp
//String.cpp -- String class method #include "String.h" #include<iostream> #include<cstring> using std::cin; using std::cout; // Initializing static class member int String::num_strings = 0; // static method int String::HowMany(){ return num_strings; } // class methods String::String(const char* s){ len = strlen(s); str = new char[len + 1]; std::strcpy(str, s); num_strings++; } String::String(){ // default constructor len = 0; str = new char[1]; str[0] = '\0'; num_strings++; } String::String(const String& st){ num_strings++; len = st.len; str = new char[len + 1]; std::strcpy(str, st.str); } String::~String(){ // destructor --num_strings; delete[] str; } // overloaded operator methods String& String::operator=(const String& st){ if(this == &st) // cannot copy itself return *this; delete[] str; // delete the previous string len = st.len; str = new char[len + 1]; std::strcpy(str, st.str); return *this; } String& String::operator=(const char* s){ delete[] str; // delete the previous string len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); return *this; } // read-write char access for non-const String // not available for (const char) as it may update the char char& String::operator[](int i){ return str[i]; } // read-only char access for const String const char& String::operator[](int i) const{ return str[i]; } //overloaded operator friends bool operator<(const String& st1, const String& st2){ return (std::strcmp(st1.str, st2.str) < 0); } bool operator>(const String& st1, const String& st2){ return st2 < st1; } bool operator==(const String& st1, const String& st2){ return (std::strcmp(st1.str, st2.str) == 0); } // simple String output std::ostream& operator<< (std::ostream& os, const String& st) { os << st.str; return os; } // quick and dirty String input istream& operator>>(istream& is, String& st){ char temp[String::CINLIM]; is.get(temp, String::CINLIM); if(is) st = temp; while (is && is.get() != '\n') continue; return is; }
-
testString.cpp
// testString.cpp -- using expanded String class // compile with String.cpp #include<iostream> #include "String.h" const int ArSize = 10; const int MaxLen = 80; int main(){ using std::cin; using std::cout; using std::endl; String name; cout << "Hi, what's your name?\n>> "; cin >> name; cout << name << ", please enter up to " << ArSize << " short sayings <empty line to quit>:\n"; String sayings[ArSize]; // array of objects char temp[MaxLen]; // temporary string storage int i; for (i = 0; i < ArSize; ++i){ cout<<i+1<<": "; cin.get(temp, MaxLen); // the second condition only for those old version // where an empty line won't get cin return false while (cin && cin.get() != '\n') continue; if(!cin || temp[0] == '\0') break; else sayings[i] = temp; } int total = i; if(total > 0){ cout << "Here"<<(total>1?" are": " is") << " your"<<(total>1?" sayings":" saying")<<":\n"; for (i = 0; i < total; ++i) cout << sayings[i][0] << ": " << sayings[i] << endl; int shortest = 0; int first = 0; for (i = 1; i < total; ++i){ if(sayings[i].length() < sayings[shortest].length()) shortest = i; if(sayings[i]<sayings[first]) first = i; } cout << "Shortest saying:\n" << sayings[shortest] << endl; cout << "First alphabetically:\n" << sayings[first] << endl; cout << "This program used " << String::HowMany() << " String objects. Bye.\n"; } else cout << "No input! Bye.\n"; cin.clear(); cin.get(); cin.get(); return 0; }
-
运行结果:
Hi, what's your name? >> God Sheng God Sheng, please enter up to 10 short sayings <empty line to quit>: 1: haste makes waste 2: an apple a day keeps doctor away 3: out of sight, out of mindd 4: Here are your sayings: h: haste makes waste a: an apple a day keeps doctor away o: out of sight, out of mindd Shortest saying: haste makes waste First alphabetically: an apple a day keeps doctor away This program used 11 String objects. Bye.
本节主要知识点:
-
str = new char[1];
与str = new char;
的区别:前者与类析构函数兼容,后者不兼容。 -
将比较函数作为友元,有助于将
String
对象与常规的C字符串进行比较。if ("love" == answer::String)
→ \rightarrow →if(operator==("love", answer))
→ \rightarrow →if(operator==(String("love"), answer))
,最后的形式与原型匹配。 -
较早的
get(char*, int)
版本在读取空行后,返回的值不是false
。然而,对于这些版本来说,如果读取了一个空行,则字符串中第一个字符将是一个空字符,对应上述代码使用的if(!cin || temp[0] == '\0')
中第二个逻辑判断。
12.3 在构造函数中使用 new 时应注意的事项
注意事项:
-
new
、delete
匹配 -
多个构造函数必须以相同方式使用
new
,因为只有一个析构函数。 -
应当分别定义一个复制构造函数和一个赋值运算符,实现深度复制。对于前者,复制数据而非地址;对于后者,做到以下四步(实例参考12.2节相应部分):①检查自我赋值情况;②释放成员指针以前指向的内存;③复制数据而不仅仅是地址;④返回一个指向调用对象的引用。
默认的逐成员复制和赋值行为有一定的智能,如下示例:
class Magazine{
private:
String title;
string publisher;
...
};
如果您将一个Magazine
对象复制或赋值给另一个 Magazine
对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。然而, Magazine
类有时候需要自己定义复制构造函数和赋值运算符,这种情况将在第13章介绍。
12.4 有关返回对象的说明
本节主要讲了返回对象和返回引用的区别和适用情景。
-
返回对象将调用复制构造函数,而返回引用不会,而后者所做的工作更少,效率更高。
-
引用指向的对象应该在调用函数执行时存在,不能是被调函数的局部对象。
-
返回类型需要和返回对象匹配。
-
两种常见的返回非
const
对象(即返回非常量引用)的情形是,重载运算符以及重载与cout
一起使用的<<
运算符。前者这样做可以提高效率(连续赋值),后者必须这样做(ostream
没有公有的复制构造函数)。 -
如果被返回的对象是被调用函数的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。
-
对于
Vector Vector::operator+()
定义有一个奇异的属性:force1 + force2 = net; // three Vector objects cout<< (force1 + force2 = net).magval()<<endl;
对于上述代码的解释:
-
没有编写这种语句的理由,但并非所有代码都是合理的;
-
复制构造函数将创建一个临时对象来表示返回值。因此,在上述代码种,表达式
force1 + force2
的结果为一个临时对象。在两个语句中,net
将被赋给该临时对象; -
使用完临时对象后,将在其作用域末尾将它丢弃。如果担心这种行为引发误用,可以将返回类型声明为
const Vector
,这样示例中的表达式写法将引发错误。另外,关于临时对象的作用域,可以结合下面的一段代码对比运行结果来体会:#include <iostream> #include "Vector.h" int main(){ using namespace VECTOR; using namespace std; { Vector front1(4, 5), front2(3, 3); Vector net(7, 7); front1 + front2 = net + front2; cout << (front1 + front2 = net).magval() <<" -MARK1- "<< endl;; cout<<" -MARK2- "<< endl; //the output help to find when to destroy the temp object cout << "Not destroyed yet~" << endl; } cout << "Done!\n"; cin.get(); return 0; }
运行结果:
Vector (10, 10) destroyed~ Vector (10, 10) destroyed~ 9.89949 -MARK1- Vector (7, 7) destroyed~ -MARK2- Not destroyed yet~ Vector (7, 7) destroyed~ Vector (3, 3) destroyed~ Vector (4, 5) destroyed~ Done!
可以初步得知,临时对象将在产生它的指令执行到末尾时销毁。(如果不对的话,请务必指出哦~)
-
小结:
- 如果方法或函数要返回局部对象,则应返回对象而不是指向对象的引用;
- 如果方法或函数要返回一个没有公有复制构造函数的类(如
ostream
)的对象,必须返回对象引用; - 有些方法或函数既可以返回对象,又可以返回引用。在此情况下,优先考虑后者,因为其效率高。
12.5 使用指向对象的指针
本节的主要内容:①指向对象的指针;②定位 new
运算符。
对象指针的应用看代码基本就OK了(testString2.cpp):
// testString2.cpp -- using pointers to objects
#include<iostream>
#include <cstdlib>
#include <ctime>
#include "String.h"
const int ArSize = 10;
const int MaxLen = 81;
int main(){
using namespace std;
String name;
cout << "Hi, what's your name?\n>> ";
cin >> name;
cout << name << ", please enter up to " << ArSize
<< " short sayings <empty line to quit>:\n";
String sayings[ArSize]; // array of objects
char temp[MaxLen]; // temporary string storage
int i;
for (i = 0; i < ArSize; ++i){
cout<<i+1<<": ";
cin.get(temp, MaxLen);
// the second condition only for those old version
// where an empty line won't get cin return false
while (cin && cin.get() != '\n')
continue;
if(!cin || temp[0] == '\0')
break;
else
sayings[i] = temp;
}
int total = i;
if(total > 0){
cout << "Here"<<(total>1?" are": " is")
<< " your"<<(total>1?" sayings":" saying")<<":\n";
for (i = 0; i < total; ++i)
cout << sayings[i] << endl;
// use pointers to keep track of shortest, first strings
String *shortest = &sayings[0];
String *first = &sayings[0];
for (i = 1; i < total; ++i){
if(sayings[i].length() < shortest->length())
shortest = &sayings[i];
if(sayings[i] < *first)
first = &sayings[i];
}
cout << "Shortest saying:\n"
<< *shortest << endl;
cout << "First alphabetically:\n"
<< *first << endl;
srand(time(0));
int choice = rand() % total;
// use new to create, initialize new String object
String *favorite = new String(sayings[choice]);
cout << "My favorite saying:\n"
<< *favorite<<endl;
delete favorite;
}
else
cout << "No much to say, eh?\nBye.\n";
cin.clear();
cin.get();
cin.get();
return 0;
}
运行结果:
Hi, what's your name?
>> God Sheng
God Sheng, please enter up to 10 short sayings <empty line to quit>:
1: a friend in need is a friend in deed
2: neither a borrower nor a lender be
3: a stitch in time saves nine
4: cold hands, warm heart
5:
Here are your sayings:
a friend in need is a friend in deed
neither a borrower nor a lender be
a stitch in time saves nine
cold hands, warm heart
Shortest saying:
cold hands, warm heart
First alphabetically:
a friend in need is a friend in deed
My favorite saying:
a friend in need is a friend in deed
定位 new 运算符:
下面先后给出两段代码,前者存在一些问题,相互比对就可以知道定位运算符的正确使用方式了:
-
placenew1.cpp
// placenew1.cpp -- new, placement new, no delete #include <iostream> #include <string> #include <new> using namespace std; const int BUF = 512; class JustTesting{ private: string words; int number; public: JustTesting(const string &s = "Just Testing", int n = 0) { words = s; number = n; cout << words << " constructed\n"; } ~JustTesting() { cout << words << " destroyed\n"; } void Show() const { cout << words << ", " << number << endl; } }; int main(){ char *buffer = new char[BUF]; JustTesting *pc1, *pc2; pc1 = new (buffer) JustTesting; pc2 = new JustTesting("Heap1", 20); cout << "Memory block addresses:\n" << "buffer: " << (void *)buffer << " heap: " << pc2 << endl; cout << "Memory contents:\n"; cout << pc1 << ": "; pc1->Show(); cout << pc2 << ": "; pc2->Show(); JustTesting *pc3, *pc4; pc3 = new (buffer) JustTesting("Bad Idea", 6); pc4 = new JustTesting("Heap2", 10); cout << "Memory contents:\n"; cout << pc3 << endl; pc3->Show(); cout << pc4 << endl; pc4->Show(); delete pc2; delete pc4; delete[] buffer; cout << "Done\n"; return 0; }
运行结果:
Just Testing constructed Heap1 constructed Memory block addresses: buffer: 0xbe2328 heap: 0xbe2148 Memory contents: 0xbe2328: Just Testing, 0 0xbe2148: Heap1, 20 Bad Idea constructed Heap2 constructed Memory contents: 0xbe2328: Bad Idea, 6 0xbe21f0: Heap2, 10 Heap1 destroyed Heap2 destroyed Done
-
placenew2.cpp
// placenew2.cpp -- new, placement new, no delete #include <iostream> #include <string> #include <new> using namespace std; const int BUF = 512; class JustTesting{ private: // 40 bytes as 8 bytes per unit string words; // 32 bytes int number; // 4 bytes public: JustTesting(const string &s = "Just Testing", int n = 0) { words = s; number = n; cout << "number: " << sizeof(number) << endl; cout << words << " constructed\n"; } ~JustTesting() { cout << words << " destroyed\n"; } void Show() const { cout << words << ", " << number << endl; } }; int main(){ char *buffer = new char[BUF]; JustTesting *pc1, *pc2; pc1 = new (buffer) JustTesting; pc2 = new JustTesting("Heap1", 20); cout << "Memory block addresses:\n" << "buffer: " << (void *)buffer << " heap: " << pc2 << endl; cout << "Memory contents:\n"; cout << pc1 << ": "; pc1->Show(); cout << pc2 << ": "; pc2->Show(); JustTesting *pc3, *pc4; pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6); pc4 = new JustTesting("Heap2", 10); cout << "Memory contents:\n"; cout << pc3 << ": "; pc3->Show(); cout << pc4 << ": "; pc4->Show(); delete pc2; delete pc4; // explicitly destroy placement new objects // take care of the order! pc3->~JustTesting(); pc1->~JustTesting(); delete[] buffer; cout << "Done\n"; cin.get(); return 0; }
运行结果:
number: 4 Just Testing constructed number: 4 Heap1 constructed Memory block addresses: buffer: 0x10640e0 heap: 0x1064310 Memory contents: 0x10640e0: Just Testing, 0 0x1064310: Heap1, 20 number: 4 Better Idea constructed number: 4 Heap2 constructed Memory contents: 0x1064108: Better Idea, 6 0x1064370: Heap2, 10 Heap1 destroyed Heap2 destroyed Better Idea destroyed Just Testing destroyed Done
说明:
-
sizeof(JustTesting) == 40
,因为std::string
为 32 字节,填充单元为 8 字节,而int
为 4 字节,故会填充。 -
要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。
-
如果使用定位 new 运算符来为对象分配内存,必须确保其析构函数被调用。由于
delete
不能与定位new
运算符配合使用,因此需要显式调用析构函数。 -
需要注意正确的删除顺序。对于使用定位 new 运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。
-
12.6 复习各种技术
转换函数(详细介绍见第11章 11.6节):
operator type_name();
虽然该函数没有返回类型,但应返回所需类型的值。
使用转换函数时要小心,可以在声明构造函数时使用关键字 explicit
,以防止它被用于隐式转换。
12.7 队列模拟
这一节首先给出队列类的接口形式,然后根据接口定义类成员函数,最后通过一个代码实例来应用它。
-
Queue.h (
class Customer
&class Queue
)// Queue.h -- interface for a queue #ifndef QUEUE_H_ #define QUEUE_H_ // This queue will contain Customer items class Customer { private: long arrive; // arrival time for customer int processtime; // processing time for customer public: Customer() { arrive = processtime = 0; } void set(long when); long when() const { return arrive; } int ptime() const { return processtime; } }; typedef Customer Item; class Queue{ private: enum { Q_SIZE = 10 }; //ERR: 不能将 "Queue::Node *" 类型的值分配到 "Queue::Node *" 类型的实体 /* struct Node { Item item; struct Node *next; } ; */ typedef struct node // 不能缺少typedef,原因如上 { Item item; struct node *next; } Node; //private class members Node *front; Node *rear; int items; // current numbers of items in Queue const int qsize; // maximum number of items in Queue // preemptive definitions to prevent public copying Queue(const Queue& q):qsize(0){ } Queue &operator=(const Queue &q) { return *this; } public: Queue(int qs = Q_SIZE); ~Queue(); bool isempty() const; bool isfull() const; int queuecount() const; bool enqueue(const Item &item); // add item to end bool dequeue(Item &item); // remove item from front }; #endif
-
Queue.cpp
#include "Queue.h" #include <cstdlib> #include <iostream> Queue::Queue(int qs):qsize(qs){ front = rear = NULL; items = 0; } Queue::~Queue(){ Node *temp; while (front != NULL){ temp = front; front = front->next; delete temp; } } bool Queue::isempty() const{ return items == 0; } bool Queue::isfull() const{ return items == qsize; } int Queue::queuecount() const{ return items; } // add item to end bool Queue::enqueue(const Item &item){ if(isfull()) return false; Node *add = new Node; // on failure, new throws std::bad_alloc exception add->item = item; add->next = NULL; items++; if(front == NULL) front = add; else rear->next = add; rear = add; return true; } // remove item from front bool Queue::dequeue(Item &item){ if(front == NULL) return false; item = front->item; items--; Node *temp = front; front = front->next; delete temp; if(items == 0) rear = NULL; return true; } void Customer::set(long when){ processtime = std::rand() % 3 + 1; arrive = when; }
-
bank.cpp
// bank.cpp -- using the Queue interface // compile with queue.cpp #include<iostream> #include<cstdlib> // for rand() and srand() and RAND_MAX = Max(rand()) #include<ctime> // for time() #include<string> #include "Queue.h" const int MIN_PER_HR = 60; bool newcustomer(double x); // is there a new customer? template<typename T> void invalidInputHandle(T& a, std::string enterText, std::string errorText); int main(){ using std::cin; using std::cout; using std::endl; using std::ios_base; // setting things up std::srand(std::time(0)); cout << "Case Study: Bank of Heather Automatic Teller\n"; cout << "Enter maximum size of queue: "; int qs; cin >> qs; // invalid input process invalidInputHandle<int>(qs, "Enter maximum size of queue: ", "qs must be a positive integer!"); Queue line(qs); // line queue holds up to qs people cout << "Enter the number of simulation hours: "; int hours; // hours of simulation cin >> hours; // invalid input process invalidInputHandle<int>(hours, "Enter the number of simulation hours: ", "hours must be a positive integer!"); //simulation will run 1 cycle per minute long cyclelimit = MIN_PER_HR * hours; // number of cycles cout << "Enter the average number of customers per hour: "; double perhour; // average number of arrival per hour cin >> perhour; // invalid input process invalidInputHandle<double>(perhour, "Enter the average number of customers per hour: ", "perhour must be a positive real number!"); 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; // join 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)){ 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\n"; } else cout << "No customers!\n"; cout << "Done!\n"; cin.get(); // filter the ENTER cin.get(); // hold the console return 0; } // x = average time, in minutes, between customers // return value is true if customer shows up this minute bool newcustomer(double x){ return x*std::rand()/RAND_MAX < 1; } template<typename T> void invalidInputHandle(T& a, std::string enterText, std::string errorText){ using namespace std; while (!cin || a<=0){ if(!cin){ cin.clear(); } // 过滤剩余字节流 while (cin.get() != '\n') continue; cout << "Error input, "<<errorText<<"\n"; cout << enterText; cin >> a; } }
运行结果:
Case Study: Bank of Heather Automatic Teller
Enter maximum size of queue: ass
Error input, qs must be a positive integer!
Enter maximum size of queue: 10
Enter the number of simulation hours: 0
Error input, hours must be a positive integer!
Enter the number of simulation hours: 100
Enter the average number of customers per hour: -1.23
Error input, perhour must be a positive real number!
Enter the average number of customers per hour: 12.5
customers accepted: 1243
customers served: 1243
turnaways: 0
average queue size: 0.10
average wait time: 0.49 minutes
Done!
本节知识点:
-
成员初始化列表(member initializer list)可用于初始化
const
数据成员,因为非静态的const
成员必须在执行到构造函数体之前,即创建对象时进行初始化,示例:Queue::Queue(int qs):qsize(qs){front = rear = NULL; items = 0;}
⇔ \Leftrightarrow ⇔
Queue::Queue(int qs):qsize(qs), front(NULL), rear(NULL), items(0){}
注:
- 数据成员被初始化的顺序与它们出现在类声明的顺序相同,与初始化器中的排列顺序无关。
- 只有构造函数可以使用这种初始化列表语法。
- 对于被声明为引用的类成员,也必须使用这种语法,因为它和
const
类似,都只能在被创建时进行初始化。 - 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高(详见第14章)。
- 初始化列表使用括号的方式也可用于常规初始化,示例:
int game(162);
、double talk(2.71828);
这使得初始化内置类型就像初始化类对象一样。 - C++11 支持类内初始化,它和相应的成员初始化列表等价;但如果若调用了使用成员初始化列表的构造函数,则实际列表将覆盖这些默认初始值。示例:
class Classy { int mem1 = 10; const int mem2 = 20; };
-
禁用类复制的方式。在
Queue.h
的私有域内定义了如下伪私有方法:class Queue { private: Queue(const Queue& q):qsize(0){ } Queue &operator=(const Queue &q) { return *this; } // ... };
它的作用:①避免了本来将自动生成的默认方法定义;②这些方法是私有的,所以不能被广泛应用,即不允许下面这种情况出现(假设
nip
和tuck
是 Queue 对象):Queue snick(nip); // not allowed tuck = nip; // not allowed
C++11 提供了另一种禁用方法的方式——使用关键字
delete
(详见第 18 章)。我们还得留心以下情况:①当对象被按值传递(或返回)时,复制构造函数将被调用;②复制构造函数还被用于创建其他的临时对象。只不过在上述例子
Queue
中并没有出现以上两种情况而已。
习题
欢迎各位大佬们于评论区进行批评指正~