C++ —— 自定义 StringBad 类

假设要创建一个类,该类的一个成员表示人的姓名。最简单的方法是使用字符数组来保存姓名,但这种方法有一些缺陷,刚开始也许会使用一个10个字符的数组,然后发现有的人名字太长,数组存不下,更保险的方法是,使用一个40个字符的。然而,如果创建包含2000个这种对象的数组,因为字符数组只使用了一部分,就会浪费大量的内存。
解决这种问题的方法是:在程序运行时(而不是编译时)就确定使用多少的内存。C++ 的方法是,在类构造函数中使用 new 运算符在程序运行时分配所需的内存。对于保存姓名字符的这种情况,通常是使用 string 类,该类为我们处理内存管理的细节,但是这样就没有机会深入学习内存管理了,因此本节自定义 string 类代码。
这里首先将设计一个 StringBad 类,然后再设计一个功能稍强的 String 类。StringBad 和 String 类将包含一个字符串指针和一个表示字符串长度的值。这里使用 StringBad 和 String 类是为了深入了解 new、delete 和静态类成员的工作原理。之所以命名为 StringBad 是为了提示 StringBad 是一个有缺陷的类,而 String 类则是修复 StringBad 中的缺陷。

StringBad

类声明

#ifndef STRINGBAD_H_
#define STRINGBAD_H_
#include <iostream>
class StringBad {
private:
    char* str; // 字符指针
    int len; // 记录字符串长度
    static int strNum; // 记录StringBad对象数目
public:
    StringBad();
    StringBad(const char *);
    ~StringBad();
    friend std::ostream & operator<<(std::ostream &, const StringBad &);
};
#endif // STRINGBAD_H_

对于这个类声明需要注意两点:
首先,使用 char 指针来表示姓名,而不是 char 数组。这意味着类声明没有为字符本身分配存储空间,而是在构造函数中使用 new 来为字符串分配空间,这避免了在声明中预先定义字符串的长度。
其次,将 strNum 声明为静态存储的类型。静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类成员的副本。也就是说,类的所有对象都共享同一个静态成员,这对于所有类对象都具有相同值的类私有数据是非常方便的。

类实现

#include "StringBad.h"
#include <cstring>
using std::cout;

int StringBad::strNum = 0;
// const double StringBad::s = 3.6;
StringBad::StringBad() {
    len = 4;
    str = new char[4];
    std::strcpy(str, "C++");
    strNum++;
    cout << "(+)" << strNum << ":\"" << str << "\" default object created\n";
}

StringBad::StringBad(const char * s) {
    len = strlen(s) + 1;
    str = new char[len];
    strcpy(str, s);
    strNum++;
    cout << "(+)" <<  strNum << ":\"" << str << "\" object created\n";
}

StringBad::~StringBad() {
    cout << "(-):\"" << str << "\" object deleted, " << --strNum << " left.\n";
    delete [] str;
}

std::ostream & operator<<(std::ostream & out, const StringBad & sb) {
    out << sb.str;
    return out;
}

静态成员的初始化

int StringBad::strNum = 0;这条语句将静态成员 strNum 的值初始化为零。需要注意:不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。还需要注意初始化语句指出了类型,使用了作用域运算符,但没有使用关键字 static。
在这里插入图片描述

静态类成员初始化是在方法文件中,而不是类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。

唯一可以在类声明中初始化的特例是静态数据成员为 const 整型or枚举型数据,其他类型的 const 变量也是要在类外初始化的。

接下来,注意到每个构造函数都包含表达式 strNum++,这确保程序每创建一个新对象,共享变量 strNum 的值都会 +1,从而记录 String 对象的总数。另外,析构函数中 --strNum,因此,StringBad 类也能跟踪对象被删除的情况,从而使 strNum 成员的值是最新的。

构造函数

StringBad::StringBad(const char * s) {
    len = strlen(s) + 1;
    str = new char[len];
    strcpy(str, s);
    strNum++;
    cout << strNum << ":\"" << str << "\" object created\n";
}

该构造函数使用一个常规的 C 字符串来初始化 StringBad 对象。类成员 str 是一个字符指针,因此需要构造函数分配足够的内存来存储字符串,然后将字符串的内容复制到申请的内存中。下面来介绍其中的每一个步骤。
首先,使用 中的 strlen() 函数来计算字符串的长度,并对 len 成员进行初始化;其次,使用 new 分配足够的空间,并将该地址赋值给 str 成员;第三,使用 strcpy() 函数将传递的字符串复制到申请的内存中;第四,更新对象的技术;最后,构造函数将显示当前的对象数目和当前对象中存储的字符串,这样便于掌握程序的运行情况,以便稍后故意使 StringBad 出错时,该特性将排上用场。

