Bootstrap

【C++ Primer Plus】第12章 类和动态内存分配

12.1 动态内存和类

  1. 让程序在运行时决定内存分配,而不是在编译时决定。
  2. C++使用new和delete运算符来动态控制内存,在类中使用这些运算符,析构函数将是必不可少的。
  3. 类对象定义的字符串并不保存在对象中。字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。
  4. 在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[](包括中括号)来分配内存,则应使用delete[](包括中括号)来释放内存。

stringbad.h

#ifndef PRIMERPLUS_STRINGBAD_H
#define PRIMERPLUS_STRINGBAD_H
#include <iostream>
class StringBad
{
private:
    char *str;      				// 类声明没有为字符串本身分配存储空间
    int len;
    static int num_strings; 		// 无论创建了多少对象,程序都只创建一个静态类变量副本。
public:
    // 在构造函数中使用new来为字符串分配空间。
    StringBad();                    // 默认构造函数
    StringBad(const char * s);      // 自定义构造函数
    StringBad(const StringBad & s); // 复制构造函数
    StringBad & operator=(const StringBad & st);    // 赋值构造函数
    ~StringBad();

    friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};
#endif //PRIMERPLUS_STRINGBAD_H

stringbad.cpp

#include <cstring>
#include "stringbad.h"
using std::cout;

int StringBad::num_strings = 0; // 初始化静态变量,用于记录创建的类数量

StringBad::StringBad(const StringBad & st)
{
    num_strings++; 				// handle static member update
    len = st.len; 				// same length
    str = new char [len + 1]; 	// allot space
    std::strcpy(str, st.str); 	// copy string to new location
    cout << num_strings << ": \"" << str
         << "\" object created\n"; // For Your Information
}

StringBad & StringBad::operator=(const StringBad & st)
{
    if (this == &st) 			// object assigned to itself
        return *this; 			// all done
    delete [] str; 				// free old string
    len = st.len;
    str = new char [len + 1]; 	// get space for new string
    std::strcpy(str, st.str); 	// copy the string
    return *this; 				// return reference to invoking object
}

StringBad::StringBad()          // 在构造函数中使用new来为字符串分配空间
{
    len = 6;
    str = new char[6];
    std::strcpy(str, "happy");
    num_strings++;
    cout << num_strings << " : \"" << str << "\" object created.\n";
}

StringBad::StringBad(const char * s)
{
    // str = s;             // 这只保存了地址,而没有创建字符串副本。
    len = std::strlen(s);   // 不包括末尾的空字符
    str = new char[len+1];  // 使用new分配足够的空间来保存字符串,然后将新内存的地址赋给str成员。
    std::strcpy(str, s);
    num_strings++;
    cout << num_strings << " : \"" << str << "\" object created.\n";
}

StringBad::~StringBad()
{
    cout << "\"" << str << "\" object delete, ";
    --num_strings;
    cout << num_strings << " left.\n";
    delete [] str;
}

std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
    os << st.str;
    return os;
}

usestringbad.cpp

#include "stringbad.h"
using std::cout;

void callme1(StringBad & rsb);
void callme2(StringBad sb);

int main(void)
{
    using std::endl;
    {
        cout << "Starting an inner block.\n";
        StringBad headline1("Celery Stalks at Midnight");
        StringBad headline2("Lettuce Prey");
        StringBad sports("Spinach Leaves Bowl for Dollars");
        cout << "headline1: " << headline1 << endl;
        cout << "headline2: " << headline2 << endl;
        cout << "sports: " << sports << endl;
        callme1(headline1);
        cout << "headline1: " << headline1 << endl;
        callme2(headline2); // 复制构造函数被用来初始化 callme2()的形参
        cout << "headline2: " << headline2 << endl;
        cout << "Initialize one object to another:\n";
        StringBad sailor = sports;  // 复制构造函数,StringBad sailor = StringBad(sports);
        cout << "sailor: " << sailor << endl;
        cout << "Assign one object to another:\n";
        StringBad knot;
        knot = headline1;   // 赋值构造函数,knot.operator=(headline1);
        cout << "knot: " << knot << endl;
        cout << "Exiting the block.\n";
    }   // 该代码块执行完调用析构函数,否则要main函数执行完调用析构函数。
    cout << "End of main()\n";
    return 0;
}
void callme1(StringBad & rsb)
{
    cout << "String passed by reference:";
    cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
    cout << "String passed by value:";
    cout << " \"" << sb << "\"\n";
}

out:

Starting an inner block.
1 : "Celery Stalks at Midnight" object created.
2 : "Lettuce Prey" object created.
3 : "Spinach Leaves Bowl for Dollars" object created.
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
String passed by reference: "Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
4: "Lettuce Prey" object created
String passed by value: "Lettuce Prey"
"Lettuce Prey" object delete, 3 left.
headline2: Lettuce Prey
Initialize one object to another:
4: "Spinach Leaves Bowl for Dollars" object created
sailor: Spinach Leaves Bowl for Dollars
Assign one object to another:
5 : "happy" object created.
knot: Celery Stalks at Midnight
Exiting the block.
"Celery Stalks at Midnight" object delete, 4 left.
"Spinach Leaves Bowl for Dollars" object delete, 3 left.
"Spinach Leaves Bowl for Dollars" object delete, 2 left.
"Lettuce Prey" object delete, 1 left.
"Celery Stalks at Midnight" object delete, 0 left.
End of main()

12.1.1 静态类成员特点

  1. 无论创建了多少对象,程序都只创建一个静态类变量副本。
  2. 类的所有对象共享同一个静态成员
  3. 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。
  4. 初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
  5. 静态数据成员在类声明中声明,在包含类方法的文件中初始化。
  6. 初始化时使用作用域运算符 来指出静态成员所属的类。
  7. 但如果静态成员是整型或枚举型const,则可以在类声明中初始化。

12.1.2 特殊成员函数

C++自动提供了下面这些成员函数:

  1. 默认构造函数,如果没有定义构造函数;
  2. 默认析构函数,如果没有定义;
  3. 复制构造函数,如果没有定义;
  4. 赋值运算符,如果没有定义; (将一个对象赋给另一个对象)
  5. 地址运算符,如果没有定义。隐式地址运算符返回调用对象的地址(即this指针的值)。

1、默认构造函数

如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。但只能有一个默认构造函数。

2、复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。
例如:StringBad类的复制构造函数原型:StringBad(const StringBad &);

何时调用:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。

