《C++PrimerPlus 6th Edition》第12章 类和动态内存分配 要点记录

本章内容

  1. 对类成员使用动态内存分配
  2. 隐式和显式复制构造函数
  3. 隐式和显示重载赋值运算符
  4. 在构造函数中使用 new 所必须完成的工作
  5. 使用静态类成员
  6. 将定位 new 运算符用于对象
  7. 使用指向对象的指针
  8. 实现队列抽象数据类型(Abstract Data Type)

本章前半部分通过对于字符串类设计方式的讨论与改进(由 StringBad 改进为 String ),从而展现了动态分配内存时的潜在问题——指针变量的浅复制(按成员复制)引发的灾难,并指出了解决方案——深度复制(定义显式复制构造函数和重载赋值运算符)

12.1 动态内存和类 (StringBad)

StringBad类的设计:

  1. 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);
    };
    
  2. 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;
    }
    
  3. 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";
    }
    
  4. 运行结果:

    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),表明程序试图访问禁止它访问的内存单元。)
    
  5. 存在的问题

    • 默认复制构造函数只提供浅复制,意味着如果成员变量有指针变量,那么这些不同对象中同名的指针变量共享分配的内存,这显然不符合我们的期望,因此需要显式声明复制构造函数(见12.2节String(const String &);)。

    • 对于 kont = headlineStringBad metoo = knot;,前者将调用自动生成的类重载赋值运算符,后者的行为不确定(可能调用复制构造函数或赋值运算符),因此有必要显式编写类重载赋值运算符((见12.2节String &operator=(const String &);))。

本节主要知识点:

  • 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。注意:静态成员变量初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字 static。声明如下:
    int StringBadd::num_strings = 0;

  • 删除对象时可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,若用 new 动态分配内存,则需使用delete 释放内存。

  • 使用一个对象来初始化另一个对象时,编译器将自动生成(或调用已显式定义的) ⌈ \lceil 复制构造函数 ⌋ \rfloor 。C++自动提供下面这些成员函数(如果没有定义的话):

    • 默认构造函数
    • 默认析构函数
    • 复制构造函数:函数按值传递对象或函数返回对象时将被调用
    • 赋值运算符:
    • 地址运算符 (疑问:是否仅仅是指 & ? )
  • 将已有的对象赋给另一个对象时,将使用重载的赋值运算符;初始化对象时,可能会使用赋值运算符,也可能会使用复制构造函数。

12.2 改进后的新 String 类 (String)

String类的设计:

  1. 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();
    };
    
  2. 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;
    }
    
  3. 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;
    }
    
  4. 运行结果:

    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 时应注意的事项

注意事项:

  • newdelete 匹配

  • 多个构造函数必须以相同方式使用 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;
    

    对于上述代码的解释:

    1. 没有编写这种语句的理由,但并非所有代码都是合理的;

    2. 复制构造函数将创建一个临时对象来表示返回值。因此,在上述代码种,表达式 force1 + force2 的结果为一个临时对象。在两个语句中,net 将被赋给该临时对象;

    3. 使用完临时对象后,将在其作用域末尾将它丢弃。如果担心这种行为引发误用,可以将返回类型声明为 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!
      

      可以初步得知,临时对象将在产生它的指令执行到末尾时销毁。(如果不对的话,请务必指出哦~)

小结:

  1. 如果方法或函数要返回局部对象,则应返回对象而不是指向对象的引用;
  2. 如果方法或函数要返回一个没有公有复制构造函数的类(如 ostream)的对象,必须返回对象引用;
  3. 有些方法或函数既可以返回对象,又可以返回引用。在此情况下,优先考虑后者,因为其效率高。
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 运算符:
下面先后给出两段代码,前者存在一些问题,相互比对就可以知道定位运算符的正确使用方式了:

  1. 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
    
  2. 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
    

    说明:

    1. sizeof(JustTesting) == 40,因为std::string 为 32 字节,填充单元为 8 字节,而 int 为 4 字节,故会填充。

    2. 要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。

    3. 如果使用定位 new 运算符来为对象分配内存,必须确保其析构函数被调用。由于 delete 不能与定位 new 运算符配合使用,因此需要显式调用析构函数。

    4. 需要注意正确的删除顺序。对于使用定位 new 运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

12.6 复习各种技术

转换函数(详细介绍见第11章 11.6节):
operator type_name();
虽然该函数没有返回类型,但应返回所需类型的值。
使用转换函数时要小心,可以在声明构造函数时使用关键字 explicit,以防止它被用于隐式转换。

12.7 队列模拟

这一节首先给出队列类的接口形式,然后根据接口定义类成员函数,最后通过一个代码实例来应用它。

  1. 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
    
  2. 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;
    }
    
  3. 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!

本节知识点:

  1. 成员初始化列表(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){}

    注:

    1. 数据成员被初始化的顺序与它们出现在类声明的顺序相同,与初始化器中的排列顺序无关。
    2. 只有构造函数可以使用这种初始化列表语法。
    3. 对于被声明为引用的类成员,也必须使用这种语法,因为它和 const 类似,都只能在被创建时进行初始化。
    4. 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高(详见第14章)。
    5. 初始化列表使用括号的方式也可用于常规初始化,示例:
      int game(162);double talk(2.71828);
      这使得初始化内置类型就像初始化类对象一样。
    6. C++11 支持类内初始化,它和相应的成员初始化列表等价;但如果若调用了使用成员初始化列表的构造函数,则实际列表将覆盖这些默认初始值。示例:
      class Classy
      {
              
              	
      	int mem1 = 10;
      	const int mem2 = 20;
      };	
      
  2. 禁用类复制的方式。在 Queue.h 的私有域内定义了如下伪私有方法:

    class Queue
    {
          
          
    private:
    	Queue(const Queue& q):qsize(0){
          
          }
        Queue &operator=(const Queue &q) {
          
           return *this; }
    // ...
    };
    

    它的作用:①避免了本来将自动生成的默认方法定义;②这些方法是私有的,所以不能被广泛应用,即不允许下面这种情况出现(假设 niptuck 是 Queue 对象):

    Queue snick(nip); // not allowed
    tuck = nip; // not allowed
    

    C++11 提供了另一种禁用方法的方式——使用关键字 delete (详见第 18 章)。

    我们还得留心以下情况:①当对象被按值传递(或返回)时,复制构造函数将被调用;②复制构造函数还被用于创建其他的临时对象。只不过在上述例子 Queue 中并没有出现以上两种情况而已。

习题

点此进入github参考习题解答

欢迎各位大佬们于评论区进行批评指正~


上一篇文章:《C++PrimerPlus 6th Edition》第11章 使用类 要点记录

下一篇文章:《C++PrimerPlus 6th Edition》第13章 类继承 要点记录

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/109605092