c++学习记录五:析构函数、this关键字和string类

(本文来自c++简明教程)
随着你操纵类的技能越来越熟练,迟早会面临资源管理的问题。
更具体地说,当你设计出特定类型的对象时,程序代码必须要求系统分配一些资源。其中,最常用的资源类型就是内存。正如本章要说明的那样,资源管理会造成一些特殊的问题。不过,辛运的是,c++提供了专门的特性来解决这些问题。
本章展示了如何写一个高级的String类,它简单而又全面地演示了资源管理问题。
说到这个类的目的,它和大多数类一样,都是隐藏细节。操纵字符串时,必须和内存打交道,而String类的宗旨就是让类的用户不必关系内存的分配和回收问题。简单地说,String类封装了char数组,并对它们进行处理,使字符串在类的用户面前表现为一种简单的数据类型,而不是一个复杂的结构。
注意:本章描述的String类是c++中的string类的一个简单版本。假如你的编译器提供了用于支持string类的库,就不必使用本章定义的这个类。不过,通过学习自己编写这样的一个类,有助于你掌握c++面向对象编程时的一些重要概念。尤其是,本章使用String类来展示this关键字、析构函数以及“深拷贝”的概念。


string类入门

c和c++程序员对于c分割的字符串都抱着一种“既爱又恨”的态度······好吧,事实上,人们对它的态度介于从“痛恨”到“勉强用之”这个范围之间。
从好处上说,c风格的字符串(也就是使用null来终止的字符串)提供了对数据的直接访问,而且不会产生任何隐藏的开销。换言之,“所见即所得”。
但是,在使用这种数据类型时,你需要时刻留意各种可能的错误。其中大多数问题都涉及内存空间的分配。假如你没有分配足够大的空间来容纳字符串数据,就可能覆盖其他内存区域,造成很难发现的bug,从而增加程序的调试难度。这是它不好地方。
c/c++字符串是一种符合数据类型,换言之,它是由一系列元素构成:
char name[] = "C++ WithOut Fear";
上述语句创建的不是单个数据项,而是由17个元素构成的一个数组(16个元素用于存储字符串数据,第17个用于存储null终止字符)。
如果有一种String数据类型,能将字符串当作单独的对象来处理,而不必担心它的内部细节,那么将为程序员带来极大的方便。另外,这个String类最好能实现对字符串数据空间的自动分配、回收和重新分配。这样一来,就永远不必担心你是否分配了足够的空间。
然后,就可以像下面这样创建和处理字符串:

1
2
3
4
String st1, str2, str3;
str1 = "To be, ";
str2 = "or not to be.";
str3 = str1 + str2;

本章将展示如何实现这些特性。


类析构函数入门

我们准备先讨论String类的一个简单版本,它能自动完成内存的创建和回收等基本任务。但在此之前,我需要结束“析构函数”的语法。它是一个成员函数,使用以下声明:
~class_name()
换言之,类的析构函数具有与构造函数类似的语法,只不过要在名称之前附加一个前缀~。除此之外,它的参数列表必须为空。
例如,Fraction类可以声明下面这个析构函数:

1
2
3
4
5
class Fraction {
// ....
public:
~Fraction();
};

不过在此之前没有介绍过析构函数的使用,因为当时用不着它。
类的析构函数的作用是在对象被销毁之前清理资源。当你使用任何形式的delete时,可以显式地销毁一个对象:

1
2
3
Fraction *pF = new Fraction;
// ...
delete pF; //销毁pF指向的对象

如果将对象声明为一个变量,然后超出作用域,那么对象也会被销毁:

1
2
3
4
5
void aFunction() {
// ...
Fraction fract1(1, 2); //创建fract1对象
cout << fract1 + 1; //fract1对象被销毁
}

