本章内容包括:
- 对类成员使用动态内存分配
- 隐式和显式复制构造函数
- 隐式和显式重载赋值运算符
- 在构造函数中使用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
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对象的引用
- 如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率.
- 首先,返回对象将调用复制构造函数,而返回引用不会.
- 其次,引用指向的对象应该在调用函数执行时存在.
- 第三,如果形参都为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 编程练习