必须要使用构造函数中的这种方式,因为字符串并不是保存在对象中,而是单独保存在堆内存中,对象中的 str 仅保存了到哪里去查找字符串。因此不能这么做:

StringBad::StringBad(const char * s) {
    str = s;
}

这样只是让 str 指向了参数 s 指向的地址,并没有创建字符串的副本。

默认构造函数和该构造函数相似,默认构造函数提供了一个默认的字符串 —— “C++”。

析构函数

StringBad::~StringBad() {
    cout << ":\"" << str << "\" object deleted, " << --strNum << " left.\n";
    delete [] str;
}

cout 语句将会在析构函数被调用时输出一些信息,以便能够得知析构函数何时被调用,这部分不是必不可少的。然而,delete 语句却是至关重用的。str 成员指向的是 new 分配的内存,StringBad 对象过期时,str 也将过期,所以 str 指向的内存应该被释放。也就是说,删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的动态分配的内存。因此,必须使用析构函数,在析构函数中使用 delete 语句确保对象过期时,有构造函数使用 new 分配的内存被释放。

PS:在构造函数中使用 new 来分配内存时,必须在析构函数中使用 delete 来释放内存;在构造函数中使用 new[] 来分配内存时,必须在析构函数中使用 delete[] 来释放内存。

类设计的缺陷

下面的程序演示了 StringBad 的构造函数和析构函数何时运行以及如何运行的。

#include "StringBad.h"
#include "String.h"
#include <iostream>

// 引用传递
void callStringBad1(StringBad &);
// 值传递
void callStringBad2(StringBad);

int main() {
    using std::cout;
    using std::endl;
    cout << "Use StringBad.\n";
    StringBad sb1("Hello, World");
    StringBad sb2("You Ka");
    StringBad sb3("Mo Xu");
    cout << "sb1: " << sb1 << endl;
    cout << "sb2: " << sb2 << endl;
    cout << "sb3: " << sb3 << endl;
    callStringBad1(sb1);
    cout << "sb1: " << sb1 << endl;
    callStringBad2(sb2);
    cout << "sb2: " << sb2 << endl;
    cout << "Initialize one object to another:\n";
    StringBad sb4 = sb3;
    cout << "sb4: " << sb4 << endl;
    cout << "Assign one object to another:\n";
    StringBad sb5;
    sb5 = sb1;
    cout << "sb5: " << sb5 << endl;
    cout << "End of useStringBad().\n";
    return 0;
}

void callStringBad1(StringBad & sb) {
    std::cout << "Passed By Reference --- StringBad: \"" << sb << "\"\n";
}

void callStringBad2(StringBad sb) {
    std::cout << "Passed By Value --- StringBad: \"" << sb << "\"\n";
}
Use StringBad.
(+)1:"Hello, World" object created
(+)2:"You Ka" object created
(+)3:"Mo Xu" object created
sb1: Hello, World
sb2: You Ka
sb3: Mo Xu
Passed By Reference --- StringBad: "Hello, World"
sb1: Hello, World
Passed By Value --- StringBad: "You Ka"
(-):"You Ka" object deleted, 2 left.
sb2: You Ka
Initialize one object to another:
sb4: Mo Xu
Assign one object to another:
(+)3:"C++" default object created
sb5: Hello, World
(-):"Hello, World" object deleted, 2 left.
(-):"Mo Xu" object deleted, 1 left.
(-):"Mo Xu" object deleted, 0 left.
(-):"You Ka" object deleted, -1 left.
(-):"Hello, World" object deleted, -2 left.

仔细观察输出结果,发现析构函数输出的信息中,剩余对象数量竟然出现负数,这很不正常。

异常现象

程序在刚开始时还是正常的,但逐渐变得异常,我们先来看程序运行正常的部分。使用 StringBad 构造方法创建了3个 StringBad 对象,使用友元函数重载的 << 运算符展示这三个对象,对应的输出如下:

Use StringBad.
(+)1:"Hello, World" object created
(+)2:"You Ka" object created
(+)3:"Mo Xu" object created
sb1: Hello, World
sb2: You Ka
sb3: Mo Xu

