第12章 类和动态内存分配

本章内容包括:

  • 对类成员使用动态内存分配
  • 隐式和显式复制构造函数
  • 隐式和显式重载赋值运算符
  • 在构造函数中使用new所必须完成的工作
  • 使用静态类成员
  • 将定位new运算符用于对象
  • 使用指向对象的指针
  • 实现队列抽象数据类型(ADT)

12.1 动态内存和类

  • C++使用new和delete运算符来动态控制内存.

12.1.1 复习示例和静态类成员

  • 程序清单12.1 strngbad.h
  • 静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本.也就是回溯哦,类的所有对象共享一个静态成员.这对于所有类对象都具有相同值的类私有数据是非常方便的.
  • 程序清单12.2 strngbad.cpp
  • 注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存.对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分.请注意,初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static.
  • 对于不能在类声明中初始化静态数据成员的一种例外情况是,静态数据成员为整型或枚举性const.
  • 注意:静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域运算符来指出静态成员所属的类.但如果静态成员是整型或枚举型const,则可以在类声明中初始化.
  • 删除对象可以释放对象本身占用的内存,但并不能自动释放术语对象成员的指针指向的内存.因此,必须使用析构函数.
  • 警告:在构造函数中使用new来分配内存时,必须在相应的析构函数汇总使用delete来释放内存.如果使用new[](包括中括号)来分配内存,则应使用delete[](包括中括号)来释放内存.
  • 程序清单12.3 vegnews.cpp 
    • 该程序通常会在显示有关还是-1个对象的信息之前中断,而有些这样的机器将报告通用保护错误(GPF).GPF表明程序试图访问禁止它访问的内存单元,这是另一种糟糕的信号.

12.1.2 特殊成员函数

  • C++自动提供了下面这些成员函数: 
    • 默认构造函数,如果没有定义构造函数
    • 默认析构函数,如果没有定义
    • 复制构造函数,如果没有定义
    • 赋值运算符,如果没有定义
    • 地址运算符,如果没有定义
  • C++11提供了另外两个特殊成员函数:移动构造函数和移动赋值运算符.
  • 1.默认构造函数
  • 2.复制构造函数 
    • 复制构造函数用于将一个对象复制到新创建的对象中.也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中.类的赋值构造函数原则通常如下:Class_name(const Class_name &);
    • 对于复制构造函数,需要指导两点:何时调用和有何功能.
  • 3.何时调用复制构造函数 
    • 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用.这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有的对象.
    • 记住,按值传递意味着创建原始变量的一个副本.
  • 4.默认的复制构造函数的功能 
    • 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值.

12.1.3 回到Stringbad:复制构造函数的哪里出了问题

  • 提示:如果类中包含这样的静态数据成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理计数问题.
  • 隐式复制构造函数是按值进行复制的.
  • 1.定义一个显式复制构造函数以解决问题(深度复制deep copy) 
    • 必须定义复制构造函数的原因在于,一些类成员是使用new初始化的,指向数据的指针,而不是数据本身.
  • 警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制.复制的另一种形式(成员复制或浅复制)只是复制指针值.浅复制仅浅浅地复制指针信息,而不会深入”挖掘”以复制指针应用的结构.

12.1.4 Stringbad的其他问题:赋值运算符

  • ANSI C允许结构赋值,而C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的.这种运算符的原型如下:Class_name & Class_name::operator=(const Class_name &);它接受并返回一个指向类对象的引用.
  • 1.赋值运算符的功能以及何时使用它 
    • 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制.如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响.
  • 2.赋值的问题出在哪里 
    • 要指出的是,如果操作结果是不确定的,则执行的操作将随编译器而异,包括显示独立声明(Declaration of Independence)或释放隐藏文件占用的硬盘空间.当然,编译器开发人员通常不会花时间添加这样的行为.
  • 3.解决赋值的问题 
    • 对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义.
    • 其实现与复制构造函数相似,但也有一些差别. 
      • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据
      • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容.
      • 函数返回一个指向调用对象的引用.

12.2 改进后的新String类 
12.2.1 修订后的默认构造函数

  • C++11空指针:在C++98中,字面值0有两个含义:可以表示数字值零,也可以表示空指针,这使得阅读程序的人和编译器难以区分.有写程序员使用(void *) 0 来标识空指针(空指针本身的内部表示可能不是零),还有些程序员使用NULL,这是一个表示空指针的C语言宏.C++11提供了更好的解决方案:引入新关键字nullptr,用于表示空指针.您仍可像以前一样使用0—否则大量现有的代码将非法,但建议您使用nullptr:
