Chapter 12 类和动态内存分配

本章内容包括:

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

12.1 动态内存和类

12.1.1 复习示例和静态类成员

类可以有静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。
静态类成员的初始化在方法文件中,不是在类声明文件中进行的,不能再头文件中初始化,因为可能产生多个语句副本,引起错误。
对于动态分配,析构函数的delete是必不可少的。
自动存储对象被删除的顺序与创建顺序相反。
本示例中的程序,是由于编译器自动生成的成员函数引起的。

12.1.2 特殊成员函数

C++会自动定义一些成员函数:

  • 默认构造函数,如果没有定义
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义

隐式地址运算符返回调用对象的地址(即this指针的值)

  1. 复制构造函数
    复制构造函数用于将一个对象复制到新创建的对象中。它用于初始化过程中(包括按值传递参数)而不是常规的赋值过程中。复制构造函数的原型通常如下:
    Class_name(const Class_name &)
  2. 何时调用复制构造函数
    新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。例如:
        StringBad ditto(motto);
        StringBad metoo = motto;
        StringBad also = StringBad(motto);
        StringBad * pStringBad = new StringBad(motto);

最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pStringBad指针。
程序生成了对象副本时,编译器都将调用复制构造函数。

  • 函数按值传递给对象
  • 函数返回对象
  1. 默认的复制构造函数的功能

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

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

复制构造函数没有复制字符串,复制了地址,执行完函数后会销毁复制对象,同时销毁被复制的字符串。

  1. 定义一个显式复制构造函数已解决问题
    解决类设计中这种问题的方法是深度复制。复制时应该复制字符串,并将副本地址赋给str成员。
    如果类中包含了使用new初始化的指针成员,应当定义一个深度复制构造函数。

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

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

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

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

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

将已有的对象赋给另一个对象时,将使用重载的赋值运算符。
初始化对象时,并不一定会使用赋值运算符:初始化总是会调用复制构造函数,使用=运算符可能会调用赋值运算符
2. 赋值的问题出在哪里
数据受损,与成员复制出现的问题相同。
3. 解决赋值的问题
解决方法是提供赋值运算符定义,其实现与复制构造函数类似,但存在差别。

  • 由于目标对象可能引用了以前分配的数据,所以应用delete来释放这些数据。
  • 函数应该避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
  • 函数返回一个指向调用对象的引用

通过返回一个对象,函数可以像常规赋值操作一样,进行连续赋值。
赋值操作并不创建新的对象,因此不需要调整静态数据成员的值。

12.2 改进后的新String类

12.2.1 修订后的默认构造函数

String::String()
{
	len = 0;
	str = new char[1];
	str[0] = '\0'
}
可以将:
	str = new char[1];
	str[0] = '\0'
改成: 
	str = 0;	// set str to the null pointer 
	or 
	str = nullptr;

12.2.2 比较成员函数

在String类中,执行比较操作的方法有三个。按字母顺序排列,第一个字符串在第二个字符串之前,则Operator<()函数返回true。
使用strcmp,第一个参数位于第二个参数之前,返回一个赋值。

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

使用operator来重载该运算符。对于[]运算符,一个操作数位于中括号前面,另一个操作数位于中括号之间。

12.2.4 静态成员函数

可以将成员函数声明为静态的。
静态成员函数:

  • 不能通过对象调用静态成员函数,不能使用this指针,通过使用类名和作用域解析运算符来调用它。
  • 静态成员函数不与特定对象关联,只能使用静态数据成员。
  • 可以使用静态成员函数来设置类级标记,以控制某些类接口的行为。例控制显示类内容所用格式。

12.2.5 进一步重载赋值运算符

重载赋值运算符之前,将一个字符串赋给String对象需要以下几步:

  1. 程序使用构造函数来创建一个临时对象,字符串的值给临时对象
  2. 使用深度复制赋值函数将临时对象复制到name对象中
  3. 调用析构函数删除临时对象
    为提高效率,最简单的方式是重载赋值运算符。

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

  • 如果构造函数使用new来初始化指针成员,则应在析构函数中使用delete
  • new和delete必须相互兼容,new对应delete,new[]对应delet[]
  • 如果有多个构造函数,必须以相同的方式使用new,要么都带[],要么都不带。然而,可以在默认构造函数中将指针初始化为空(0或C++11的nullptr)
  • 应定义一个复制构造函数,深度复制
  • 应该定于一个赋值构造函数,深度复制

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

逐成员复制将使用成员类型定义的复制构造函数和赋值运算符

12.4 有关返回对象的说明

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

12.4.1 返回指向const对象的引用