那么,当一个对象被销毁的时候,会发生什么?通常,什么都不会发生。对象占用的内存被释放,并可由其他数据项使用。在此之前,如果有类的析构函数的话,它会被自动调用。
以Fraction类为例,其实没有什么事情需要做。在析构之后,Fraction的数据成员(特别是num和den)会简单地消失。无需特意关闭或释放附加的系统资源。
但是,String类的情况有所不同。String类需要包含一个数据成员——指向字符串数据的一个指针:

1
2
3
4
5
class String {
private:
ptr;
// ...
};

你以后会知道,所有String构造函数都会在对象本身占用的内存上方分配实际的字符串数据(也就是数据成员ptr指向的数据)。析构函数的作用就是释放这个字符串数据占用的空间:
~String() {delete [] ptr;}
假如没有这个析构函数,使用String类会造成持续的内存泄漏。这个问题刚开始或许并不明显。但是,随着越来越多的程序使用这个类,宝贵的内存会极快地流失——直到最后根本没有内存做任何事情,只好重新启动计算机。写得不好的类会使用户感到非常难受和生气。这可不是一件好事情。


示例1:一个简单的String类

本节展示了String类的简单版本。这个版本提供了足够的功能,使它至少有一些实际的用处。它包含两个构造函数、一个析构函数以及一个测试相等性(==)操作符函数,如下图所示。

下面是用于实现和测试该类的代码。
string1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <string.h>
using namespace std;

class String {
private:
char *ptr;
public:
String();
String(char *s);
~String();

int operator==(const String &other);
operator char*() {return ptr;}
};
int main() {
String a("STRING 1");
String b("STRING 2");
cout << "The value of a is: " << endl;
cout << a << endl;
cout << "The value of b is: " << endl;
cout << b << endl;
}
// -----------------------------------------------
// String类的成员函数
String::String() {
ptr = new char[1];
ptr[0] = '\0';
}

String::String(char *s) {
int n = strlen(s);
ptr = new char[n + 1];
strcpy(ptr, s);
}
String::~String() {
delete [] ptr;
}
int String::operator==(const String &other) {
return (strcmp(ptr, other.ptr) == 0);
}

幕后玄机

从某种角度说,这个类相当简单。它只包含一个数组成员,即ptr:

1
2
private:
char *ptr;

这个成员之所以要设为private,是因为它将由负责分配内存块的类成员函数来设置和重新设置。必须保证类外部的代码不能更改这个指针。
类的所有复杂性都来源于它的行为。这个类最重要的规则是,每次将字符串数据赋给String对象时,都必须为它分配字符串数据所需的空间,而且必须将数据的地址赋给ptr。
即使将一个空白字符串赋给String对象,这一点也是成立的。因此,默认构造函数必须分配一个字节的字符串数据,并拷贝一个null终止字符:

1
2
3
4
String::String() {
ptr = new char[1];
ptr[0] = '\0';
}

在这里,假如只是将一个null值(地址=0)赋给ptr,那么是不够的。你一般希望在能够使用一个C/C++字符串(char*)的地方使用一个String对象。在那些情况下,就需要传递一个空字符串,也就是长度为1个字节的字符串,其中包含一个null终止字符:

String(char*)构造函数理解起来就容易得多;显然,它必须从char*参数拷贝字符串数组。构造函数分配足够多的字节来容纳所有字符串数据;这个长度是使用strlen函数来确定的。构造函数还必须分配一个额外的字节来容纳null终止字符(因为strlen函数不会将null终止字符计算在内):

1
2
3
4
5
String::String(char *s) {
int n = strlen(s);
ptr = new char[n + 1];
strcpy(ptr, s);
}

如上一节所述,析构函数必须在对象销毁之前释放字符串数据:

1
2
3
String::~String() {
delete [] ptr;
}

operator==函数很容易写,但要注意,只比较ptr值不会产生正确的结果:

1
2
3
4
//不正确的版本——过于局限
int String::operator==(const String &other) {
return (ptr == other.ptr);
}