// 以下四种情况都将调用复制构造函数,假设motto是一个StringBad对象
StringBad ditto(motto);
StringBad metoo = motto; 			// 可能直接创建,也可能生成一个临时对象
StringBad also = StringBad(motto);	// 可能直接创建,也可能生成一个临时对象
StringBad * pStringBad = new StringBad(motto);	// 初始化一个匿名对象,并将新对象的地址赋给pstring指针。

每当程序生成了对象副本时,编译器都将使用复制构造函数。(当函数按值传递对象或函数返回对象时,都将使用复制构造函数。)
由于按值传递对象将调用复制构造函数,因此应该按引用传递对 象。这样可以节省调用构造函数的时间以及存储新对象的空间。

有何作用:默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态函数(如num_strings)不受影响,因为它们属于整个类,而不是各个对象。

必须定义复制构造函数的原因:

  1. 如果类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题。
  2. 必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
  3. 如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制
  4. 复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
// StringBad类的显式复制函数
StringBad::StringBad(const StringBad & s)
{
    num_strings++;	// 静态数据成员
    ...// important stuff to go here
}

3、赋值构造函数

  1. 将已有的对象赋给另一个对象时,将使用重载的赋值运算符。
  2. 初始化对象时,并不一定会使用赋值运算符 (也可能使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。)
  3. 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。
  4. 如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。

赋值构造函数的特点:

  1. 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[ ]来释放这些数据。
  2. 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
  3. 函数返回一个指向调用对象的引用。
// 赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。
StringBad & StringBad::operator=(const StringBad & st)
{
    if (this == &st) 			// object assigned to itself
    return *this; 				// all done
    delete [] str; 				// free old string
    len = st.len;
    str = new char [len + 1]; 	// get space for new string
    std::strcpy(str, st.str); 	// copy the string
    return *this; 				// return reference to invoking object
}

12.2 改进后的String类

12.2.1 空指针

  1. delete[]与使用new[]初始化的指针和空指针都兼容。
  2. 在C++98中,字面值0有两个含义:可以表示数字值零,也可以表示空指针。
  3. C++11引入新关键字nullptr,用于表示空指针。
char *str;
str = nullptr;	// 空指针
str = 0;		// 空指针
str = NULL;		// 空指针

12.2.2 比较成员函数

要实现字符串比较函数,最简单的方法是使用标准的trcmp()函数:

  1. 如果依照字母顺序,第一个参数位于第二个参数之前,则该函数返回一个负值;
  2. 如果两个字符串相同,则返回0;
  3. 如果第一个参数位于第二个参数之后,则返回一个正值。
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);
}

将比较函数作为友元,有助于将String对象与常规的C字符串进行比较。

12.2.3 使用中括号表示法访问字符

对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。

char & String::operator[](int i)
{
	return str[i];
}

// 调用
String opera("The Magic Flute");
cout << opera[4];	// cout << opera.operator[](4);

// 将返回类型声明为char &,便可以给特定元素赋值。
String means("might");
means[0] = 'r';		// means.operator[](0) = 'r';
means.str[0] = 'r';

// 提供另一个仅供const String对象使用的operator版本:
const char & String::operator[](int i) const
{
	return str[i];
}

// 有了const版本可以读/写常规String对象,对于const String对象,则只能读取其数据:
String text("Once upon a time");
const String answer("futile");
cout << text[1]; 	// ok, uses non-const version of operator[]()
cout << answer[1]; 	// ok, uses const version of operator[]()
cin >> text[1]; 	// ok, uses non-const version of operator[]()
//cin >> answer[1]; 	// compile-time error

12.2.4 静态类成员函数

  1. 可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static)
  2. 首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。
  3. 如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
  4. 由于静态成员函数不与特定的对象相关联,因此只能使用类中的静态数据成员。
// 在String类声明中添加如下原型
static int HowMany() { return num_strings; }
// 调用
int count = String::HowMany(); // invoking a static member function

12.2.5 进一步重载赋值运算符

重载赋值运算符,使之能够直接将常规字符串复制到String对象中。这样就不用创建和删除临时对象了。

String & String::operator=(const char * s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}

string1.h

#ifndef PRIMERPLUS_STRING1_H
#define PRIMERPLUS_STRING1_H
#include <iostream>
using std::ostream;
using std::istream;
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:
// constructors and other methods
    String(const char * s);     // constructor
    String();                   // default constructor
    String(const String &);     // copy constructor
    ~String();                  // destructor
    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 &st, const String &st2);
    friend bool operator>(const String &st1, const String &st2);
    friend bool operator==(const String &st, const String &st2);
    friend ostream & operator<<(ostream & os, const String & st);
    friend istream & operator>>(istream & is, String & st);
// static function
    static int HowMany();
};
#endif //PRIMERPLUS_STRING1_H

string1.cpp

// string1.cpp -- String class methods
#include <cstring> // string.h for some
#include "string1.h" // includes <iostream>
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) // construct String from C string
{
    len = std::strlen(s); // set size
    str = new char[len + 1]; // allot storage
    std::strcpy(str, s); // initialize pointer
    num_strings++; // set object count
}
String::String() // default constructor
{
    len = 4;
    str = new char[1];
    str[0] = '\0'; // default string
    num_strings++;
}
String::String(const String & st)
{
    num_strings++; // handle static member update
    len = st.len; // same length
    str = new char [len + 1]; // allot space
    std::strcpy(str, st.str); // copy string to new location
}
String::~String() // necessary destructor
{
    --num_strings; // required
    delete [] str; // required
}
// overloaded operator methods
// assign a String to a String
String & String::operator=(const String & st)
{
    if (this == &st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char[len + 1];
    std::strcpy(str, st.str);
    return *this;
}
// assign a C string to a String
String & String::operator=(const char * s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}
// read-write char access for non-const String
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
ostream & operator<<(ostream & os, const String & st)
{
    os << st.str;
    return os;
}
// quick and dirty String input
// 重载>>运算符提供了一种将键盘输入行读入到String对象中的简单方法。
// 它假定输入的字符数不多于String::CINLIM的字符数,并丢弃多余的字符。
// 在if条件下,如果由于某种原因
// (如到达文件尾或get(char *, int)读取的是一个空行)导致输入失败,istream对象的值将置为 false。
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;
}

usestring1.cpp

