Bootstrap

类和动态内存分配——C++ Prime Plus CH12

①动态内存和类

1.复习示例和静态类成员

使用程序复习new和delete用法。

// badstring.h文件
#include<iostream>
#ifndef STRING_BAD_
#define STRING_BAD_

class Stringbad
{
private:
	char* str;
	int len;
	static int num_strings;          //静态类成员
public:
	Stringbad();
	Stringbad(const char * s);
	~Stringbad();
	friend std::ostream& operator<<(std::ostream & os,const Stringbad& St);
};
#endif

程序说明:

1.静态类成员:特点,无论创建了多少个对象,程序都只将创建一个静态类变量副本。假设创建10个Stringbad 对象,将有10个str成员和10个len成员,但只有一个共享的num_string成员。通常字符串类不需要这样的成员吗,这里只是为了引出问题。

2.变量是 char指针,而不是数组,这意味着并没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。 

//badtring.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstring>
#include"stringbad.h"

using std::cout;
int Stringbad::num_strings = 0;

//char* str;
//int len;
//static int num_strings;          //静态类成员
//public:
Stringbad::Stringbad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ": \"" << str<< "\" object created\n";       //提醒
}

Stringbad::Stringbad(const char* s)
{
	len = std::strlen(s);
	str = new char[int(len + 1)];
	std::strcpy(str, s);
	num_strings++;
	cout << num_strings << ": \"" << str << "\" object created\n";       //提醒
}
Stringbad::~Stringbad()
{
	cout << ": \"" << str << "\" object deleted,\n";          //提醒
	num_strings--;
	cout << num_strings << "lefted.\n";               //提醒
	delete [] str;

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

程序说明:

1.静态类成员不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分,并且初始化中使用了作用域运算符,但没有使用关键字static。

2.初始化是在方法文件中,而不是在头文件中,这是因为类声明位于头文件中,程序很可能将头文件包括在其他几个文件中,这将出现多个初始化语句,引发错误。

3.构造函数中并不是简单的将字符串指针进行赋值:

str = s;

 上述语句只保存了地址,没有创建字符串的副本。所以要采用复制字符串的方式来及进行构造。

3.析构函数中将str分配的内存释放掉。

//use_badstring.cpp
#include<iostream>
#include"stringbad.h"
using std::cout;
using std::endl;
void callme1(Stringbad& rsb);
void callme2(Stringbad rsb);

int main()
{
	
	{
		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);         //值传递
		cout << "headline2: " << headline2 << endl;

		//创建新变量并初始化
		Stringbad sailor = sports;
		cout << "sailor: " << sailor << endl;

		//赋值未初始化变量
		Stringbad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block.\n";
	}
	cout << "End of the main()\n";
	return 0;
}

void callme1(Stringbad& rsb)
{
	cout << "String passed by reference:\n";
	cout << "\"" << rsb << "\"\n";
}

void callme2(Stringbad rsb)
{
	cout << "String passed by value:\n";
	cout << "\"" << rsb << "\"\n";
}

代码结果:

 结果出现了异常!!!我们一步步分析:

1.我们可以看到,下边的语句运行时正常的。

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;

2.callme1()函数运行正常:

		callme1(headline1);     //引用传递
		cout << "headline1: " << headline1 << endl;

3.严重的问题:

callme2(headline2);         //值传递
		cout << "headline2: " << headline2 << endl;

现象:

首先,按值传递防止原始参数被修改,实际上函数已使得原始字符串无法识别。程序出错,导致最后余下-2个对象,并且在删除sports时,最后删除的两个对象(headline1  headline2)已无法识别。

原因:

1.析构函数次数比num_strings递增次数多两次,但结果表明程序使用了第三个构造函数:

Stringbad sailor = sports;

这使用的是哪个构造函数呢?这种初始化函数调用了一个复制构造函数(编译器自动生成的),

上述代码相当于

Stringbad sailor = Stringbad(sports);

这个例子说明的所有问题都是由编译器自动生成的成员函数引起的。 

 2.特殊成员函数:

Stringbad类的问题是由特殊成员函数引起的。这些成员函数是自动定义的,具体的说,C++自动提供了下面这些成员函数:

默认构造函数,默认析构函数,复制构造函数,赋值运算符,地址运算符。

更准确的说,编译器将生成上述最后三个函数的定义。C++11提供了移动构造函数和移动赋值运算符,这将在18章讨论。

隐式地址运算符返回调用对象的地址(this指针的值),在此不详细讨论该成员函数。

1.默认构造函数:

如果没有提供任何构造函数,C++将创建默认构造函数,编译器将提供一个不含任何参数,也不执行任何操作的构造函数,也就是说,它的值在初始化时是未知的。

如果定义了构造函数,C++将不会定义默认构造函数,如果希望在创建对象时可以不显式地对它进行初始化,则必须显式地定义默认构造函数。