然后,程序将 sb1 作为实参传递给 callStringBad1() 函数,并在调用后再展示一次 sb1,对应的输出如下:

Passed By Reference --- StringBad: "Hello, World"
sb1: Hello, World

这部分是正常的,但程序之后将 sb2 作为实参传递给 callStringBad2() 函数,然后在调用之后展示 sb2,对应的输出如下:

Passed By Value --- StringBad: "You Ka"
(-):"You Ka" object deleted, 2 left.
sb2: You Ka

这里 callStringBad2() 按值传递 sb2,输出表明这是一个严重的问题!因为 callStringBad2() 调用结束时调用了一次析构函数,此后为对象自动调用析构函数时就会出现下面的情况:

(-):"Hello, World" object deleted, 2 left.
(-):"Mo Xu" object deleted, 1 left.
(-):"Mo Xu" object deleted, 0 left.
(-):"You Ka" object deleted, -1 left.
(-):"Hello, World" object deleted, -2 left.

实际上,计数异常是一条线索,因为每个对象被构造和析构一次,因此调用构造函数的次数应该与析构函数的调用次数相同。对象计数递减次数比递增多两次,这表明使用了不将 strNum 递增的构造函数创建了两个对象。在类声明中定义了两个将 strNum 递增的构造函数,但结果表明,存在第三个构造函数。例如,请看下面的代码:

StringBad sb4 = sb3;

这使用的是那个构造函数呢?不是默认构造函数,也不是参数为 const char * 的构造函数,其实该语句等效于下面的语句

StringBad sb4 = StringBad(sb3);

因此该构造函数的原型应该如下:

StringBad(const StringBad &);

当使用一个对象来初始化另一个对象时,编译器将自动上述构造函数,该构造函数被称为复制构造函数。这个例子中所有的问题都是由于编译器自动生成的复制构造函数造成的。

复制构造函数问题

问题描述

StringBad 使用程序中出现的异常之处 —— 析构函数的调用比构造函数次数多2次,原因是程序使用默认的复制构造函数另外创建了两个对象。在程序 callStringBad2() 被调用时用复制构造函数初始化形参,还使用复制构造函数初始化 sb4 对象。默认的复制构造函数只是将非静态成员的值复制了,没有修改静态成员 strNum,但析构函数被调用时更新了 strNum。对于这个问题的解决方案就是声明一个对 strNum 进行更新的显式复制构造函数。

介绍

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

class_name(const class_name &);

它接受一个指向本类的对象的常量引用作为参数。例如,上例中 StringBad 类的复制构造函数的原型如下:

StringBad(const StringBad &);

对于复制构造函数需要知道两点 —— 何时调用以及有什么功能。

何时调用?

新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显示地初始化为一个现有对象。例如,假设 sb 是一个 StringBad 对象,下面的声明都将调用复制构造函数:

StringBad sb1 = StringBad(sb);
StringBad sb2 = sb;
StringBad sb3(sb);
StringBad * psb = new StringBad(sb);

前两种声明可能会使用复制构造函数直接生成 sb2 和 sb3,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给 sb2 和 sb3,这取决于具体实现。
另一种常见的调用复制构造函数是:调用函数时按值传递类对象或者函数按值返回对象。更广泛地来说,生成临时对象时都会调用复制构造函数。例如,重载 + 运算符之后,将 3 个类对象相加时,编译器可能生成临时的类对象来保存中间结果。具体来说,上面 StringBad 类的使用程序中,下面的函数调用将调用复制构造函数:

 callStringBad2(sb2);

正是由于按值传递类对象将调用复制构造函数,因此应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间。

默认复制构造函数(浅拷贝)

默认的复制构造函数逐个复制非静态成员,复制的是成员的值,在代码中下述语句:

 StringBad sb4 = sb3;

与下面的代码等效(不过由于私有成员无法访问,因此这些代码不能通过编译):

StringBad sb4;
sb4.str = sb3.str;
sb4.len = sb3.len;

如果类的成员本身就是类对象,则使用该类的复制构造函数来复制成员对象。
在这里插入图片描述

解决方案

显式声明一个更新 strNum 成员的复制构造函数:

class StringBad {
public:
    // ...
    StringBad(const StringBad &);
}