// sayings1.cpp -- using expanded String class
// compile with string1.cpp
// 程序首先提示用户输入,然后将用户输入的字符串存储到 String对象中,并显示它们,
// 最后指出哪个字符串最短、哪个字符串按 字母顺序排在最前面。
#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
    using std::cout;
    using std::cin;
    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);
        while (cin && cin.get() != '\n')
            continue;
        if (!cin || temp[0] == '\0')    // empty line?
            break;                      // i not incremented
        else
            sayings[i] = temp;          // overloaded assignment
    }
    int total = i;                      // total # of lines read
    if ( total > 0)
    {
        cout << "Here are your sayings:\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";
    return 0;
}

12.3 在构造函数中使用new时应注意的事项

  1. 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
  2. new和delete必须相互兼容。new对应于delete,new[ ]对应于delete[ ]。
  3. 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论是带中括号还是不带中括号)可以用于空指针。
  4. 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。具体地说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
  5. 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体地说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。

12.3.1 默认构造函数示例

String::String()
{
    len = 0;
    str = new char[1]; 	// uses new with []
    str[0] = '\0';
}
String::String()
{
    len = 0;
    str = 0; 			// or, with C++11, str = nullptr;
}
String::String()
{
    static const char * s = "C++"; 	// initialized just once
    len = std::strlen(s);
    str = new char[len + 1]; 		// uses new with []
    std::strcpy(str, s);
}

12.3.2 包含类成员的类的逐成员复制

假设类成员的类型为String类或标准string类:

  1. 如果您将一个 Magazine对象复制或赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。
  2. 如果Magazine类因其他成员需要定义复制构造函数和赋值运算符,情况将更复杂;在这种情况下,这些函数必须显式地调用String和string的复制构造函数和赋值运算符。
class Magazine
{
private:
    String title;
    string publisher;
    ...
};

12.4 有关返回对象的说明

当成员函数或独立的函数返回对象时:可以返回指向对象的引用、指向对象的const引用或const对象。

  1. 如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。
  2. 如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。
  3. 最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。

12.4.1 返回指向const对象的引用

如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率。

  1. 首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本所做的工作更少,效率更高。
  2. 其次,引用指向的对象应该在调用函数执行时存在。
  3. 第三,版本二v1和v2都被声明为const引用,因此返回类型必须为const,这样才匹配。
// 例如,假设要编写函数Max(),它返回两个Vector对象中较大的一个,
// 其中Vector 是第11章开发的一个类。
Vector force1(50,60);
Vector force2(10,70);
Vector max;
max = Max(force1, force2);

// version 1
Vector Max(const Vector & v1, const Vector & v2)
{
    if (v1.magval() > v2.magval())
    	return v1;
    else
    	return v2;
}
// version 2
const Vector & Max(const Vector & v1, const Vector & v2)
{
    if (v1.magval() > v2.magval())
    	return v1;
    else
    	return v2;
}

12.4.2 返回指向非const对象的引用

两种常见的返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。前者这样做旨在提高效率,而后者必须这样做。
Operator<<()的返回值用于串接输出:返回类型必须是ostream &,而不能仅仅是ostream。如果使用返回类型ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数。幸运的是,返回一个指向cout的引用不会带来任何问题,因为cout已经在调用函数的作用域内。

12.4.3 返回对象

如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。 因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。
下面的例子:
构造函数调用Vector(x + b.x,y + b.y)创建一个方法operator+()能够访问的对象;
而返回语句引发的对复制构造函数的隐式调用创建一个调用程序能够访问的对象。

Vector force1(50,60);
Vector force2(10,70);
Vector net;
net = force1 + force2;
// 返回的不是force1,也不是force2,force1和force2在这个过程中应该保持不变。
// 因此,返回值不能是指向在调用函数中已经存在的对象的引用。

// 在Vector::operator+( )中计算得到的两个矢量的和被存储在一个新的临时对象中,
// 该函数也不应返回指向该临时对象的引用,
// 而应该返回实际的Vector对象,而不是引用。
Vector Vector::operator+(const Vector & b) const
{
	return Vector(x + b.x, y + b.y);
}

12.4.4 返回const对象

上述返回对象的+运算符重载函数可以这样使用:

net = force1 + force2;			// 1: three Vector objects
force1 + force2 = net; 			// 2: dyslectic programming
cout << (force1 + force2 = net).magval() << endl; // 3: demented programming
  1. 复制构造函数将创建一个临时对象来表示返回值。因此,表达式force1 + force2的结果为一个临时对象。在语句1中,该临时对象被赋给net;在语句2和3中,net被赋给该临时对象。
  2. 使用完临时对象后,将把它丢弃。例如,对于语句2,程序计算force1和force2之和,将结果复制到临时返回对象中,再用net的内容覆盖临时对象的内容,然后将该临时对象丢弃。原来的矢量全都保持不变。语句3显示临时对象的长度,然后将其删除。
  3. 如果Vector::operator+()的返回类型被声明为const Vector,则语句1仍然合法,但语句2和语句3将是非法的。

12.5 使用指向对象的指针

如果Class_name是类,value的类型为Type_name,则下面的语句:
Class_name * pclass = new Class_name(value);
将调用如下构造函数:
Class_name(Type_name);或者Class_name(const Type_name &);
另外,如果不存在二义性,则将发生由原型匹配导致的转换(如从int到double)。下面的初始化方式将调用默认构造函数:
Class_name * ptr = new Class_name;

usestring2.cpp

// compile with string1.cpp
// 最初,shortest指针指向数组中的第一个对象。
// 每当程序找到比指向的字符串更短的对象时,就把shortest重新设置为指向该对象。
// 同样,first指针跟踪按字母顺序排在最前面的字符串。
// 这两个指针并不创建新的对象,而只是指向已有的对象。因此,这些指针并不要求使用new来分配内存。
#include <iostream>
#include <cstdlib>  // (or stdlib.h) for rand(), srand()
#include <ctime>    // (or time.h) for time()
#include "string1.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];
    char temp[MaxLen]; // temporary string storage
    int i;
    for (i = 0; i < ArSize; i++)
    {
        cout << i+1 << ":";
        cin.get(temp, MaxLen);
        while (cin && cin.get() != '\n')
            continue;
        if (!cin || temp[0] == '\0') // empty line?
            break; // i not incremented
        else
            sayings[i] = temp; // overloaded assignment
    }
    int total = i; // total # of lines read
    if (total > 0)
    {
        cout << "Here are your sayings:\n";
        for (i = 0; i < total; i++)
            cout << sayings[i] << "\n";
// use pointers to keep track of shortest, first strings
        String * shortest = &sayings[0]; // initialize to first object
        String * first = &sayings[0];
        for (i = 1; i < total; i++)
        {
            if (sayings[i].length() < shortest->length())
                shortest = &sayings[i];
            if (sayings[i] < *first) // compare values
                first = &sayings[i]; // assign address
        }
        cout << "Shortest saying:\n" << * shortest << endl;
        cout << "First alphabetically:\n" << * first << endl;
        srand(time(0));
        int choice = rand() % total; // pick index at random
// use new to create, initialize new String object
        String * favorite = new String(sayings[choice]);    // 指针favorite指向new创建的未被命名对象。
        cout << "My favorite saying:\n" << *favorite << endl;
        delete favorite;
    }
    else
        cout << "Not much to say, eh?\n";
    cout << "Bye.\n";
    return 0;
}