可以返回对象,效率低,因为会调用复制构造函数;
可以返回const引用

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

重载赋值运算符以及cout<<运算符

12.4.3 返回对象

如果被返回的对象是调用函数的局部变量,则不应按引用方式返回它,应该返回对象。
通常,被重载运算符属于这一类。

12.4.4 返回const对象

如果没有返回常对象,则force1 + force2 = net是合法的,但是该代码却不合理,因此应该返回常对象,不能在左边。
方法或函数要返回局部对象,则应返回对象。

12.5 使用指向对象的指针

使用new初始化对象
Class_name为类,value的类型为Type_name,则下面的语句:
Class_name * pclass = new Class_name(value);
会调用如下构造函数:
Class_name(Type_name)
这里可能还有一些琐碎的转换,如:
Class_name(const Type_name &)
下面的初始化方式将调用默认构造函数:
Class_name * pclass = new Class_name

12.5.1 再谈new和delete

String * favourite = new String(sayings[choice])使用new来为整个对象分配内存:这里是为保存字符串地址的指针和len成员分配内存。
delete favourite,不会释放str指向的内存,该任务由析构函数来完成。
析构函数被调用的时机:

  • 如果对象是自动变量,则执行完定义该对象的程序块时,将调用析构函数。
  • 如果对象时静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时才调用对象的析构函数。
  • 如果对象是用new创建的,仅当显式使用delete删除对象,析构函数才会被调用。

12.5.2 指针和对象小结

使用对象指针,需要注意:

  • 使用常规表示法来声明指向对象的指针
  • 可以将指针初始化为指向已有的对象
  • 使用new来初始化指针,这将创建一个新的对象
  • 对类使用new将调用相应的类构造函数来初始化新创建的对象
  • 可以使用->运算符通过指针访问类方法
  • 通过对对象指针应用指针运算符(*)来获得对象

new的过程:先分配空间,然后将地址赋给指针变量。

12.5.3 再谈定位new运算符

使用定位new运算符:

  • 必须考虑内存管理的问题,编程使用定位new运算符确保使用的两个对象的内存单元不重叠通常使用sizeof()。
  • 使用定位new运算符来为对象分配内存,必须确保析构函数被调用,需要显示调用析构函数。
    对于使用定位new运算符创建的对象,应按照相反的顺序调用析构函数

12.6 复习各种技术

12.6.1 重载<<运算符

重新定义<<运算符,cout一起用来显示对象的内容:

std::ostrem & operator<<(ostream & os,const c_name & obj)
{
	os << ...;
	return os;
}

12.6.2 转换函数

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

12.7 队列模拟

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

12.7.1 队列类

需要设计一个队列类,队列的特征如下:

  • 队列存储有序的项目序列
  • 队列所能容纳的项目数有一定限制
  • 应当能创建空队列
  • 应该能检查队列是否为空
  • 应该能够检查队列是否是满的
  • 应当能够在队尾添加项目
  • 应当能从队首删除项目
  • 应当能够确定队列中项目数
  1. Queue类的接口
    实现返回有几个队列元素;
  2. Queue类的实现
    队列用数组表示不合适,删除数组第一个元素之后,需要将余下的所有的元素向前移动一位。
    链表可以很好的满足队列的要求。链表由节点序列构成,每一个节点包含保存到链表中的信息以及指向下一个节点的指针。
    用一个结构来表示节点:
    Struct Node
    {
    Item item;
    Struct Node * next;
    };

类队列需要有的数据成员,队首、队尾指针,队列最大成员数,队列成员数。
通常将节点的声明放在类中,但只能在类中使用。
嵌套结构和类
在类声明中声明的结构、类或枚举被称为是嵌套在类中,其作用域为整个类。

3.类方法
对于类的私有常变量数据成员,其初始化应该使用列表初始化。成员初始化列表的方法并不限于初始化向量。
对于被声明为引用的类成员,必须使用初始化列表语法。
列表初始化的注意事项:

  • 这种格式只能用于构造函数;
  • 必须用这种格式来初始化非静态const数据成员(C++11之前是这样);
  • 必须用这种格式来初始化引用数据成员。

成员初始化列表使用的()方式也可以用于常规初始化。如int games(162);与int games = 162;等价。C++11的类内初始化

class Classy
{
	int mem1 = 10;
	const int mem2 = 20;
// ...
}