operator==函数这个版本的问题在于,它只有在当前对象和other对象(与右操作数对应的对象)指向同一个内存地址时,才会返回true(1)。但是,假定两个String对象都指向一个”cat”字符串,但每个对象都包含了相同的”cat”字符串拷贝。也就是说:

1
2
String str1("cat");
String str2("cat");

在两个字符串具有完全一致的内容时,operator==函数应该返回true——即使字符串的内容存储在不同的内存位置。因此,简单地比较两个指针,不足以测试两个String对象的相等性。

解决方案是调用strcmp库函数。这个函数获取两个字符串地址,并对指向的两个字符串内容进行比较。如果两个字符串的内容相同,它就返回0。这正是我们想要的效果。

1
2
3
int String::operator==(const String &other) {
return (strcmp(ptr, other.ptr) == 0);
}

main函数对String的各个成员函数执行简单的测试。前两个语句调用了String(char*)构造函数:

1
2
3
4
5
6
7
8
int main() {
String a("STRING 1");
String b("STRING 2");
cout << "The value of a is: " << endl;
cout << a << endl;
cout << "The value of b is: " << endl;
cout << b << endl;
}

注意,字符串对象(a和b)是“可打印”的;换言之,可以使用流操作符<<,将它们输出到cout。
之所以能这样做,是因为String类声明了一个char*转换函数。有了这个函数,在能够使用char*的任何地方,都能使用一个String对象:

1
operator char*() {return ptr;}

这个转换函数返回的是字符串数据的地址,该地址包含在数据成员ptr中。

练习

  1. 重写示例1的main函数,在其中使用并测试String类的默认构造函数。
  2. 为String类写operator>和operator<函数。提示:strcmp函数根据第一个字符串按字母表顺序是靠后还是考前,分别返回一个大于零或者小于零的值。所以,字符串”abc”小于”xyz”。

深拷贝和拷贝构造函数

前面设计的String类确实能正常工作,但老实说,它用起来并不方便。事实上,如果你不能为它添加更多有用的功能,那么没有什么理由用它来取代c/c++的char*类型,虽然后者也存在不少的缺点。
如果能够将一个String对象拷贝到另一个,类就显得更有用了。然而,这同时会使我们的设计变得稍微复杂一些。
为了将一个对象拷贝到另一个,最容易想到的办法就是采取默认(编译器提供的)拷贝构造函数的做法:执行一次简单的、逐个成员的拷贝。ptr的值将直接拷贝,使新对象和第一个对象都指向同一个内存位置。这称为“浅拷贝”(shallow copy)。

这种做法在某些情况下是可行的,但存在一个问题:如果str2指向的字符串数据出了什么事,那么该怎么办?具体说来,假如str2超出了作用域,或者因为某个原因而被删除,那么str1对象也会失效。

事实上,在以下代码中,当你试图为str1赋一个新的字符串时,就会出现前面所说的情况:

1
2
String str1("Hello.");
str1 = "cat";

将上述代码添加到示例1中,你会发现它不能将字符串”cat”成功地放到对象变量str1中——至少不能可靠地完成这个操作。在我的计算机上,它最终删除了str1的值,使它似乎包含一个空白字符串。如果你接着打印str1的值,则会得到不尽人意的结果:
cout << str1;
这是为什么呢?请考虑一下当程序执行以下语句时所发生的事情:
str1 = "cat";
默认情况下,编译器会提供一个operator=(const String&)类型的赋值操作符函数,它从一个对象拷贝到相同类型的另一个对象。所以,为了从字符串”cat”赋值,编译器将上述语句解释成下面这种写法:
str1 = String("cat");
程序会做两件事情:

  1. 调用String(char *)构造函数来创建一个临时性的String对象。
  2. 调用operator=(const string&)函数,将一个String对象的内容赋给另一个。