12.5.1 再谈new和delete

String * favorite = new String(sayings[choice]);
这不是为要存储的字符串分配内存,而是为对象分配内存;也就是说,为保存字符串地址的str指针和len成员分配内存(程序并没有给num_string成员分配内存,这是因为num_string成员是静态成员,它独立于对象被保存)。创建对象将调用构造函数,后者分配用于保存字符串的内存,并将字符串的地址赋给str。然后,当程序不再需要该对象时,使用delete删除它。对象是单个的,因此,程序使用不带中括号的delete。与前面介绍的相同,这将只释放用于保存str指针和len成员的空间,并不释放str指向的内存,而该任务将由析构函数来完成。

在下述情况下析构函数将被调用(参见图12.4):

  1. 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。因此,在程序清单12.3中,执行完main()时,将调用headline[0]和headline[1]的析构函数;执行完callme1( )时,将调用grub的析构函数。
  2. 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。这就是程序清单12.3中sports对象所发生的情况。
  3. 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。

12.5.2 指针和对象小结

都在这两张图里了。

12.5.3 再谈定位new运算符

定位new运算符让您能够在分配内存时能够指定内存位置。
以下程序使用了定位new运算符和常规new运算符给对象分配内存。

// placenew1.cpp -- new, placement new, no delete
// 使用了定位new运算符和常规new运算符给对象分配内存.
// 该程序使用new运算符创建了一个512字节的内存缓冲区,
// 然后使用new运算符在堆中创建两个JustTesting对象,
// 并试图使用定位new运算符在内存缓冲区中创建两个JustTesting对象。
// 最后,它使用delete来释放使用new分配的内存。
#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]; // get a block of memory
    JustTesting *pc1, *pc2;
    pc1 = new (buffer) JustTesting; // place object in buffer
    pc2 = new JustTesting("Heap1", 20); // place object on heap
    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 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
    delete pc2; // free Heap1
    delete pc4; // free Heap2
    delete [] buffer; // free buffer
    cout << "Done\n";
    return 0;
}

out:

Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 0x1ff73f715b0 heap: 0x1ff73f71260
Memory contents:
0x1ff73f715b0: Just Testing, 0
0x1ff73f71260: Heap1, 20
Bad Idea constructed
Heap2 constructed
Memory contents:
0x1ff73f715b0: Bad Idea, 6
0x1ff73f717c0: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Done

在使用定位new运算符时存在两个问题:

  1. 首先,在创建第二个对象时,定位new运算符使用一个新对象来覆盖用于第一个对象的内存单元。显然,如果类动态地为其成员分配内存,这将引发问题。
  2. 其次,将delete用于pc2和pc4时,将自动调用为pc2和pc4指向的对象调用析构函数;然而,将delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数。

解决方法:
程序员必须负责管用定位new运算符用从中使用的缓冲区内存单元。
要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。

// 其中指针pc3相对于pc1的偏移量为JustTesting对象的大小。
pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof (JustTesting)) JustTesting("Better Idea", 6);

如果使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。即显式地为使用定位new运算符创建的对象调用析构函数。
指针pc1指向的地址与 buffer相同,但buffer是使用new []初始化的,因此必须使用delete [ ]而不 是delete来释放。即使buffer是使用new而不是new []初始化的,delete pc1 也将释放buffer,而不是pc1。这是因为new/delete系统知道已分配的512 字节块buffer,但对定位new运算符对该内存块做了何种处理一无所知。
显式地调用析构函数时,必须指定要销毁的对象。由于有指向对象的指针,因此可以使用这些指针:

pc3->~JustTesting(); // destroy object pointed to by pc3
pc1->~JustTesting(); // destroy object pointed to by pc1

以下为改进的程序:
对定位new运算符使用的内存单元进行管理,加入到合适的delete和显式析构函数调用。
对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

// placenew2.cpp -- new, placement new, no delete
// 该程序使用定位new运算符在相邻的内存单元中创建两个对象,并调用了合适的析构函数。
#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]; // get a block of memory
    JustTesting *pc1, *pc2;
    pc1 = new (buffer) JustTesting; // place object in buffer
    pc2 = new JustTesting("Heap1", 20); // place object on heap
    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;
// fix placement new location
    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; // free Heap1
    delete pc4; // free Heap2
// explicitly destroy placement new objects
    pc3->~JustTesting(); // destroy object pointed to by pc3
    pc1->~JustTesting(); // destroy object pointed to by pc1
    delete [] buffer; // free buffer
    cout << "Done\n";
    return 0;
}

out:

Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 0x231c1ea15b0 heap: 0x231c1ea1260
Memory contents:
0x231c1ea15b0: Just Testing, 0
0x231c1ea1260: Heap1, 20
Better Idea constructed
Heap2 constructed
Memory contents:
0x231c1ea15d8: Better Idea, 6
0x231c1ea17c0: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Better Idea destroyed
Just Testing destroyed
Done

12.6 复习各种技术

12.6.1 重载<<运算符

要重新定义 << 运算符,以便将它和cout一起用来显示对象的内容,请定义下面的友元运算符函数:

ostream & operator<<(ostream & os, const c_name & obj)
{
    os << ... ; // display object contents
    return os;
}

其中c_name是类名。如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,这样便不用将它们设置为友元函数了。

12.6.2 转换函数

  1. 要将单个值转换为类类型,需要创建原型如下所示的类构造函数: c_name(type_name value);
    其中c_name为类名,type_name是要转换的类型的名称。
  2. 要将类转换为其他类型,需要创建原型如下所示的类成员函数:operator type_name();
    虽然该函数没有声明返回类型,但应返回所需类型的值。
  3. 使用转换函数时要小心。可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。