StringBad::StringBad(const StringBad & sb) {
    str = sb.str;
    len = sb.len;
    strNum++;
    cout << "(+)" << strNum << ":\"" << str << "\" object created(Copy constructor)\n";
}

添加了 StringBad 类的复制构造函数之后再次运行程序,可以发现计数异常问题已经被解决:

Use StringBad.
(+)1:"Hello, World" object created
(+)2:"You Ka" object created
(+)3:"Mo Xu" object created
sb1: Hello, World
sb2: You Ka
sb3: Mo Xu
Passed By Reference --- StringBad: "Hello, World"
sb1: Hello, World
(+)4:"You Ka" object created(Copy constructor)
Passed By Value --- StringBad: "You Ka"
(-):"You Ka" object deleted, 3 left.
sb2: You Ka
Initialize one object to another:
(+)4:"Mo Xu" object created(Copy constructor)
sb4: Mo Xu
Assign one object to another:
(+)5:"C++" default object created
sb5: Hello, World
End of useStringBad().
(-):"Hello, World" object deleted, 4 left.
(-):"Mo Xu" object deleted, 3 left.
(-):"Mo Xu" object deleted, 2 left.
(-):"You Ka" object deleted, 1 left.
(-):"Hello, World" object deleted, 0 left.

如果类中包含静态数据成员,并且该成员在新对象被创建时发生变化,则应该提供一个显式的复制构造函数来处理。

动态内存分配问题

问题描述

虽然从输出上来看,该程序好像挺正常的,但实际上还存在一个更危险的问题 —— 字符指针指向的动态内存分配被多次释放。原因在于声明的复制构造函数中 str 成员是按值复制的:

str = sb.str;

这里复制的不是字符串的内容,而是一个指向字符串的指针,也就是说,通过复制构造函数初始化的对象和原先的对象的 str 成员指向的是同一个字符串的指针。当某一个对象调用析构函数释放动态分配的内存之后,另一个对象的 str 成员指向的就是一块已经被释放过的地址。例如:

(+)4:"You Ka" object created(Copy constructor)
Passed By Value --- StringBad: "You Ka"
(-):"You Ka" object deleted, 3 left.
sb2: You Ka

在调用 callStringBad2() 函数时,将通过复制构造函数初始化形参,此时形参和 sb2 的 str 成员指向同一个地址,当 callStringBad2() 调用结束会调用形参的析构函数销毁形参,此时 sb2 的 str 成员指向的就是一个被释放过的地址。虽然在 callStringBad2() 调用之后使用了 << 运算符展示的 sb2 看上去并没有问题,但这段代码非常危险,会导致不确定的、可能有害的后果,特别是尝试释放内存两次可能导致程序异常终止。

解决方案

解决这类问题的办法是声明复制构造函数时采用深拷贝。也就是说,复制构造函数应当复制字符串的内容并将副本的地址赋给 str 成员。这样每个成员都拥有自己的字符串,而不是引用另一个对象的字符串,每个对象在调用析构函数时都将释放不同的字符串,而不会去释放已经被释放的字符串。深拷贝的复制构造函数可以这样编写:

StringBad::StringBad(const StringBad &sb) {
    // 正确的复制构造函数
    len = sb.len;
    str = new char[len + 1];
    strcpy(str, sb.str);
    strNum++;
    cout << "(+)" << strNum << ":\"" << str << "\" object created(Copy constructor)\n";
}

如果类成员是使用 new 初始化的指针成员,应定义一个赋值构造函数,以复制指向的数据,而不是指针,这被称为深拷贝。

赋值运算符问题

问题描述

除了默认复制构造函数的问题之外,StringBad 类设计中还存在赋值运算符的问题。ANSI C 允许结构赋值,而 C++ 允许类对象赋值,这是通过编译器自动为类重载赋值运算符来实现的。如果不显式声明一个赋值运算符,在运行下面的程序时会出现释放同一块内存的现象:

StringBad sb5;
sb5 = sb1;
介绍

赋值运算符的原型如下:

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

赋值运算符接受并返回一个指向类对象的引用。例如,StringBad 类的赋值运算符的原型如下:

StringBad & StringBad::operator=(const StringBad &);
何时调用?

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

StringBad sb5;
sb5 = sb1;

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

StringBad sb = sb1;

