本章内容包括:
- 对类成员使用动态内存分配
- 隐式和显式复制构造函数
- 隐式和显式重载赋值运算符
- 在构造函数中使用new所必须完成的工作
- 使用静态类成员
- 将定位new运算符用于对象
- 使用指向对象的指针
- 实现队列抽象数据类型(ADT)
12.1 动态内存和类
12.1.1 复习示例和静态类成员
类可以有静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。
静态类成员的初始化在方法文件中,不是在类声明文件中进行的,不能再头文件中初始化,因为可能产生多个语句副本,引起错误。
对于动态分配,析构函数的delete是必不可少的。
自动存储对象被删除的顺序与创建顺序相反。
本示例中的程序,是由于编译器自动生成的成员函数引起的。
12.1.2 特殊成员函数
C++会自动定义一些成员函数:
- 默认构造函数,如果没有定义
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
隐式地址运算符返回调用对象的地址(即this指针的值)
- 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。它用于初始化过程中(包括按值传递参数)而不是常规的赋值过程中。复制构造函数的原型通常如下:
Class_name(const Class_name &) - 何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。例如:
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);
最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pStringBad指针。
程序生成了对象副本时,编译器都将调用复制构造函数。
- 函数按值传递给对象
- 函数返回对象
- 默认的复制构造函数的功能
默认的复制构造函数将逐个复制非静态成员(成员复制称为浅复制),复制的是成员的值。
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响,因为它们属于整个类,而不是对象。
12.1.3 回到StringBad:复制构造函数的哪里出了问题
复制构造函数没有复制字符串,复制了地址,执行完函数后会销毁复制对象,同时销毁被复制的字符串。
- 定义一个显式复制构造函数已解决问题
解决类设计中这种问题的方法是深度复制。复制时应该复制字符串,并将副本地址赋给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对象需要以下几步:
- 程序使用构造函数来创建一个临时对象,字符串的值给临时对象
- 使用深度复制赋值函数将临时对象复制到name对象中
- 调用析构函数删除临时对象
为提高效率,最简单的方式是重载赋值运算符。
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 队列类
需要设计一个队列类,队列的特征如下:
- 队列存储有序的项目序列
- 队列所能容纳的项目数有一定限制
- 应当能创建空队列
- 应该能检查队列是否为空
- 应该能够检查队列是否是满的
- 应当能够在队尾添加项目
- 应当能从队首删除项目
- 应当能够确定队列中项目数
- Queue类的接口
实现返回有几个队列元素; - 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>