12.6.3 其构造函数使用new的类

  1. 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,该运算符将释放分配的内存。
  2. 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
  3. 构造函数中要么使用new [],要么使用new,而不能混用。如果构造函数使用的是new[],则析构函数应使用delete [];如果构造函数使用的是new,则析构函数应使用delete。
  4. 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象。原型通常如下: className(const className &)
  5. 应定义一个重载赋值运算符的类成员函数,其函数定义如下(其中c_pointer是c_name的类成员,类型为指向type_name的指针)。下面的示例假设使用new []来初始化变量c_pointer:
c_name & c_name::operator=(const c_name & cn)
{
    if (this == & cn)
    	return *this; // done if self-assignment
    delete [] c_pointer;
    // set size number of type_name units to be copied
    c_pointer = new type_name[size];
    // then copy data pointed to by cn.c_pointer to
    // location pointed to by c_pointer
    ...
    return *this;
}

12.7 队列模拟

队列是一种抽象的数据类型(Abstract Data Type,ADT),可以存储有序的项目序列。
新项目被添加在队尾,并可以删除队首的项目。先进先出(FIFO)。

例子:Heather银行希望对顾客排队等待的时间进行估测。
通常,三分之一的顾客只需要一分钟便可获得服务,三分之一的顾客需要两分钟,另外三分之一的顾客需要三分钟。另外,顾客到达的时间是随机的,但每个小时使用自动柜员机的顾客数量相当稳定。工程的另外两项任务是:设计一个表示顾客的类;编写一个程序来模拟顾客和队列之间的交互。

12.7.1 队列类Queue类

定义一个Queue类的特征:

  1. 队列存储有序的项目序列;
  2. 队列所能容纳的项目数有一定的限制;
  3. 应当能够创建空队列;
  4. 应当能够检查队列是否为空;
  5. 应当能够检查队列是否是满的;
  6. 应当能够在队尾添加项目;
  7. 应当能够从队首删除项目;
  8. 应当能够确定队列中项目数。

1、设计Queue类的公有接口

class Queue
{
    enum {Q_SIZE = 10};
private:
// private representation to be developed later
public:
    Queue(int qs = Q_SIZE); 		// create queue with a qs limit
    ~Queue();
    bool isempty() const;
    bool isfull() const;
    int queuecount() const;			// 返回队列中节点的个数
    bool enqueue(const Item &item); // 入队
    bool dequeue(Item &item); 		// 出队
};

2、表示队列数据:链表

链表由节点序列构成。每一个节点中都包含要保存到链表中的信息以及一个指向下一个节点的指针。

struct Node
{
    Item item; 			// 当前节点的数据信息
    struct Node * next; // 指向下一个节点的位置
};

单向链表,每个节点都只包含一个指向其他节点的指针。
知道第一个节点的地址后,就可以沿指针找到后面的每一个节点。
通常,链表最后一个节点中的指针被设置为NULL(或0),以指出后面没有节点了。

3、设计Queue类的私有部分

让Queue类的一个数据成员指向链表的起始位置。
还可以使用数据成员来跟踪队列可存储的最大项目数以及当前的项目数。

typedef Customer Item;	// Customer 是一个类
class Queue
{
private:
// class scope definitions
// Node is a nested structure definition local to this class
    struct Node { Item item; struct Node * next;};
    enum {Q_SIZE = 10};
// private class members
    Node * front; 		// pointer to front of Queue
    Node * rear; 		// pointer to rear of Queue
    int items; 			// current number of items in Queue
    const int qsize; 	// maximum number of items in Queue
    ...
public:
//...
};

嵌套结构和类
在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。例如,如果Node是在Queue类的公有部分声明的,则可以在类的外面声明Queue::Node类型的变量。

4、类方法*

构造函数—成员初始化列表

  1. 对于const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。
  2. 只有构造函数可以使用这种初始化列表语法。
  3. 对于非静态const类数据成员,必须使用这种语法。(静态的不在类内存中)
  4. 对于被声明为引用的类成员,也必须使用这种语法。(因为在定义一个引用的时候就要给它赋值)
  5. 初值可以是常量或构造函数的参数列表中的参数。
  6. 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
  7. 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。
// 队列最初是空的,因此队首和队尾指针都设置为NULL(0或nullptr),
// 并将items设置为0。另外,还应将队列的最大长度qsize设置为构造函数参数qs的值。
Queue::Queue(int qs) : qsize(qs) // initialize qsize to qs
{
    front = rear = NULL;
    items = 0;
}
// 初值可以是常量或构造函数的参数列表中的参数。
Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0)
{}

为什么需要成员初始化列表?
一般,调用构造函数时,对象将在括号中的代码执行之前被创建。因此,调用Queue(int qs)构造函数将导致程序首先给成员变量分配内存(就相当于初始化)。然后,程序流程进入到括号中,使用常规的赋值方式将值存储到内存中。这时候就不能再对const常量赋值。

将项目添加到队尾(入队):

bool Queue::enqueue(const Item & item)
{
    if (isfull())
        return false;
    Node * add = new Node; // create node
// on failure, new throws std::bad_alloc exception
    add->item = item; // set node pointers
    add->next = NULL; // or nullptr;
    items++;
    if (front == NULL) // if queue is empty,
        front = add; // place item at front
    else
        rear->next = add; // else place at rear
    rear = add; // have rear point to new node
    return true;
}

删除队首项目(出队):

bool Queue::dequeue(Item & item)
{
    if (front == NULL)
        return false;
    item = front->item; // set item to first item in queue
    items--;
    Node * temp = front; // save location of first item
    front = front->next; // reset front to next item
    delete temp; // delete former first item
    if (items == 0)
        rear = NULL;
    return true;
}

显示析构函数:
向队列中添加对象将调用new来创建新的节点。通过删除节点的方式,dequeue( )方法确实可以清除节点,但这并不能保证队列在到期时为空(队列不为空的时候解散队列)。因此,类需要一个显式析构函数——该函数删除剩余的所有节点。

Queue::~Queue()
{
    Node * temp;
    while (front != NULL) // while queue is not yet empty
    {
        temp = front; // save address of front item
        front = front->next;// reset pointer to next item
        delete temp; // delete former front
    } 
}

最后,要克隆或复制队列,必须提供复制构造函数和执行深度复制的赋值构造函数。

12.7.2 Customer类

客户何时进入队列以及客户交易所需的时间。
当模拟生成新客户时,程序将创建一个新的客户对象,并在其中存储客户的到达时间以及一个随机生成的交易时间。当客户到达队首时,程序将记录此时的时间,并将其与进入队列的时间相减,得到客户的等候时间。

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; }
};
// set()成员函数将到达时间设置为参数,并将处理时间设置为1~3中的一个随机值。
void Customer::set(long when)
{
    processtime = std::rand() % 3 + 1;
    arrive = when;
}