从表面上看,这样做似乎是有效的。但请注意编译器提供的“赋值操作符函数”所采取的操作:它只是执行一次简单的、逐个成员的拷贝。ptr数据成员会接受到临时性String对象的ptr的值。这意味着两个对象都会指向同一个内存地址。
但是,临时String对象很快就会被抛弃(只要语句结束执行)。程序会为临时String对象调用析构函数,并回收字符串数据占用的内存。结果是什么?数据成员str1.ptr将指向一个已经删除的内存区域!
所以,为了避免这种问题,你必须使用“深拷贝”(deep copy)。换言之,当你拷贝一个对象时,它必须能够重构它的全部内容,而不是仅仅拷贝值。在String类的情况下,这意味着要为目标对象赋予字符串数据的一个完整拷贝。

我们可以首先让拷贝构造函数利用”深拷贝”技术。以下代码利用了strlen库函数(它获取一个字符串的当前长度,但最后的null终止字符不计算在内)以及strcpy库函数(它将字符串数据从一个内存区域拷贝到另一个):

1
2
3
4
5
String::String(const String &src) {
int n = strlen(src.ptr);
ptr = new char[n + 1];
strcpy(ptr, src.ptr);
}

一个正确编写的赋值操作符函数将使用类似的代码。但是,你首先必须理解另一个c++关键字:this


this关键字

初一看,this关键字似乎有些另类。一些程序员经常用它,另一些程序员则很少用它——除非是在赋值操作符的情况下(你稍后就会看见,在这种情况下必须使用this)。
简单地说,this关键字是指向当前对象的一个指针。所谓“当前对象”,是值通过它来调用一个成员函数的对象。this关键字只有在类的成员函数内部才有意义。
调用一个成员函数时,c++编译器实际会传递一个隐藏的参数,即this指针。看看下面这个对Fraction类成员函数的调用:
fract1.set(1, 3);
虽然在源代码中看不出来,但上述函数调用实际会转换成以下形式:
fract1::set(&fract, 1, 3);
换言之,指向fract1的一个指针将作为隐藏的第一个参数来传递。在成员函数内部,你可以使用this来访问该参数。
Fraction对象如下图所示。

下面是Fraction类的set函数的定义:

1
2
3
4
5
6
void set(int n, int d)
{
num = n;
den = d;
normalize();
}

但是,由于还存在一个隐藏的this指针,所以从行为上看,上述函数似乎是像下面这样写的:

1
2
3
4
5
void set(int n, int d) {
this -> num = n;
this -> den = d;
normalize();
}

而且你事实上真的可以像上面那样来写set函数。当然,这并不是必须的,因为在成员函数中,如果不加限定地引用一个数据成员(比如num),那么会默认为this指针来引用(即this->num)。大多数时候,用this指针来引用数据成员虽然合法,但没有多大必要。
另外要说的是,对normalize的调用也会传递一个隐藏的this指针,虽然这一点在源代码中没有反映出来:
normalize(this);
实际上,这个语句并不合法,因为this指针虽然是作为一个参数来传递的,但必须保存隐藏状态。合法的写法如下所示——虽然和引用数据成员一样,你并非要使用这种写法:
this -> normalize();


赋值操作符

那么,this指针和赋值操作符到底有什么关系呢?为了理解这个问题,你首先需要复习一下c++的赋值操作是如何进行的。记住,赋值操作符(=)和其他所有操作符一样,会构成一个表达式,而且必须生成一个值——虽然它还具有一个重要的“副作用”,也就是设置其左侧操作数的值。
赋值表达式(比如x=y)为了生成一个值,会返回所赋的值(也就是左侧操作数的新值)。然后,这个值可以在一个更大的表达式中重用。例如以下语句:
x = y = 0;
该语句等价于以下语句:
x = (y = 0);
在其中,表达式y=0会返回y的新值(即0),并把它赋给左侧的操作数(即x)。所以,该语句的结果是将0同时赋给x和y。

所以,在c++中,赋值操作符函数也必须生成一个值,也就是左侧操作数的值。对于类的操作符函数来说,左侧的操作数就是“当前对象”,也就是通过它调用该函数的那个对象。