str = nullptr; //C++11 null pointer notation
  • 1

12.2.2 比较成员函数 
12.2.3 使用中括号表示法访问字符

  • 在C++中,两个中括号组成一个运算符—中括号运算符,可以使用方法operator来重载该运算符.

12.2.4 静态类成员函数

  • 可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static),这样做有两个重要的后果. 
    • 首先,不能通过对象调用静态成员函数;
    • 其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员.

12.2.5 进一步重载赋值运算符

  • 程序清单12.4 string1.h
  • 程序清单12.5 string1.cpp
  • 程序清单12.6 sayings1.cpp
  • 注意:较早的get(char *,int)版本在读取空行后,返回的值不为false.然而,对于这些版本来说,如果读取了一个空行,则字符串中第一个字符将是一个空字符.如果实现遵循了最新的C++标准,则if语句中的第一个条件将检测到空行,第二个条件用于旧版本实现中检测空行.

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

  • 使用new初始化对象的指针成员时必须特别小心.具体: 
    • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete.
    • new和delete必须相互兼容.new对应于delete,new[]对应于delete[].
    • 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带.因为只有一个析构函数,所有的构造函数都必须与它兼容.然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中nullptr),这是因为delete(无论是带中括号还是 不带中括号)可以用于空指针.
    • NULL,0还是nullptr:以前,空指针可以用0或NULL(在很多头文件中,NULL是一个被定义为0的符号常量)来表示.C程序员通常使用NULL而不是0,以指出这是一个指针,就像使用’\0’而不是0来表示空字符,以指出这是一个字符一样.然而,C++传统上更喜欢用简单的0,而不是等价的NULL.但正如前面指出的,C++11提供了关键字nullptr,这是一种更好的选择.
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象.
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象.

12.3.1 应该和不应该 
12.3.2 包含类成员的类的逐成员复制 
12.4 有关返回对象的说明

  • 当成员函数或独立的函数返回对象时,有几种返回方式可供选择.可以返回指向对象的引用,指向对象的const引用或const对象.

12.4.1 返回指向const对象的引用

  • 如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率. 
    1. 首先,返回对象将调用复制构造函数,而返回引用不会.
    2. 其次,引用指向的对象应该在调用函数执行时存在.
    3. 第三,如果形参都为const,返回类型必须为const,这样才匹配.

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

  • 两种常见的返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符.

12.4.3 返回对象

  • 如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数.因此,当控制权回到调用函数时,引用指向的对象将不再存在.在这种情况下,应返回对象而不会引用.

12.4.4 返回const对象

  • force1 + force2 = net;首先,没有要编写这种语句的合理理由,但并非所有代码都是合理的.即使是程序员也会犯错.其次,这种代码之所以可行,是因为复制构造函数将创建一个临时对象来表示返回值.第三,使用完临时对象候,将把它丢弃.
  • 如果您担心这种行为可能引发的误用或滥用,有一种简单的解决方案:将返回类型声明为const Vector.
  • 总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用.在这种情况下,将使用复制构造函数来生成返回的对象.如果方法或函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个指向这种对象的引用.最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高.

12.5 使用指向对象的指针

  • 程序清单12.7 sayings2.cpp
  • 使用new初始化对象:通常,如果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;

12.5.1 再谈new和delete

  • 对象是单个的,因此,程序使用不带中括号的delete.这将只释放用于保存str指针和len成员的空间,并不释放str指向的内存,而该任务将由析构函数来完成.
  • 在下述情况下析构函数将被调用 
    • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数.
    • 如果对象是静态变量(外部,静态,静态外部或来自名称空间),则在程序结束时将调用对象的析构函数.
    • 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用.

12.5.2 指针和对象小结

  • 使用对象指针时,需要注意几点: 
    • 使用常规表示法来声明指向对象的指针:String * glamour;
    • 可以将指针初始化为指向已有的对象:String * first = &sayings[0];
    • 可以使用new来初始化指针,这将创建一个新的对象:String * favorite = new String(sayings[choice]);
    • 对类使用new将调用相应的类构造函数来初始化新创建的对象;
    • 可以使用->运算符通过指针访问类方法
    • 可以对对象指针应用解除应用运算符(*)来获得对象.