12.7.3 ATM模拟

程序允许用户输入3个数:队列的最大长度、程序模拟的持续时间(单位为小时)以及平均每小时的客户数。程序将使用循环——每次循环代表一分钟。在每分钟的循环中,程序将完成下面的工作:

  1. 判断是否来了新的客户。如果来了,并且此时队列未满,则将它添加到队列中,否则拒绝客户入队。
  2. 如果没有客户在进行交易,则选取队列的第一个客户。确定该客户的已等候时间,并将wait_time计数器设置为新客户所需的处理时间。
  3. 如果客户正在处理中,则将wait_time计数器减1。
  4. 记录各种数据,如获得服务的客户数目、被拒绝的客户数目、排队等候的累积时间以及累积的队列长度等。

queue.h

#ifndef PRIMERPLUS_QUEUE_H
#define PRIMERPLUS_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:
// class scope definitions
// Node is a nested structure definition local to this class
// 结构体表示链表的每一个节点,存放节点信息和下一个指向的位置
// 当前节点保存的是一个类的对象,链表中依次存类的对象
    struct Node { Item item; struct Node * next;};
    enum {Q_SIZE = 10};
// private class members
    Node * front;   // pointer to front of Queue
    Node * rear;    // pointer to rear of Queue
    int items;      // current number 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);         // create queue with a qs limit
    ~Queue();
    bool isempty() const;
    bool isfull() const;
    int queuecount() const;         // 返回队列中节点的个数
    bool enqueue(const Item &item); // 入队
    bool dequeue(Item &item);       // 出队
};
#endif //PRIMERPLUS_QUEUE_H

queue.cpp

// queue.cpp -- Queue and Customer methods
#include "queue.h"
#include <cstdlib> // (or stdlib.h) for rand()
// Queue methods
Queue::Queue(int qs) : qsize(qs)
{
    front = rear = NULL;    // or nullptr
    items = 0;
}
// 析构函数:把原来开辟的内存空间都释放掉,队列不为空的时候解散队列
Queue::~Queue()
{
    Node * temp;
    while (front != NULL)   // while queue is not yet empty
    {
        temp = front;       // save address of front item
        front = front->next;// reset pointer to next item
        delete temp;        // delete former front
    }
}
bool Queue::isempty() const
{
    return items == 0;
}
bool Queue::isfull() const
{
    return items == qsize;
}
int Queue::queuecount() const
{
    return items;
}
// Add item to queue
bool Queue::enqueue(const Item & item)
{
    if (isfull())
        return false;
    Node * add = new Node;  // create node,存放新的队列节点
// on failure, new throws std::bad_alloc exception
    add->item = item;       // set node pointers
    add->next = NULL;       // or nullptr;队列的最后是空的
    items++;                // 节点个数++
    if (front == NULL)      // if queue is empty,
        front = add;        // place item at front
    else
        rear->next = add;   // else place at rear
    rear = add;             // have rear point to new node
    return true;
}
// Place front item into item variable and remove from queue
bool Queue::dequeue(Item & item)
{
    if (front == NULL)
        return false;
    item = front->item;     // set item to first item in queue
    items--;                // 节点个数--
    Node * temp = front;    // save location of first item
    front = front->next;    // reset front to next item
    delete temp;            // delete former first item
    if (items == 0)
        rear = NULL;
    return true;
}
// customer method
// when is the time at which the customer arrives
// the arrival time is set to when and the processing
// time set to a random value in the range 1 - 3
void Customer::set(long when)
{
    processtime = std::rand() % 3 + 1;  // 记录操作了多长时间
    arrive = when;  // 记录何时开始操作
}

usequeue.cpp

// 入队列,队列入满就出队列
#include "queue.h"
#include <iostream>
using namespace std;
int main(void)
{
    Item temp;
    int qs;
    int i = 0;
    int customers = 0;

    cout << "Enter max size of queue:";
    cin >> qs;

    Queue line(qs);

    while(!line.isfull())
    {
        temp.set(i++);      // 填当前进入队列的时间long类型的整数
        line.enqueue(temp);
        customers++;
    }
    cout << "enqueue, customers :" << customers << endl;

    while(!line.isempty())
    {
        line.dequeue(temp);
        customers--;
    }
    cout << "dequeue, now customers :" << customers << endl;
    return 0;
}

out:

Enter max size of queue:12
enqueue, customers :12
dequeue, now customers :0

bank.cpp

// 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\n";
    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\n";
    }
    else
        cout << "No customers!\n";
    cout << "Done!\n";
    return 0;
}
// x = average time, in minutes, between customers
// return value is true if customer shows up this minute
// 值RAND_MAX是在cstdlib文件(以前是 stdlib.h)中定义的,
// 值RAND_MAX是是rand( )函数可能返回的最大值(0是最小值)。
bool newcustomer(double x)
{
    return (std::rand() * x / RAND_MAX < 1);
}

out:

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: 113
customers served: 108
turnaways: 0
average queue size: 2.27
average wait time: 4.74 minutes
Done!

12.8 总结

  1. 在类构造函数中,可以使用new为数据分配内存,然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时提前固定数组的长度。在类构造函数中使用new,也可能在对象过期时引发问题。如果对象包含成员指针,同时它指向的内存是由new分配的,则释放用于保存对象的内存并不会自动释放对象成员指针指向的内存。因此在类构造函数中使用new类来分配内存时,应在类析构函数中使用delete来释放分配的内存。这样,当对象过期时,将自动释放其指针成员指向的内存。
  2. 如果对象包含指向new分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,也会出现问题。在默认情况下,C++逐个对成员进行初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次,这将出错。解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。在上述任何一种情况下,新的定义都将创建指向数据的副本,并使新对象指向这些副本。这样,旧对象和新对象都将引用独立的、相同的数据,而不会重叠。由于同样的原因,必须定义赋值运算符。对于每一种情况,最终目的都是执行深度复制,也就是说,复制实际的数据,而不仅仅是复制指向数据的指针。
  3. 对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数。如果使用new运算符为对象分配内存,并将其地址赋给一个指针,则当您将delete用于该指针时将自动为对象调用析构函数。然而,如果使用定位new运算符(而不是常规new运算符)为类对象分配内存,则必须负责显式地为该对象调用析构函数,方法是使用指向该对象的指针调用析构函数方法。C++允许在类中包含结构、类和枚举定义。这些嵌套类型的作用域为整个类,这意味着它们被局限于类中,不会与其他地方定义的同名结构、类和枚举发生冲突。
  4. C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和由逗号分隔的初始化列表,被放在构造函数参数的右括号后,函数体的左括号之前。每一个初始化器都由被初始化的成员的名称和包含初始值的括号组成。从概念上来说,这些初始化操作是在对象创建时进行的,此时函数体中的语句还没有执行。语法如下: queue(int qs) : qsize(qs), items(0), front(NULL), rear(NULL) { }如果数据成员是非静态const成员或引用,则必须采用这种格式,但可将C++11新增的类内初始化用于非静态const成员。
  5. C++11允许类内初始化,即在类定义中进行初始化:
    这与使用成员初始化列表等价。然而,使用成员初始化列表的构造函数将覆盖相应的类内初始化。