但是,对象如何返回它本身么?这就需要用到this指针。将提领操作符(*)应用于this,你返回的就不是一个指向当前对象的指针,而是对象本身。
换言之,以下语句:
return *this;
表示的意思是:“返回我自己!”
虽然上述逻辑推导过程听起来很复杂,但你实际只需记住以下基本规则:

基本规则:在任何赋值操作符(=)的定义中,最后一个语句应该是return *this。

this关键字真正的用处其实就在这个地方。不过,这个关键字偶尔也能在其他一些地方发挥作用,比如当一个对象需要调用一个全局函数,并向函数显式地传递指向它自己的一个指针的时候。但是,它绝大多数时候都只在赋值操作符函数中使用。

为了简化问题,让我们首先创建一个cpy函数,它的作用是从一个char*参数中拷贝字符串数据。该函数会释放当前的字符串数据内存,为新的字符串分配的内存,然后通过调用strcpy来执行拷贝。

1
2
3
4
5
6
void String::cpy(char *s) {
delete [] ptr;
int n = strlen(s);
ptr = new char[n+1];
strcpy(ptr, s);
}

接着就可以很容易地写出一个合格的赋值操作符函数:

1
2
3
4
String &operator = (const String &src) {
cpy(src.ptr);
return *this;
}

注意,该函数返回的是一个String对象引用(返回类型为String&),而不是返回String对象本身。这个返回类型能避免不必要地使用拷贝构造函数。记住,使用引用类型,可以在幕后传递一个指针,但同时避免使用指针语法。

也可以写一个赋值操作符函数,让它获取一个char*参数。虽然该函数对于类来说并不是必须的(因为已经由一个构造函数支持从char*类型的转换),但这个函数的存在,能使某些表达式的求值变得更高效。

1
2
3
4
String &operator = (char *s) {
cpy(s);
return *this;
}

上述两个函数都非常简单,所以适合进行内联。

注意:比较拷贝构造函数(参见后面的示例2)和赋值操作函数(它调用cpy成员函数)的定义,你会发现两者做的是相似的事情。但它们存在一个重要的区别:cpy成员函数假定当前对象已经创建并初始化。所以,它能直接使用delete操作符来回收当前由ptr指向的内存。相反,拷贝构造函数不能进行这样的假定,因为对象是新建的。


写一个连接函数

String类现在变得有用多了。例如,你可以反复将不同的值赋给同一个字符串,不必担心这样做会超出为字符串保留的空间。在下例中,我们首先为str1赋一个较短的字符串(“the”),接着赋一个较大的字符串(“the cat”);

1
2
3
4
5
6
7
8
String str1, str2, str3;
str1 = "the";
cout << str1 << endl;
str1 = "the cat";
cout << str1 << endl;
str2 = "the cat";
if(str1 == str2)
cout << "str1 and str2 hold the same data." << endl;

但是,我们还可以使String类变得更有用。你真正想写的也许是下面这样的语句:

1
2
3
String a("the");
String b("end.");
String c = "This is " + b + c;

为了支持这样的写法,你必须为String类写一个operator+函数。为了简化问题,让我们先来写一个名为cat的成员函数(cat代表”concatenation”,即”连接”)。写好cat之后,operator+函数就很容易写了。

和String的大多数成员函数一样,这里要解决的一个基本问题在于,你需要分配多大的字符串空间。幸好这个问题很容易解决,因为你惟一要做的就是使用strlen库函数来判断两个字符串的长度,然后把它们加起来。
cat函数的算法是:

1
2
3
4
5
将N设为当前字符串与要连接的字符串的长度之和
分配长度为N+1的一个新的char内存块,让指针P1指向这个快
将当前字符串拷贝到P1指向的内存块,将新字符串数据连接到后面
删除ptr指向的旧内存块
设置数据成员ptr,让它指向和P1相同的地址