带参数地构造函数只要所有参数都有默认值,则这个函数也是默认构造函数,但只能有一个默认构造函数。

2.复制构造函数:

复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:

Class_name(const Class_name &);

何时调用复制构造函数:

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

	Stringbad motto;
		Stringbad ditto(motto);
		Stringbad metoo = motto;
		Stringbad also = Stringbad(motto);
		Stringbad* pStringbad = new Stringbad(motto);

后边四个都会调用复制构造函数,中间的两个声明可能使用复制构造函数直接创建metto 和 also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo also,这取决于具体的实现,最后一个声明使用motto初始化一个匿名对象,毕竟新对象的地址赋给pstring指针。

每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。按值传递意味创建原始变量的一个副本,编译器生成临时对象时,也将使用复制构造函数。合适生成临时对象随编译器而异,但无论哪种编译器,当按值传递和返回对象时,都将调用复制构造函数。因此我们应该按引用来传递对象。

默认的复制构造函数功能:

默认的复制构造函数逐个复制非静态成员(浅复制),复制的是成员的值。静态成员不受影响,一因为它们属于整个类,而不是各个对象。

3.赋值运算符:

 C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:

Class_name & Class_name::operator=(const Class_name &);

它接受并返回一个指向类对象的引用。

赋值运算符的功能以及何时使用它:

将已有的对象赋给另一个对象时,将使用重载的赋值运算符:

knot = headline1;

初始化对象时,并不一定会使用赋值运算符:

Stringbad metto = knot;   //用默认复制函数  或默认复制函数和赋值运算符;

与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。

3.Stringbad的问题在哪:

1.首先,程序的输出表明,析构函数的调用次数比构造函数的调用次数多2,原因可能是程序使用默认复制构造函数创建了另外两个对象。callme2()调用和对sailor的初始化。这意味着程序将无法正确记录对象的数量。解决办法时提供一个对计数进行更新的显示复制构造函数:

Stringbad::Stringbad(const Stringbad& st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	strcpy(str, st.str);
}

注意:如果类中包含这样的静态数据成员,即其值在新对象被创建时发生变化,则应该提供一个显示复制构造函数来处理计数问题。

2.第二个异常之处更可怕,字符串出现了乱码。原因在于隐式复制构造函数是按值进行复制的。

sailor.str=soprt.str;

这里复制的并不是字符串,而是一个指向字符串的指针,最后我们将得到两个指向同一个字符串的指针,当析构函数被调用时,这将发生问题。析构函数释放sailor.str指针指向的内存,sports.str指向的内存已经被sailor的析构函数析构掉,这将导致不确定、有害的结果。另一个症状是,试图释放内存两次可能导致程序异常终止。解决问题是,定义一个显式复制构造函数来解决问题。

Stringbad::Stringbad(const Stringbad& st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	strcpy(str, st.str);
}

 必须定义复制构造函数的原因在于,一些类成员函数是使用new初始化、指向数据的指针,而不是数据本身。

注意:如果不给指针分配指向的内存,strcpy语句无法编译通过。

3.Stringbad的问题除了可以归咎到默认复制构造函数外,还需要看一眼默认赋值运算符的问题。

knot = headline1;

为knot调用析构函数时,正常释放内存,为headline1调用析构函数时,出现错误。出现问题的原因和隐式复制构造函数相同:数据受损。这也是成员复制的问题,这导致了headline.str和knot.str指向相同的地址。为解决这样的问题,解决办法是提供赋值运算符(进行深度复制)定义。其与实现复制构造函数相似,但也有一些差别。

1.由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据;

2.函数应该避免将对象付给本身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容;

3.函数返回一个指向调用对象的引用,这样函数可以像常规赋值操作那样,连续进行赋值。