class Queue
{
private:
    ...
    Node * front = NULL;
    enum {Q_SIZE = 10};
    Node * rear = NULL;
    int items = 0;
    const int qsize = Q_SIZE;
    ...
};

12.9 复习题

1、

#include <cstring>
using namespace std;
class String
{
private:
    char * str;
    int len;
};
// 构造函数
// 需要给str指针指向一个内存空间,不能不管它
String::String(const char *s)
{
    len = strlen(s);
    str = new char[len+1];
    strcpy(str, s);
}

2、如果您定义了一个类,其指针成员是使用new初始化的,请指出可能出现的3个问题以及如何纠正这些问题。

  1. 首先,当这种类型的对象过期时,对象的成员指针指向的数据仍将保留在内存中,这将占用空间,同时不可访问,因为指针已经丢失。可以让类析构函数删除构造函数中new分配的内存,来解决这种问题。
  2. 其次,析构函数释放这种内存后,如果程序将这样的对象初始化为另一个对象,则析构函数将试图释放这些内存两次。这是因为将一个对象初始化为另一个对象的默认初始化,将复制指针值,但不复制指向的数据,这将使两个指针指向相同的数据。解决方法是,定义一个复制构造函数,使初始化复制指向的数据。
  3. 第三,将一个对象赋给另一个对象也将导致两个指针指向相同的数据。解决方法是重载赋值运算符,使之复制数据,而不是指针。

4、

#include <iostream>
#include <cstring>
using namespace std;
class Nifty
{
private:    // 可以省略
    char *personality;
    int talents;
public:     // 不可以省略
    Nifty();
    Nifty(const char * s);              // 加const不希望被修改
    ~Nifty(){delete [] personality;}    // 释放构造函数中开辟的内存空间
    friend ostream & operator<<(ostream & os, const Nifty & n);
};
Nifty::Nifty()
{
    personality = NULL;
    talents = 0;
}
Nifty::Nifty(const char * s)
{
    int len;
    len = strlen(s);
    personality = new char[len+1];
    strcpy(personality, s);
    talents = 0;
}
ostream & operator<<(ostream & os, const Nifty & n)
{
    os << "personality : " << n.personality << endl;
    os << "talents = " << n.talents << endl;
    return os;
}

5、下列各条语句将调用哪些类方法?

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();
};
// 下列各条语句将调用哪些类方法?
Golfer nancy; 					// #1
Golfer lulu(“Little Lulu”); 	// #2
Golfer roy(“Roy Hobbs”, 12); 	// #3
Golfer * par = new Golfer; 		// #4,new开辟类这么大的内存空间,调用默认构造函数。
Golfer next = lulu; 			// #5,一个类的对象初始化另一个类对象,复制构造函数。
Golfer hazzard = “Weed Thwacker”; // #6,用字符串初始化类的对象,将某一种类型转化为类的类型时将使用带字符串参数的构造函数。
*par = nancy; 					// #7,两边对象都存在不会调用构造函数,调用默认赋值运算符。
nancy = “Nancy Putter”; 		// #8,右边调用带字符串的构造函数,然后调用默认的赋值运算符。

12.10 编程练习

第一题

12p1.h


#ifndef PRIMERPLUS_12P1_H
#define PRIMERPLUS_12P1_H
class Cow
{
    char name[20];
    char * hobby;
    double weight;
public:
    Cow();
    Cow(const char * nm, const char * ho, double we);
    Cow(const Cow & c);
    ~Cow();
    Cow & operator=(const Cow & c);
    void ShowCow() const;
};
#endif //PRIMERPLUS_P1_H

12p1.cpp

#include "12p1.h"
#include <cstring>
#include <iostream>
using namespace std;
Cow::Cow()
{
    name[0] = '\0';
    hobby = nullptr;    // NULL
    weight = 0.0;
}
Cow::Cow(const char * nm, const char * ho, double wt)
{
    strncpy(name, nm, 20);  // strncpy()和strnpy()略有不同
    if (strlen(nm) >= 20)
        name[19] = '\0';
    hobby = new char[strlen(ho) + 1];   // 深度拷贝,指针指向开辟的空间
    strcpy(hobby, ho);
    weight = wt;
}
Cow::Cow(const Cow & c)
{
    strcpy(name, c.name);
    hobby = new char[strlen(c.hobby) + 1];   // 深度拷贝,指针指向开辟的空间
    strcpy(hobby, c.hobby);
    weight = c.weight;
}
Cow::~Cow()
{
    delete [] hobby;
}
Cow & Cow::operator=(const Cow & c)
{
    if (this == &c)
        return *this;
    delete [] hobby;    // 释放成员指针以前指向的内存
    strcpy(name, c.name);
    hobby = new char[strlen(c.hobby) + 1];   // 深度拷贝,指针指向开辟的空间
    strcpy(hobby, c.hobby);
    weight = c.weight;
    return *this;
}
void Cow::ShowCow() const
{
    cout << "Name  : " << name << endl;
    cout << "Hobby : " << hobby << endl;
    cout << "Weight: " << weight << endl;
}

use12p1.cpp

#include "12p1.h"
int main(void)
{
    Cow cow1;
    Cow cow2("cow2", "cccc", 123.4);
    Cow cow3(cow2);
    cow1 = cow2;
    cow1.ShowCow();
    cow2.ShowCow();
    cow3.ShowCow();
    return 0;
}

第二题

string2.h

#ifndef PRIMERPLUS_STRING2_H
#define PRIMERPLUS_STRING2_H
#include <iostream>
using std::ostream;
using std::istream;
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:
// constructors and other methods
    String(const char * s);     // constructor
    String();                   // default constructor
    String(const String &);     // copy constructor
    ~String();                  // destructor
    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 &st, const String &st2);
    friend bool operator>(const String &st1, const String &st2);
    friend bool operator==(const String &st, const String &st2);
    friend ostream & operator<<(ostream & os, const String & st);
    friend istream & operator>>(istream & is, String & st);