12.5.3 再谈定位new运算符

  • 定位new运算符让您能够在分配内存时能够制定内存位置.
  • 经验教训:程序员必须负责管用定位new运算符用从中使用的缓冲区内存单元.要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠.第二,如果使用定位new运算符来为对象分配内存,必须确保其西瓜头函数被调用.原因在于delete可与常规new运算符配合使用,但不能与定位new运算符配合使用.
  • 程序清单12.9 placenew2.cpp

12.6 复习各种技术 
12.6.1 重载<<运算符 
12.6.2 转换函数

  • 使用专函函数时要小心.可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换.

12.6.3 其构造函数使用new的类 
12.7 队列模拟

  • 栈是一种后进先出LIFO的结构,而队列是先进先出FIFO的.

12.7.1 队列类

  • 队列的特征 
    • 队列存储有序的项目序列
    • 队列所能容纳的数有一定的限制
    • 应当能够创建空队列
    • 应当能够检查队列是否为空
    • 应当能够检查队列是否是满的
    • 应当能够在队尾添加项目
    • 应当能够从队首删除项目
    • 应当能够确定队列中项目数.
  • 1.Queue类的接口
  • 2.Queue类的实现 
    • 嵌套结构和类:在类声明中声明的结构,类或枚举被称为是被嵌套在类中,其作用域为整个类.这种声明不会创建数据对象,而只是制定了可以在类中使用的类型.如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型.
  • 3.类方法 
    • 成员初始化列表的语法:如果Classy是一个类,而mem1,mem2,和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy(int n, int m):mem1(n),mem2(0),mem3(n*m+2)
{
//...
}
  • 上述代码mem1初始化为n,将mem2初始化为0,将mem3初始化为n*m+2.从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码.请注意以下几点: 
    • 这种格式只能用于构造函数
    • 必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的)
    • 必须用这种格式类初始化引用数据成员.
  • 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关.
  • 警告:不能将成员初始化列表语法用于构造函数之外的其他类方法.
  • 成员初始化列表使用的括号方式也可用于常规初始化.这使得初始化内置类型就像初始化类对象一样.
  • C++11的类内初始化 
    • C++11允许您以更直观的方式进行初始化:
class Classy
{
    int mem1 = 10; //in-class initialization
    const int mem2 = 20; //in-class initialization
//...
};
  • 这与在构造函数中使用成员初始化列表等价:Classy::Classy() : mem1(10), mem2(20) {…}成员mem1和mem2将分别被初始化为10和20,除非调用了使用成员初始化列表的构造函数,在这种情况下,实际列表将覆盖这些默认初始值:Classy::Classy(int n) :mem1(n) {…}在这里,构造函数将使用n来初始化mem1,但mem2仍被设置为20.
  • C++11提供了另一种禁用方法的方式—使用关键字delete

12.7.2 Customer类

  • 程序清单12.10 queue.h
// 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(0), 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); // add item to end
    bool dequeue(Item &item);       // remove item from front
};
#endif
  • 程序清单12.11 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; 
}

12.7.3 ATM模拟

  • 程序清单12.12 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";
    // cin.get();
    // cin.get();
    return 0;
}
//  x = average time, in minutes, between customers
//  return value is true if customer shows up this minute
bool newcustomer(double x)
{
    return (std::rand() * x / RAND_MAX < 1); 
}

注意:编译器如果没有bool,可以用int代替bool,用0代替false,用1代替true;还可能 必须使用stdlib.h和time.h代替较新的cstdlib和ctime;另外可能必须自己来定义RAND_MAX;

12.8 总结

  • 通常,对于诸如复制构造函数等概念,都是在由于忽略他们而遇到了麻烦后逐步理解的.
  • 对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数.如果使用new运算符为对象分配内存,并将其地址赋给一个指针,则当您将delete用于该指针时将自动为对象调用析构函数.然而,如果使用定位new运算符(而不是常规new运算符)为类对象分配内存,则必须负责显式地为该对象调用析构函数,方法是使用指向该对象的指针调用析构函数方法.C++允许在类中包含结构,类和枚举定义.这些嵌套类型的作用域为整个类,这意味着它们被局限于类中,不会与其他地方定义的同名结构,类和枚举发生冲突.

12.9 复习题 
12.10 编程练习

附件:本章源代码下载地址

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/82110438