Stringbad& Stringbad::operator=(const Stringbad& st)
{
	if (this == &st)
		return *this;         //防止把数据意外释放掉
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

②改进后的新String类

我们对Stringbad类进行修订,添加其他关于字符串的功能。

// stringbad.h文件
#include<iostream>
#ifndef STRING_BAD_
#define STRING_BAD_

class Stringbad
{
private:
	char* str;
	int len;
	static const int CINLIM = 80; 
	static int num_strings;//静态类成员
public:
	Stringbad();
	Stringbad(const char * s);
	~Stringbad();
	Stringbad(const Stringbad& st);
	Stringbad& operator=(const Stringbad& st);
	bool operator>(const Stringbad& st);
	bool operator<(const Stringbad& st);
	bool operator==(const Stringbad& st);
	char& operator[](int i);
	const char& operator[](int i)const;
	int length()const { return len; };
	friend std::ostream & operator<<(std::ostream & os,const Stringbad& St);
	friend std::istream & operator>>(std::istream& is,  Stringbad& St);
	static int HowMany();
};
#endif

除了第一部分的代码,还增加了比较代码、输入符重载,以及提供数组表示法访问字符的功能。此外,还增加了静态类方法补充静态数据成员num_strings。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstring>
#include"stringbad.h"

using std::cout;
int Stringbad::num_strings = 0;


Stringbad::Stringbad()
{
	len = 0;
	str = new char[1];
	str = nullptr;
	num_strings++;
}

Stringbad::Stringbad(const char* s)
{
	len = std::strlen(s);
	str = new char[int(len + 1)];
	std::strcpy(str, s);
	num_strings++;
}
Stringbad::~Stringbad()
{
	num_strings--;
	delete [] str;

}

Stringbad::Stringbad(const Stringbad& st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	strcpy(str, st.str);
}
Stringbad& Stringbad::operator=(const Stringbad& st)
{
	if (this == &st)
		return *this;         //防止把数据意外释放掉
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}
bool Stringbad::operator>(const Stringbad& st)
{
	return (std::strcmp(str, st.str));
}
bool Stringbad::operator<(const Stringbad& st)
{
	return(!(str > st.str));
}
bool Stringbad::operator==(const Stringbad& st)
{
	return (std::strcmp(str, st.str) == 0); 
}
char& Stringbad::operator[](int i)
{
	return str[i];
}
const char& Stringbad::operator[](int i)const
{
	return str[i];
}
std::istream & operator>>(std::istream& is,  Stringbad& St)
{
	char temp[Stringbad::CINLIM];
	is.get(temp, Stringbad::CINLIM);
	if (is)
		St = temp;     //赋值符号已经被重载
	while (is && is.get() != '\n')
		continue;
	return is;
}

std::ostream& operator<<(std::ostream& os, const Stringbad& St)
{
	os << St.str;
	return os;
}
int Stringbad::HowMany()
{
	return num_strings;
}

 程序说明:

1.修订后的默认构造函数:

Stringbad::Stringbad()
{
	len = 0;
	str = new char[1];
	str = nullptr;
	num_strings++;
}

为什么str空间为char[1],他是为了和析构函数 delete [] str 相兼容。如果没有[1],那调用析构函数便会出问题,空指针可以用0表示,也可以用nullptr,建议用后者。

2.比较成员函数:

用strcmp()函数是最简单的方法,将比较函数作为友元,有助于将Stringbad对象与常规的C字符串比较。假设answer是Stringbad对象,则下面的代码:

if ( "love" == answer )

将被转换为:
if(operator ==("love),answer)

然后,编译器将使用某个构造函数将代码转换为:

if(operator == (Stringbad("love"),answer))

这与原型是相匹配的。

 3.使用中括号表示法访问字符:

通常二元C++运算符,一个操作数位于第一个中括号前边,另一个操作数位于两个操作数之间,但是对于中括号,第一个 操作数位于第一个中括号前边,另一个操作数位于两个中括号之间。如果opear是一个Stringbad对象,opera[4] 本质是调用了 opera.operator[](4);这样的话,公有方法可以访问私有数据。

将返回类型声明为char的引用,便可以给特定元素赋值:

opera[2] ='r';

在重载时,C++将严格区分常量和非常量的特征标,因此可以提供一个仅供 const Stringbad对象使用的operator[]()版本。

4.静态成员类函数:

可以将成员函数声明为静态的(函数声明必须包含static,但如果函数定义是独立的(声明和定义是分开的),定义环节可以不包括static)。

不能通过对象调用静态成员函数;静态成员函数不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算来调用它。

由于静态成员函数不与特定对象相关联,函数只能使用静态数据成员。

5.进一步重载赋值运算符

Stringbad& Stringbad::operator=(const Stringbad& st)
{
	if (this == &st)
		return *this;         //防止把数据意外释放掉
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

直接使用常规字符串,不用创建和删除临时对象,提高效率。

6.重载>>运算符:

关注while条件,is是为了防止到达文件结尾 或输入错误。

//use_stringbad.cpp
#include<iostream>
#include"stringbad.h"
using std::cout;
using std::endl;
using std::cin;
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
	cout << "Hi,what's your name?\n";
	Stringbad name;
	cin>>name;            //cin还是在std里,因为类用的友元函数,std类中<<  >>根据操作数类型进行判断用哪个函数来输出
	cout << name << ",please enter up to 10 short sayings <empty line to quit>:\n";
	Stringbad input[ArSize];
	char temp[MaxLen];
	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] == '\n')
			break;
		else
			input[i] = temp;
	}
	int total = i;
	if (total > 0)
	{
		cout << "Here are your sayings:\n";
		for (int j = 0; j < total; j++)
			cout << input[j][0] << ":" << input[j] << endl;
		int shortest = 0;
		int first = 0;
		for (i = 1; i < total; i++)
		{
			if (input[i].length() < input[shortest].length())
				shortest = i;
			if (input[i] < input[first])
				first = i;
		}
		cout << "shortest saying:\n" << input[shortest] << endl;
		cout << "First alphabetically:\n" << input[first] << endl;
		cout << "This program uesd " << Stringbad::HowMany() << " String object.bye\n";
	}
	else
		cout << "No input!Bye.\n";
	return 0;
}