使用这个算法,旧内存块(由ptr指向)会在数据拷贝到新的内存块之后被删除。这意味着你不能一开始就使用delete操作符,而且必须使用指针P1来临时性地存储新地址。
下面是实现了该算法的c++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void String::cat(char *s) {
//为新的字符串数据分配足够的空间
int n = strlen(ptr) + strlen(s);
char *p1 = new char[n+1];

//将数据拷贝到新的内存块
strcpy(p1, ptr);
strcpy(p1, s);

//回收旧的内存块,并更新ptr
delete[] ptr;
ptr = p1;
}

现在,operator+函数很容易写出来。调用一下cat函数,就能完成所需的大多数工作:

1
2
3
4
5
String String::operator+(char *s) {
String new_str(ptr);
new_str.cat(s);
return new_str;
}

由于有了成员函数cat的支持,operator+显得十分简单。但要注意的是,和String类的其他操作符函数不同,operator+函数不能返回一个引用。这是由于操作符+会创建一个新对象——它不等于操作符+左右的任何一个操作数。另外,整个对象(而不是引用)必须返回给函数的调用者。

请记住以下基本规则:

基本规则:一个成员函数需要返回一个现有的对象时(比如在赋值操作符函数中),应该返回一个引用类型。但是,假如函数需要返回一个新的对象,就不应返回引用类型。


示例2:完整的String类

现在,我们很容易写出一个相当完整的String类,虽然它还为你预留了大量空间来添加更多的功能。为了完成这个String类,我们只需添加前几节描述的成员函数。
string2.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <iostream>
#include <string.h>
using namespace std;

class String {
private:
char *ptr;
public:
String();
String(char *s);
String(const String &src);
~String();

String& operator=(const String &src)
{cpy(src.ptr); return *this;}

String& operator=(char *s)
{cpy(s); return *this;}

String operator+(char *s);
int operator==(const String &other);
operator char*() {return ptr;}

void cat(char *s);
void cpy(char *s);
};
int main() {
String a, b, c;
a = "I ";
b = "am ";
c = "so ";
String d = a + b + c + "very happy!\n";
cout << d;
return 0;
}
// -----------------------------------------------
// String类的成员函数
String::String() {
ptr = new char[1];
ptr[0] = '\0';
}

String::String(char *s) {
int n = strlen(s);
ptr = new char[n + 1];
strcpy(ptr, s);
}

String::String(const String &src) {
int n = strlen(src.ptr);
ptr = new char[n+1];
strcpy(ptr, src.ptr);
}

String::~String() {
delete [] ptr;
}
int String::operator==(const String &other) {
return (strcmp(ptr, other.ptr) == 0);
}

String String::operator+(char *s) {
String new_str(ptr);
new_str.cat(s);
return new_str;
}

//cpy函数--用于拷贝字符串
//
void String::cpy(char *s) {
delete [] ptr;
int n = strlen(s);
ptr = new char[n+1];
strcpy(ptr, s);
}

//cat函数--用于连接字符串
//
void String::cat(char *s) {
//为新的字符串数据分配足够的空间
int n = strlen(ptr) + strlen(s);
char *p1 = new char[n+1];

//将数据拷贝到新的内存块
strcpy(p1, ptr);
strcpy(p1, s);

//回收旧的内存块,并更新ptr
delete[] ptr;
ptr = p1;
}

幕后玄机

本例的所有函数都已经在前面的小节中讲过,所以这里不再赘述。但是,本例确实由一个地方值得讨论以下。你或许已经注意到,整个程序只提供了一个版本的operator+函数:

1
2
3
4
5
String String::operator+(char *s) {
String new_str(ptr);
new_str.cat(s);
return new_str;
}

该函数假定右侧的操作数具有char*类型。所以,它支持像下面这样的操作:

1
2
String a("King ");
String b = a + "Kong";

但是,它怎么支持两个操作数都是String对象的操作呢?例如:

1
2
String a("King "), b("Kong");
String b = a + b;