这里 sb 是新创建的对象,被初始化为 sb1 的值,因此使用的复制构造函数。然而,实现时也可能是分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值运算符将临时对象复制到新对象中。当然,也可能是通过复制构造函数一步到位,具体看编译器的实现。总的来说,初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。
与复制构造函数相似的是,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身的类对象,则程序将使用改成员类定义的赋值运算符来复制该成员,但静态数据不受影响。

因此,在 StringBad 程序运行的最后释放对象时:

(-):"Hello, World" object deleted, 4 left.
(-):"Mo Xu" object deleted, 3 left.
(-):"Mo Xu" object deleted, 2 left.
(-):"You Ka" object deleted, 1 left.
(-):"Hello, World" object deleted, 0 left.

第一个"Hello, World"是 sb5 对象释放指针指向的地址,而最后一个"Hello, World"会再次释放该地址。

解决方案

解决这类问题的方案是显式声明一个赋值运算符函数进行深拷贝,其实现与复制构造函数相似,但也有一些差别:

  • 由于目标对象可能引用过之前分配的数据,因此在赋值运算符重载函数中应该使用 delete[] 来释放这些数据。
  • 函数应避免将对象给自身,不然的话,在给对象重新赋值之前释放内存的操作可能删除对象的指针成员指向的内容。
  • 函数返回一个指向调用对象的引用。

通过返回一个对象,函数可以想常规赋值操作那样,连续进行赋值:

S0 = S1 = S2;

对应的函数表示法如下:

S0.operator=(S1.operator=(S2));

下面的代码是为 StringBad 类编写的赋值运算符:

StringBad & StringBad::operator=(const StringBad &sb) {
    if (this == &sb) // 自己给自己赋值
        return *this;

    delete [] sb.str; // 释放之前指向的内存
    this->len = sb.len;
    this->str = new char[len + 1];
    strcpy(this->str, sb.str);
    cout << "(+)" << strNum << ":\"" << str << "\" object modify(operator =)\n";
    return *this;	
}

代码首先检查是否是自我赋值,这是通过判断两个对象的地址是否相同来完成的。如果地址相同,说明这两个对象是同一个对象,此时程序返回 *this,然后结束。
如果地址不同,函数将释放 str 指向的内存,这是因为稍后要将一个新的字符串的地址赋值给 str,因此 str 需要先将当前指向的内存释放掉,防止内存被浪费(内存泄漏)。

注:释放 str 指向的内存这里不需要先判断 str 是否为空,因为 delete 允许释放 NULL 指针,实际上是因为 delete 的实现中会进行 NULL 判断的。

接下来的操作与赋值构造函数相似,完成上述操作之后程序返回 *this 并结束。由于赋值运算符并不创建新的对象,因此不需要更新静态成员 strNum 的值。

将前面的复制构造函数和赋值运算符添加到 StringBad 类中之后,所有的问题都解决了。

改进之后的 String 类

有了更丰富的知识后,可以对 StringBad 类进行修订,将它重命名为 String。首先,添加前面介绍过的复制构造函数和赋值运算符,使类能够正确管理类对象使用的内存。其次,将会添加一些新功能,使 String 类更贴近 C++ 的 string 类的功能。 需要添加的函数如下:

int length() const;
friend bool operator<(const String & s1, const String & s2);
friend bool operator>(const String & s1, const String & s2);
friend bool operator==(const String & s1, const String & s2);
friend std::istream operator>>(std::istream & in, String & s);
char & operator[](int index);
const char & operator[](int index) const;
static int HowMany();

第一个新方法返回存储的字符串的长度,接下来的三个友元函数实现字符串的比较,>>运算符重载提供简单的输入功能;两个 operator 函数提供数组表示法访问字符串中的字符,之所以返回字符引用是为了能够通过数组表示法修改字符串指定位置的字符,静态方法 HowMany() 将补充静态类成员 strNum。
鉴于 String 类构造函数以及析构函数在 StringBad 类的解决方案中已经列出,这里就不在展示,以下主要讲解这几个新函数的实现。

比较成员函数

bool operator<(const String & s1, const String & s2) {
    if (strcmp(s1.str, s2.str) < 0)
        return true;
    return false;
}

bool operator>(const String & s1, const String & s2) {
    return s2 < s1;
}

bool operator==(const String & s1, const String & s2) {
    return strcmp(s1.str, s2.str) == 0;
}

将比较函数声明为友元函数的好处在于可以将 String 对象与常规的 C 字符串进行比较。例如,假设 str 是 String 类对象,则下面的代码:

if ("C++" < str)

将被转换成:

if(operator<("C++", str))

然后编译器会调用构造方法将代码转换为:

if(operator<(String("C++"), str))

如果不讲比较函数声明为友元函数,则只能接受 str < "C++" 这样顺序的调用。

数组表示法访问 String

char & String::operator[](int index) {
    return str[index];
}

const char & String::operator[](int index) const {
    return str[index];
}

首先,[] 运算符只能作为成员函数来重载;其次,返回 char & 类型可以给指定位置的字符赋值。例如,可以这样使用 String 对象:

String s("YouKa");
s[3] = 'M'; 

const char & String::operator[](int index) const函数则是用于 const String 对象读取字符。

静态类成员函数

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

int String::HowMany() {
    return strNum;
}

静态成员函数不能通过对象调用,甚至不能使用 this 指针。如果静态成员是 public 声明的,则可以使用类名和作用域解析运算符来调用它。例如,String 类的 HowMany 静态成员函数可以这样调用:

int count = String::HowMany();

由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。例如,静态方法 HowMany() 可以访问静态成员 strNum,但不能访问 str 和 len。
静态成员函数常用于设置类级标记,以控制某些类接口的行为。

进一步重载赋值运算符

假设现在要将常规字符串复制到 String 对象中,例如使用 getline() 读取了一个字符串,并且要将这个字符串放置到 String 对象中,此时 String 对象有以下函数:

String & operator=(String &);
String(char * p);
String(const String &);

因此,对于前面的需求,如果是首次输入字符串,可以编写以下代码:

// 首次输入
char temp[20];
cin.getline(temp, 20);
String s = String(temp);

继续输入字符串并保存在对象 s 中:

// 首次输入
char temp[20];
cin.getline(temp, 20);
String s = String(temp);
// 再次输入
cin.getline(temp, 20);
s = temp;

重载的赋值运算符只接受 String & 类型的参数,而 temp 是 char * 类型的变量,因此会先使用转换构造函数String(const String &);生成一个临时对象,然后将这个临时对象赋值给 s,之后程序调用析构函数 ~String() 删除临时对象。
为了提高处理的效率,最简单的方法就是新增一个重载赋值运算符,接受常规字符串作为参数,这样就不必创建和删除临时对象了。

String & String::operator=(const char * p) {
    delete [] this->str;

    this->len = strlen(p);
    this->str = new char[len + 1];
    strcpy(str, p);
    std::cout << "(+)" << strNum << ":\"" << str << "\" object modify(operator =)\n";
    return *this;
}

重载 >> 运算符

通过重载 >> 运算符提供一种将键盘输入读入到 String 对象中的方法,如果由于某种原因导致输入失败,istream 对象的值将被置为 false。

std::istream & operator>>(std::istream & is, String & s) {
    char temp[40];
    std::cin.get(temp, 40);
    
    // 赋值
    if (is) s = temp;
    // 清除无效字符
    while(is && is.get() != '\n') continue;

    return is;
}

使用 String 的程序

using std::cout;
using std::endl;
const int SIZE = 10;
const int CHAR_SIZE = 40;
String arr[SIZE];

for (int i = 0; i < SIZE; i++) {
    cout << "Please input " << SIZE - i << " words.\n";
    std::cin >> arr[i];
}

for (int j = 0; j < SIZE; j++) {
    cout << arr[j] << endl;
}

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

  • 如果在构造函数中使用 new 来初始化指针成员,则应在析构函数中使用 delete;如果构造函数中使用的是new[],析构函数中应使用 delete[]。
  • 如果有多个构造函数都是用 new,那么他们必须以相同的方式使用,要么都带中括号,要么都不带中括号。因为只有一个析构函数,所有的构造函数都必须与它兼容。不过可以在一个构造函数中使用 new,另一个构造函数中将其初始化为 nullptr,这是因为 delete 可用于空指针。
  • 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。通常,这种构造函数与下面类似:
String(const String & s) {
    strNum++;
    len = s.len;
    str = new char[len + 1];
    strcpy(str, s.str);
}
  • 应该定义一个赋值运算符,通过深拷贝讲一个对象复制给另一个对象。通常,该类方法与下面的类似:
String & operator=(const String & s) {
    if(this == &s)
        return *this;
    delete str;
    len = strlen(s.str);
    str = new char[len + 1];
    str = strcpy(str, s.str);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值