说明:

所有的读取,不管是这里还是>>重载,都是要先创建一个临时数组,数据放到临时数组没问题,我再把临时数组的东西赋给输入的类。

较早的cin.get()版本在读取空行后,返回值不为false,而是字符串第一个字符为空,所以while有两个判断条件。 

③在构造函数中使用new时应注意的问题

1.构造函数中使用new来初始化对象,析构函数中应该使用delete,并且二者形式应该匹配或兼容,在众多构造函数中,如果使用[],都要使用,如果不使用,就都不使用,因为只有一个析构函数。

2.要定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。构造函数应该与下边的类似。

Stringbad::Stringbad(const char* s)
{
	len = std::strlen(s);
	str = new char[int(len + 1)];
	std::strcpy(str, s);
	num_strings++;
}

具体地说,复制构造函数应该分配足够的空间来复制数据,而不仅仅是数据的地址,并且还要更新所有受影响的静态类成员。

3.应当定义一个赋值运算符,通过深度复制将一个对象赋值给另一个对象。赋值运算符重载函数与下边的类似。

Stringbad& Stringbad::operator=(const Stringbad& st)
{
	if (this == &st)
		return *this;         //防止把数据意外释放掉
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

具体的说,应该完成这些操作:检查自我赋值情况,释放成员指针以前指向的内存,复制数据而不仅仅是复制数据的地址,并返回一个指向调用对象的引用(允许多次赋值)。

4.包含类成员的类的逐成员复制:

假设类成员的类型为string /Stringbad类,这是否意味着需要为这个类编写复制构造函数或赋值运算符的重载?至少对这个类本身来说不需要,因为类逐成员赋值时,会调用string /Stringbad类的复制构造函数或复制运算符的重载。如果这个类因为其他成员需要定义复制构造函数和赋值运算符,情况更复杂,会在13章介绍。 

④有关返回对象的说明

当成员函数或独立的函数返回对象时,有几种返回方式可以选择。可以返回指向对象的引用,指向对象的const引用或const对象。接下来分别介绍这些返回情况。

1.返回指向const对象的引用

使用const引用的常见原因是提高效率,对于何时可以采用这些方式存在一些限制。如果函数要返回传递给它的对象(通过调用对象的方法或将对象作为参数),可以通过返回引用来提高效率。

三点需要说明:

①返回对象将调用复制构造函数,而返回引用不会。

②引用指向对象应该在调用函数执行执行时存在(不是在被调函数中定义的,因为不能变量在代码块运行结束后释放掉,不能返回指向已经释放的数据)

③函数参数都必须声明为const类型,因此返回类型必须为const,这样才匹配。

2.返回指向非const对象的引用

两种常见的返回非const对象的情形是,重载赋值运算符 以及重载<<运算符,前者是为了提高效率,后者必须这样做。前者是为了连续赋值(并且不需要复制构造函数构造新的对象),后者不仅仅是为了和普通cout一起输出,或者是避免复制构造函数构造新对象,除此之外,最大的原因在于ostream没有公有的复制构造函数,如果不返回引用,出现错误。

3.返回对象

如果返回的对象是被调用函数的局部变量,则不应该按照引用的方式返回它,因为它在函数块运行结束后已经被析构掉,通常,被重载的算术运算符属于这一类。在这种情况下,存在调用复制构造函数来创建被返回对象的开销,这是无法避免的。

4.返回const对象

我们用例子来说明,假设我们定义了vector类的加法重载运算符:

net = force1 + force2;
	force1 + force2 = net;

第一行,两个力相加得到的结果存储在一个临时对象中,该临时对象通过赋值符重载将值赋给net;

第二行,net的值被赋给两个力相加得到的结果存储在一个临时对象中,然后临时对象被丢弃,三个对象的值未发生变化。

如果您担心这种行为可能引发的误用和滥用,将返回类型声明为const vector.那么句法2将非法的。

总结:如果方法或函数返回局部对象,返回对象;如果方法或函数要返回一个没有公有复制构造函数的类的对象,返回指向该对象的引用,其他情况,返回引用,效率更高。

⑤使用指向对象的指针

C++程序经常使用指向对象的指针。这里来练习一下。上一节的程序采用数组索引跟踪最短字符和按字母顺序排在最前边的字符串,另一种方法是使用指针指向这些类别的开始位置。

//use_stringbad.cpp
#include<iostream>
#include"stringbad.h"
#include<ctime>
#include<cstdlib>
using std::cout;
using std::endl;
using std::cin;
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
	cout << "Hi,what's your name?\n";
	Stringbad name;
	cin>>name;            //cin还是在std里,因为类用的友元函数,std类中<<  >>根据操作数类型进行判断用哪个函数来输出
	cout << name << ",please enter up to 10 short sayings <empty line to quit>:\n";
	Stringbad input[ArSize];
	char temp[MaxLen];
	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] == '\n')
			break;
		else
			input[i] = temp;
	}
	int total = i;
	if (total > 0)
	{
		cout << "Here are your sayings:\n";
		for (int j = 0; j < total; j++)
			cout << input[j][0] << ":" << input[j] << endl;
		Stringbad* p_s = &input[0];
		Stringbad* p_f = &input[0];
		for (i = 1; i < total; i++)
		{
			if (input[i].length() < p_s->length())
				p_s = &input[i];
			if (input[i] < *p_f)
				p_f = &input[i];
		}
		cout << "shortest saying:\n" << *p_s << endl;
		cout << "First alphabetically:\n" << *p_f << endl;
		cout << "This program uesd " << Stringbad::HowMany() << " String object.bye\n";
	}
	else
		cout << "No input!Bye.\n";
	srand(time(0));
	int choice = rand() % total;
	Stringbad* favor = new Stringbad(input[choice]);
	cout << "my favorite choice is " << *favor << endl;
	delete favor;
	return 0;
}