// static function
    static int HowMany();

    friend String operator+(const char * s, const String & st);
    String operator+(const String & st);
    void stringlow();
    void stringup();
    int has(char ch) const;
};
#endif //PRIMERPLUS_STRING1_H

string2.cpp

// string1.cpp -- String class methods
#include <cstring>      // string.h for some
#include "string2.h"    // includes <iostream>
#include <cctype>
using namespace std;
// initializing static class member
int String::num_strings = 0;
// static method
int String::HowMany()
{
    return num_strings;
}
// class methods
String::String(const char * s) // construct String from C string
{
    len = std::strlen(s); // set size
    str = new char[len + 1]; // allot storage
    std::strcpy(str, s); // initialize pointer
    num_strings++; // set object count
}
String::String() // default constructor
{
    len = 4;
    str = new char[1];
    str[0] = '\0'; // default string
    num_strings++;
}
String::String(const String & st)
{
    num_strings++; // handle static member update
    len = st.len; // same length
    str = new char [len + 1]; // allot space
    std::strcpy(str, st.str); // copy string to new location
}
String::~String() // necessary destructor
{
    --num_strings; // required
    delete [] str; // required
}
// overloaded operator methods
// assign a String to a String
String & String::operator=(const String & st)
{
    if (this == &st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char[len + 1];
    std::strcpy(str, st.str);
    return *this;
}
// assign a C string to a String
String & String::operator=(const char * s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}
// read-write char access for non-const String
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
ostream & operator<<(ostream & os, const String & st)
{
    os << st.str;
    return os;
}
// quick and dirty String input
// 重载>>运算符提供了一种将键盘输入行读入到String对象中的简单方法。
// 它假定输入的字符数不多于String::CINLIM的字符数,并丢弃多余的字符。
// 在if条件下,如果由于某种原因
// (如到达文件尾或get(char *, int)读取的是一个空行)导致输入失败,istream对象的值将置为 false。
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;
}

String operator+(const char * s, const String & st)
{
    String temp;
    temp.len = strlen(s) + st.len;
    temp.str = new char[temp.len+1];
    strcpy(temp.str, s);
    strcat(temp.str, st.str);
    return temp;
}
String String::operator+(const String & st)
{
    String temp;
    temp.len = len + st.len;
    temp.str = new char[temp.len+1];
    strcpy(temp.str, str);
    strcat(temp.str, st.str);
    return temp;
}
void String::stringlow()
{
    for (int i=0; i < len; i++)
        str[i] = tolower(str[i]);
}
void String::stringup()
{
    for (int i=0; i < len; i++)
        str[i] = toupper(str[i]);
}
int String::has(char ch) const
{
    int count = 0;
    for (int i=0; i < len; i++)
    {
        if (str[i] == ch)
            count++;
    }
    return count;
}

usestring2.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 << ".\n";
    s2 = s2 + s1;
    s2.stringup(); // converts string to uppercase
    cout << "The string\n" << s2 << "\ncontains " << s2.has('A')
         << " 'A' characters in it.\n";
    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!\n";
                success = true;
                break;
            } }
        if (success)
            break;
        else
            cout << "Try again!\n";
    }
    cout << "Bye\n";
    return 0;
}

out:

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

第四题

12p4.h


#ifndef PRIMERPLUS_12P4_H
#define PRIMERPLUS_12P4_H
typedef unsigned long Item;         // 起别名,为存放不同的数据类型
class Stack
{
private:                // 私有部分放成员变量
    enum {MAX = 10};    // 枚举类型的符号常量
    Item * pitems;
    int size;
    int top;            // 顶部堆栈项的索引,栈顶指针
public:
    Stack(int n = MAX);                // 默认构造函数
    Stack(const Stack & st);
    ~Stack();
    Stack & operator=(const Stack & st);
    bool isempty() const;   // 判断是否为空
    bool isfull() const;    // 判断是否满了
    // push() returns false if stack already is full, true otherwise
    bool push(const Item & item);   // 入栈
    // pop() returns false if stack already is empty, true otherwise
    bool pop(Item & item);          // 出栈
};
#endif //PRIMERPLUS_STACK_H

12p4.cpp

// stack.cpp -- Stack member functions
#include "12p4.h"
Stack::Stack(int n) // create an empty stack
{
    pitems = new Item[n];
    size = n;
    top = 0;            // 初始化栈顶指针
}
Stack::Stack(const Stack & st)
{
    pitems = new Item[st.size];
    for (int i=0; i<st.size; i++)
        pitems[i] = st.pitems[i];
    size = st.size;
    top = st.top;
}
Stack::~Stack()
{
    delete [] pitems;
}
bool Stack::isempty() const
{
    return top == 0;    // 是否等于最底层
}
bool Stack::isfull() const
{
    return top == MAX;  // 是否等于最高层
}
bool Stack::push(const Item & item)
{
    if (top < MAX)      // 入栈条件
    {
        pitems[top++] = item;
        return true;
    }
    else
        return false;
}
bool Stack::pop(Item & item)
{
    if (top > 0)
    {
        item = pitems[--top];
        return true;
    }
    else
        return false;
}
Stack & Stack::operator=(const Stack & st)
{
    if (this == &st)
        return *this;
    delete [] pitems;
    pitems = new Item[st.size];
    for (int i=0; i<st.size; i++)
        pitems[i] = st.pitems[i];
    size = st.size;
    top = st.top;
    return * this;
}

use12p4.cpp

// stacker.cpp -- testing the Stack class
#include <iostream>
#include <cctype>   // or ctype.h
#include "12p4.h"
const int MAX = 5;
int main()
{
    using namespace std;
    Stack st(MAX);       // create an empty stack
    Item item;
    for (int i=0; i<MAX; i++)
    {
        cout << "Enter a number you want to push to stack:";
        cin >> item;
        while(cin.get() != '\n');
        st.push(item);
    }

    Stack st_new(st);   // 复制构造函数
    for (int i=0; i < MAX; i++)
    {
        st_new.pop(item);
        cout << item << " is poped." << endl;
    }

    return 0;
}

out:

Enter a number you want to push to stack:1
Enter a number you want to push to stack:2
Enter a number you want to push to stack:3
Enter a number you want to push to stack:4
Enter a number you want to push to stack:5
5 is poped.
4 is poped.
3 is poped.
2 is poped.
1 is poped.
;