入队方法需要经过几个阶段:
1.如果队列已满,则结束
2.创建一个新节点。如果new无法创建新节点,它将引发异常
3.在节点中放入正确的值
4.将项目计数加1
5.将节点附加到队尾。首先,将节点与队尾节点连接起来,将rear指向新的队尾,如果队列为空,将front指向新节点。
出队需要多个步骤:
1.队列为空就结束。
2.将队列的第一个项目提供给调用函数,item=ftonr->item;
3.将项目计数减1
4.保存front节点的位置
5.让节点出队。front = front->next;
6.删除出队的节点
7.如果链表为空,则将rear设置为NULL,将front设置为NULL

其他类方法:

  • 析构函数释放每个节点
  • **要克隆或赋值队列,必须提供复制构造函数和执行深度复制的赋值构造函数 **
  • 不实现队列的复制构造函数,可以将复制构造函数和赋值运算符设置为私有方法。

12.8 复习题

1.加入String类有如下私有成员:

class String
{
private:
	char * str;
	int len;
// ...
}

a. 下述默认构造函数有什么问题?
String::String(){}
对于使用指针作为私有成员的类,其默认构造函数必须将成员初始化。
b. 下述构造函数有什么问题?
String::String(const char * s)
{
str = s;
len = strlen(s)
}
str只是和字符串的地址相等,没有创建新的字符串
c. 下述构造函数有什么问题?
String::String(const char * s)
{
strcpy(str,s);
len = strlen(s)
}
没有为str分配内存空间,无法复制成功。
2.如果你定义了一个类,其指针成员是使用new初始化的,请指出可能出现的3个问题以及如何纠正这些问题。

  • 析构函数将是试图释放内存两次将对象作为参数时,临时对象浅复制临时对象,函数结束时将对象销毁,编写深复制构造函数代替默认构造函数。
  • 一个对象给另一个对象赋值时,浅复制导致该对象被销毁,编写深复制的赋值函数代替默认赋值函数。
  • 默认构造函数无法与析构函数的delete配对,默认构造函数赋值空指针
    3.如果没有显示提供类方法,编译器将自动生成哪些类方法?请描述这些隐式生成的函数的行为。
  • 默认构造函数,创建一个没有初始化的对象时调用。
  • 默认复制构造函数,需要创建对象副本时调用。
  • 默认赋值函数,将一个对象赋给另一个对象时调用。
  • 析构函数,删除对象时调用
  • 如果每有定义地址运算符,,将提供地址运算符,隐式地址运算符返回调用对象的地址
    4.找出并改正下述类声明的错误:
class nifty
{
// data
	char personality[];
	int talents;
// methods
	nifty();
	nifty(char * s);
	ostream & operator<<(ostream & os, nifty & n);
}

nifty:nifty()
{
	personality = NULL;
	talents = 0;
}
nifty:nifty(char * s)
{
	personality = new char [strlen(s)];
	personality = s;
	talents = 0;
}
ostream & operator<<(ostream & os, const nifty & n)
{
	os << n;
}

方法没有声明公有,形参没有使用const,重载运算符没有使用友元,自定义构造函数分配空间的长度不对,运算符重载实现方法不对,且没有返回值。修改如下:

class nifty
{
// data
	char personality[];
	int talents;
// methods
publice:
	nifty();
	nifty(const char * s);
	friend ostream & operator<<(ostream & os, nifty & n);
}

nifty:nifty()
{
	personality[0] = '\0';
	talents = 0;
}
nifty:nifty(char * s)
{
	personality = new char [strlen(s)+1];
	strcpy(personality, s);
	talents = 0;
}
ostream & operator<<(ostream & os, const nifty & n)
{
	os << n.personality << ": " << n.talents;
	return os;
}

5.对于下面的类声明:
class Golfer
{
private:
char * fullname;
int games;
int * 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();
};
a. 下列各条语句将调用哪些类方法?

Golfer nancy;						// #1
Golfer lulu("Little lulu");				// #2
Golfer roy("Roy Hobbs", 12);				// #3
Golfer * par = new Golfer;				// #4
Golfer next = lulu;					// #5
Golfer hazzard = "Weed Thwacker";			// #6
*par = nancy;						// #7
nancy = "Nancy Putter";					// #8

1调用默认构造函数,2调用自定义构造函数,3调用自定义构造函数,4默认构造函数,5调用复制构造函数,和默认赋值函数,6调用构造函数转换函数 7调用默认赋值函数 8调用构造转换函数**默认赋值运算符**。
b. 很明显,类需要另外几个方法才能更有用,但是类需要哪些方法才能防止数据被破坏呢?
~~深度复制复制构造函数~~,深度复制赋值函数。</font>
posted @ 2021-12-28 20:47  Fight!GO  阅读(52)  评论(0编辑  收藏  举报