说明——使用new初始化对象 :

通常,如果Class_name是类,value的类型为Type_name,则下面的语句:

Class_name * pclass = new Class_name(value);

将调用构造函数:

Class_name(Type_name);肯能还存在一些琐碎的转换,如果是下边的形式,则调用默认构造函数:

Class_name * pclass = new Class_name;

2.再谈new和delete:

        Stringbad* p_s = &input[0];
		Stringbad* p_f = &input[0];

上边的代码使用new为整个对象分配内存;而下边代码不是为存储的字符串分配内存,而是为对象分配内存,也就是说,为了保存字符串地址的str指针和len成员分配内存。

Stringbad* favor = new Stringbad(input[choice]);

创建对象将调用构造函数,后者分配用于保存字符串的内存,当程序不再需要该对象的时候,使用delete删除它,对象是单个的,因此程序使用不带中括号的delete;然而这只释放了用于保存str指针和len成员的空间,并不释放str指向的内存,而该任务将由析构函数来完成(自动调用),最后没有内存泄漏的风险。

在下述情况下,析构函数将被调用:

1.对象是动态变量,当执行完该对象的程序块时,将调用该对象的析构函数;

2.对象是静态变量(外部、静态、静态外部或来自名称空间),程序结束时将调用对象的析构函数;

3. 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。

指针和对象小结:

可以使用常规表示法来声明指向对象的指针:

Stringbad* p_s;

可以将指针初始化为指向已有的对象:

Stringbad* p_s = &input[0];

 可以使用new来初始化指针,浙江创建一个新的对象:

Stringbad* favor = new Stringbad(input[choice]);

对类使用new将调用相应的类构造函数来初始化新创建的对象:

Stringbad* favor = new Stringbad(input[choice]);
调用构造函数:
Stringbad(const char*);

可以使用->运算符通过指针访问类方法:

if (input[i].length() < p_s->length())

 可以对对象指针应用解除引用运算符来获得对象:

cout << "shortest saying:\n" << *p_s << endl;

3.再谈定位new运算符:

第9章从内置类型的角度讨论了new运算符,将这种运算符用于对象时情况有些不同,下述使用了定位new运算符和常规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; }
	~JustTesting() {}
	void Show() const { cout << words << "," << number << endl; }
};
int main()
{
	//创建一个内存块
	char* buffer = new char[BUF];

	JustTesting* p1, * p2;

	p1 = new (buffer) JustTesting;    //定位new 默认构造函数;
	p2 = new JustTesting("Heap1", 20);     //new运算符,初始化的构造函数
	 
	cout << "Memory block addresses:\n";
	cout <<"buffer:" << (void *)buffer << "heap:" << p2 << endl;   //必须强制转换成指针类型,不然cout无法判断输出哪个

	cout << p1 << endl;
	p1->Show();
	cout << p2 << endl;
	p2->Show();

	JustTesting* p3, * p4;
	p3 = new (buffer) JustTesting("Bad idea", 6);
	p4 = new JustTesting("Heap2", 20);

	cout << p3 << endl;
	p3->Show();
	cout << p4 << endl;
	p4->Show();

	delete p2;
	delete p4;

	delete[] buffer;


	return 0;
}

 说明:

1.创建第二个对象的时候,定位new运算符使用了一个新对象覆盖了用于第一个对象的内存单元,所以必须自己计算偏移量的大小:

p3 = new (buffer+sizeof(JustTesting)) JustTesting("Bad idea", 6);

2.将delete用于pc2和pc4时,将自动调用为pc2和pc4指向的对象的析构函数,然而将delete用于buffer时,不会为使用定位new运算符创建的对象调用析构函数,delete[] buffer,释放了使用常规new运算符分配的整个内存块,但他没有为定位new运算符子啊该内存块中创建的对象调用析构函数,存在了内存泄漏,这种问题的解决方案是,显式地为使用new运算符所创建地对象调用析构函数。正常情况下将自动调用析构函数,这是需要显式调用析构函数地少数情形之一。显式调用析构函数时,必须指定要销毁地对象。由于有指向对象的指针,因此可以使用这些指针:

p3->~JustTesting();
	p1->~JustTesting();

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

⑥复习各种技术

至此,已经介绍了多用用于处理与类相关地问题地编程技术,下面对其进行总结,并介绍何时使用它们。

1.重载<<运算符

定义友元运算符:

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

2.转换函数

要将单个值转换为类类型,需要创建原型如下所示的类构造函数:

c_name(type_name value);

要将类转换为其他类型,需要创建原型如下的类成员函数:

operactor type_name();

虽然函数没有声明返回类型,但应返回所需类型的值。使用转换函数要小心,可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。

3.其构造函数使用new的类型

预防措施:

1.构造函数中使用new来初始化对象,析构函数中应该使用delete,并且二者形式应该匹配或兼容,在众多构造函数中,如果使用[],都要使用,如果不使用,就都不使用,因为只有一个析构函数。

2.如果析构函数通过指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。

3.要定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。构造函数应该与下边的类似。

Stringbad::Stringbad(const char* s)
{
	len = std::strlen(s);
	str = new char[int(len + 1)];
	std::strcpy(str, s);
	num_strings++;
}

具体地说,复制构造函数应该分配足够的空间来复制数据,而不仅仅是数据的地址,并且还要更新所有受影响的静态类成员。

4.应当定义一个赋值运算符,通过深度复制将一个对象赋值给另一个对象。赋值运算符重载函数与下边的类似。

Stringbad& Stringbad::operator=(const Stringbad& st)
{
	if (this == &st)
		return *this;         //防止把数据意外释放掉
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

具体的说,应该完成这些操作:检查自我赋值情况,释放成员指针以前指向的内存,复制数据而不仅仅是复制数据的地址,并返回一个指向调用对象的引用(允许多次赋值)。

⑦队列模拟

队列是一种抽象数据类型,可以存储有序的项目序列。新项目被添加在队尾,并可以删除队首的项目。队列是先进先出。队列中的项目是顾客。通常1/3的顾客只需要1分钟就可以获得服务,1/3的顾客需要两分钟,另外的1/3顾客需要3分钟。顾客到达时间是随机的,但每个小时使用自动柜员机的顾客数量相当稳定。

1.队列类

队列的特征:

1)队列存储有序的项目序列                   2)队列所能容纳的项目数有一定的限制

3)能创建空队列                                     4)检查队列是否为空

5)检查队列是否为满                              6)队尾添加项目

7)在队首删除项目                                 8)确定队列中的项目数    

Queue类的公有接口:

public:
	enum { Q_SIZE = 10 };
	Queue(int qs = Q_SIZE);
	~Queue();
	bool isempty() const;
	bool isfull() const;
	bool enqueue(const Item& item);
	bool dequeue(Item& item);

说明: 构造函数创建一个空队列。默认情况下,队列最多存储10个项目,但是可以用显式初始化参数覆盖默认参数:

    Queue line1;
	Queue line2(20);

Queue类的实现: 

1.确定如何表示对列数据。一种是使用new动态分配一个数组,但是对于队列操作而言,数组并不合适,例如当我们删除队首元素后,对于数组来说,要将后边所有的元素都要向前移动一个。链表可以很好的满足队列的需求。链表是一个结构体,包含了数据值以及指向下一个节点的指针。对于这里的队列来说,数据部分就是一个Item的类型的值,因此可以使用下边的结构来表示节点:

struct Node
	{
		Item item;
		struct Node* next;
	};

通常,链表的最后一个指针设置为nullptr,用来指出后边没有节点了。要跟踪链表,必须知道第一个节点的地址,可以让Queue类的一个数据成员指向链表的起始位置,由于队列总是将新项目添加到队尾,因此包含一个指向最后一个节点的数据成员,此外,还可以使用数据成员来跟踪队列可存储的最大项目数以及当前的项目数,类声明完整如下:

class Queue
{
	struct Node
	{
		Item item;
		struct Node* next;
	};
private:
	Item item;
	Node* front;
	Node* rear;
	int items;
	const int q_size;
public:
	enum { Q_SIZE = 10 };
	Queue(int qs = Q_SIZE);
	~Queue();
	bool isempty() const;
	bool isfull() const;
	bool enqueue(const Item& item);
	bool dequeue(Item& item);
};

 上述声明使用了C++的一项特性:在类中嵌套结构或类声明。通过将Node声明放在Queue类里,可以使其作用域为整个类。Node可以使用它来声明类成员,也可以将它作为类方法中类型名称,但只能在类中使用,这样就不用担心Node声明与某些全局声明或其他类声明Node发生冲突。如果声明是在私有部分声明,则只能在这个类使用被声明对象,在公有部分声明,则可以从类的外部通过作用域解析运算符使用被声明的类型。

2.类方法

①类构造函数应该提供类成员的值,但如下方法不可行,因为 q_size是const常量,我们只可以对它初始化,不能对它赋值:

Queue::Queue(int qs)
{
	front = rear = nullptr;
	items = 0;
	q_size = 0;   //错误
}

从概念上来说,调用构造函数前,对象将在括号中代码执行前被创建,我们现在需要将const数据初始化和对象一起创建,C++提供了一种特殊的语法来完成上述工作,它叫做成员初始化列表。成员初始化列表由逗号分隔的初始化列表组成,位于参数列表右括号之后,函数体左括号之前,通常,初值可以是常量或构造函数的参数列表中的参数。这种方法并不限于初始化常量,

Queue::Queue(int qs ): q_size(qs),front(nullptr),rear(nullptr),items(0)
{
	
}

注:

1.只有构造函数可以使用这种初始化列表语法。

2.对于const成员,必须使用这种语法。对于被声明为引用的类成员,也必须使用这种语法(引用和const类似,只能初始化,不能赋值)。

3. 数据成员初始化顺序与他们出现在类声明中的顺序相同,与初始化列表中的排列顺序无关。

4.如果const不采用列表初始化,const数据将被覆盖为默认const的值(定义初始化中的值)。

②入队:

bool Queue::enqueue(const Item& item)
{
	if (isfull())
		return false;
	Node* add = new Node;
	add->item = item;
	add->next = nullptr;
	items++;
	if (front == nullptr)
		front = add;
	else
		rear->next = add;
	rear = add;
	return true;
}

③出队:

bool Queue::dequeue(Item& item)
{
	if (isempty())
		return false;
	item = front->item;
	items--;
	Node* temp = front;    //保留节点进行释放
	front = front->next;
	delete temp;
	if (items = 0)
		rear = nullptr;
	return true;
}

 ④其他的方法:

类构造函数没有使用new,乍一看,好像不用理会由于在构造函数中使用new给类带来的特殊要求。然后向队列中添加项目使用了new来创建新的节点。通过删除节点的方式,dequeue()方法确实可以清楚节点,但这并不能保证队列在到期时为空。因此,类需要一个显式析构函数——该函数删除剩余的所有节点。

Queue::~Queue()
{
	Node* temp;
	while (front != nullptr)
	{
		temp = front;
		front = front->next;
		delete temp;
	}
}

使用new还需要显式定义复制构造函数和赋值运算符的重载,然而有一个小技巧可以避免这样(队列复制的需求太少了),这就是将所需的方法定义为伪私有方法:

private:
	Item item;
	Node* front;
	Node* rear;
	int items;
	const int q_size;
	Queue(const Queue& q) :q_size(0) { }
	Queue& operator=(const Queue& q) { return *this; }

这样做有两个作用:第一,他避免了本来将自动生成的默认方法定义;第二,因为这些方法是私有的,所以不能被广泛应用。也就是说,如果nip 和 tuck 是 Queue对象,则编译器不允许这样做:

Queue snick(nip);
tuck = nip;

 在定义其对象不允许被复制的类时,这种方法也很有用。C++还提供了另一种禁止方法的方式——使用关键字delete,这将在18章介绍。

除此之外,当对象被按值传递或返回时,复制构造函数将被调用,然而如果遵循优先采用引用传递对象的惯例,将不会产生问题。另外,复制构造对象还被用于创建其他对象,但Queue定义中并没有导致创建临时对象的操作,例如加法操作运算符。

2.顾客类

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

class Customer
{
private:
	long arrive;
	int processtime;
public:
	Customer() { arrive = processtime = 0; }
	void set(long when);
	long when() const { return arrive; }
	int ptime() const { return processtime; }
};
void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

3.ATM模拟

现在已经拥有模拟ATM所需的工具。程序允许用户输入三个数:队列最大长度,程序模拟的持续时间(单位为小时)以及平均每小时的客户数。程序将使用循环——每次循环代表1分钟。在每分钟循环里,程序将完成下面的工作:

1.判断是否来了新客户。如果来客户,队列不满,将客户添加到队列里,否则拒绝客户入队。

2.如果没有客户再进行交易,则选取队列的第一个客户。确定该客户的等待时间,并将wait_time计数器设置为新客户所需的处理时间。

3.如果客户正在处理,则将wait_time计数器减1.

4.记录各种数据,如获得服务的客户数目、被拒绝的客户数目、排队等候的累计时间以及累计队列的长度等。

如何确认新客户的到来呢?

假设平均每小时有10名客户到达,这相当于每6分钟有一名客户。我们希望一个随机的过程,而不是平均每6分钟来一名客户。程序将使用下面的函数来确定是否在循环期间有客户到来:

bool newcustomer(double x)
{
	return ((std::rand() * x) / RAND_MAX < 1);
}

程序说明:假设客户到达的平均间隔时间为x为6,则rand * x /RAND_MAX的值将位于0-6之间。平均每隔6次,这个值会有一次小于1.如果客户到达时间小于1分钟,上述方法不适用,但模拟并不是针对这种情况,如果确实需要处理这种情况,最好提高时间分辨率,比如每分钟循环代表10秒钟。

//queue.h
#ifndef QUEUE_H
#define QUEUE_H
class Customer
{
private:
	long arrive;
	int processtime;
public:
	Customer() { arrive = processtime = 0; }
	void set(long when);
	long when() const { return arrive; }
	int ptime() const { return processtime; }
};
typedef  Customer Item;
class Queue
{
	struct Node
	{
		Item item;
		struct Node* next;
	};
private:
	Item item;
	Node* front;
	Node* rear;
	int items;
	const int q_size;
	Queue(const Queue& q) :q_size(0) { }
	Queue& operator=(const Queue& q) { return *this; }
public:
	enum { Q_SIZE = 10 };
	Queue(int qs = Q_SIZE);
	~Queue();
	bool isempty() const;
	bool isfull() const;
	int queuecount()const;
	bool enqueue(const Item & item);
	bool dequeue(Item& item);
};

#endif
//queue.cpp
#include<iostream>
#include"queue.h"
#include<cstdlib>

Queue::Queue(int qs ): q_size(qs),front(nullptr),rear(nullptr),items(0)
{
	
}

bool Queue::isempty() const
{
	return (items == 0);
}
 
bool Queue::isfull() const
{
	return (items == q_size);
}
int Queue::queuecount()const
{
	return items;
}
bool Queue::enqueue(const Item& item)
{
	if (isfull())
		return false;
	Node* add = new Node;
	add->item = item;
	add->next = nullptr;
	items++;
	if (front == nullptr)
		front = add;
	else
		rear->next = add;
	rear = add;
	return true;
}
bool Queue::dequeue(Item& item)
{
	if (isempty())
		return false;
	item = front->item;
	items--;
	Node* temp = front;    //保留节点进行释放
	front = front->next;
	delete temp;
	if (items = 0)
		rear = nullptr;
	return true;
}
Queue::~Queue()
{
	Node* temp;
	while (front != nullptr)
	{
		temp = front;
		front = front->next;
		delete temp;
	}
}

void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}
//bank.cpp
#include<iostream>
#include"queue.h"
#include<cstdlib>
#include<ctime>
bool newcustomer(double x)
{
	return ((std::rand() * x) / RAND_MAX < 1);
}
const int MIN_PER_HR = 60;
int main()
{
	using namespace std;
	std::srand(time(0));
	cout << "Case Study:Bank of Heather Automatic Teller\n";
	cout << "Enter maximum size of queue:";
	int qs;
	cin >> qs;
	Queue line(qs);

	cout << "Enter the number of simulation hours:";
	int hours;
	cin >> hours;
	long cyclelimit = MIN_PER_HR * hours;
   
	cout << "Enter the average number of customers per hours:";
	double perhour;
	cin >> perhour;
	double min_per_cust;
	min_per_cust = MIN_PER_HR / perhour;

	Item temp;
	long turnaways = 0;
	long customers = 0;
	long served = 0;
	long sum_line = 0;
	int wait_time = 0;
	long line_wait = 0;

	for (int cycle = 0; cycle < cyclelimit; cycle++)
	{
		if (newcustomer(min_per_cust))
		{
			if (line.isfull())
				turnaways++;
			else
			{
				customers++;
				temp.set(cycle);
				line.enqueue(temp);
			}
		}

		if (wait_time <= 0 && !line.isempty())
		{
			line.dequeue(temp);
			wait_time = temp.ptime();
			line_wait += cycle - temp.when();
			served++;
		}
		if (wait_time > 0)
			wait_time--;
		sum_line += line.queuecount();
	}

	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 << endl;

	}
	else
		cout << "No customers!\n";
	cout << "Done!\n";
	return 0;
}

 

 

;