答案是:虽然该类并不直接支持两个String对象的+操作,但由于有一个好用的char*转换函数,所以这个问题得到了圆满的解决:
operator char*() {return ptr;}
该类支持如何将String对象转换成char*类型。某些情况下,只有执行这种转换,才能合法地求值一个表达式。在上面的“Kong”表达式中,对象b会转换成char*字符串,所以语句会转换成:
String b = a + "Kong";
这样一来,提供一个版本的operator+函数就可以了。
但是,函数的写法仍然存在一个限制。由于operator+是一个成员函数,而不是一个全局函数,所以String对象只能出现在操作符的右边。换言之,以下语法是非法的:
String b = "My name is " + a + "Kong.";
为了使上述语句成为合法语句(允许一个char*字符串作为操作符+的左操作数),惟一的办法就是重写operator+函数,把它变成一个全局的友元函数。这将作为练习题来解决。

练习

  1. 将operator+函数重写为一个全局的友元函数。提示:除了函数的声明方式,无需对代码进行实质性的更改。
  2. 修改有一个char*参数的所有成员函数,让它们使用const char*类型的一个参数。main中的代码仍能工作吗?
  3. 添加一个能转换成整数的函数,它调用atoi库函数,将字符串中出现的数位(如果有的话)转换成一个整数。再写一个能转换成double值的函数,它调用atof库函数(记住要包容stdlib.h)
  4. 写一个String(int n)构造函数,它初始化字符串数据,在其中包含n个空格。再写一个operator=(int n)函数,让它执行类似的赋值。
  5. 为String类写一个operatorp[]函数,以便直接模拟数组索引,不需要提取一个char*字符串,就能访问单独的字符。该函数应返回一个char的引用,以便用数组索引来获取一个“左值”(也就是能在赋值操作符左侧出现的一个表达式)。
    该函数的声明如下:
    char& operator[] (const int i);

小结

下面总结了第15章的要点:

  • 类析构函数是在对象被销毁之前调用的。如果一个对象占用了内存或者其他需要关闭(回收)的系统资源,就有必须写一个析构函数。析构函数使用以下声明:~class_name()
  • 使用delete操作符来显式地销毁一个对象,或者在对象超出作用域的时候,就会调用析构函数。
  • “浅拷贝”是值一个对象和另一个对象之间的一次简单的、逐个成员的拷贝。对于简单的类,这通常是足够的。
  • “深拷贝”将重构一个对象的内容,然后将它们拷贝到另一个对象。这种拷贝要求的作者写一个拷贝构造函数和一个赋值操作符函数。如果类需要操纵系统资源(比如内存或文件),那么通常都有必要执行深拷贝。
  • this关键字可以在一个类的成员函数中使用,它转换成指向当前对象的一个指针。所谓“当前对象”,就是通过它来调用函数的那个对象。
  • 在一个成员函数中,未限定的成员引用等价与通过this指针来引用。比如在String类中,normalize函数调用等价于:this -> normalize();
  • 在任何版本的赋值操作符函数中,最后都应该返回对当前对象的一个引用:return *this;
  • 如果成员函数要返回一个现有的对象,那么返回类型应该是一个引用类型(比如是String&,而不是String)。例如,所有赋值操作符函数应该像下面这样返回一个引用:
    1
    2
    3
    4
    String& String::operator=(const String &src) {
    cpy(src.ptr);
    return *this;
    }
  • 如果成员函数(比如operator+)要返回一个全新的对象,它的返回类型就不应该是一个引用类型。相反,它们需要创建一个新对象,设置它的值,然后返回对象本身:
    1
    2
    3
    4
    5
    String String::operator+(char *s) {
    String new_str(ptr);
    new_str.cat(s);
    return new_str;
    }

c++学习记录五:析构函数、this关键字和string类

http://example.com/2018/01/27/cplusplus5/

作者

bd160jbgm

发布于

2018-01-27

更新于

2021-05-08

许可协议