c++是C语言的扩展。因为c++寻求向后兼容C代码,所以大多数用原始C语言编写的代码仍然可以正常工作。例如,C编程中广泛使用的约定是文本字符串通过以空结尾的字符数组表示。文本字符串存储在char数组中,特殊的空字符‘\0’放在数组的末尾,作为结束标记。那些存储以空结尾文本的字符数组几乎都是通过char*指针访问的。
为了支持这种约定,C标准库提供了许多标准函数来使用C风格的文本字符串执行常见任务。例如,函数
Int strlen(char* str)
计算并返回存储在str所指向的数组中的字符串的长度。
为了支持在c++中使用C风格的文本习惯用法,c++标准库通过头文件<cstring>提供了赢博体育遗留的文本操作函数。
下面是一个示例程序,它使用c风格的字符串来解决一个涉及字符串的简单问题。文件words.txt和test.txt包含两个单词列表。在每个列表中,每行存储一个单词。我们想要构建一个程序来查找test.txt中没有出现在words.txt中的赢博体育单词。
下面是一个简单程序的代码。
#include <iostream> #include <vector> #include <cstring> int main(int argc, const char* argv[]) {std::vector<char*> strs;std::向量< char * > testStrs;std::向量< char * > diffStrs;std:: ifstream;字符缓冲区[64];//读取单词列表in.open("words.txt");while(in.getline(buffer,63)) {char* newStr = new char[std::strlen(buffer)+1];std::拷贝字符串(newStr、缓冲);strs.push_back (newStr);} in.close ();//读取test. open("test.txt")中的测试单词;while(in.getline(buffer,63)) {char* newStr = new char[std::strlen(buffer)+1];std::拷贝字符串(newStr、缓冲);testStrs.push_back (newStr);} in.close ();//查找没有出现在单词列表中的赢博体育测试单词for(auto otr = testStrs.begin();otr != testStrs.end();otr++) {char* testStr = *otr;Bool found = false;For (auto itr = strs.begin();!)= strs.end();itr++) if(std::strcmp(*itr,testStr) == 0) found = true;如果发现(!)diffStrs.push_back (testStr);} //打印没有出现在单词列表中的测试单词,for(auto itr = diffStrs.begin();itr != diffStrs.end();itr++) std::cout << *itr << std::endl;//释放字符数组使用的内存。For (auto itr = strs.begin();itr != strs.end();itr++) delete[] *itr;for(auto itr = testStrs.begin();itr != testStrs.end();itr++) delete[] *itr;返回0;}
该程序混合使用了C风格的字符串和简单的c++思想来完成工作。
这里我们需要做的第一件事是从输入文本文件中读取单词。像c++中一样,设置ifstream对象来从原始文本文件中读取。ifstream类提供了一个方法getline(),它可以从文本文件中读取一行文本,并将该文本存储在c风格的字符数组中。Getline()接受两个参数——第一个参数是指向将存储字符的字符数组的指针,第二个参数是该数组可存储的最大字符数。该程序使用getline()通过如下安排来读取单词:
字符缓冲区[64];in.getline(缓冲区,63);
当程序从两个文件中读取单词时,它希望将这些单词存储在一对向量中。这些向量被声明为vector<char*>类型,实际上将存储指向每个单词数组的指针。下面是从ifstream中读取单词并将其存储在vector中的代码部分:
in.open(“words.txt”);while(in.getline(buffer,63)) {char* newStr = new char[std::strlen(buffer)+1];std::拷贝字符串(newStr、缓冲);strs.push_back (newStr);} in.close ();
这段代码使用getline()读取一个单词并将其存储在缓冲区数组中。然后,使用strlen()确定单词的长度,生成第二个数组来永久存储该单词,并使用strcpy()函数将该单词从缓冲区数组复制到新数组中。最后,它使用vector类的push_back()方法将指向副本的指针存储在vector中。
下面是代码的一部分,它查找第二个向量中没有出现在第一个向量中的单词。
//查找没有出现在单词列表中的赢博体育测试单词for(auto otr = testStrs.begin();otr != testStrs.end();otr++) {char* testStr = *otr;Bool found = false;For (auto itr = strs.begin();!)= strs.end();itr++) if(std::strcmp(*itr,testStr) == 0) found = true;如果发现(!)diffStrs.push_back (testStr);}
代码使用了一个相当明显的双循环结构。外部循环遍历测试文件中的赢博体育单词。对于每个单词,内循环扫描从words.txt读取的单词列表,寻找匹配项。如果没有找到匹配项,则将当前测试词推入第三个向量diffStrs。在程序的最后,我们将diffStrs中的赢博体育字符串输出到cout。
逻辑的关键部分是将我们的测试字符串与来自words.txt的字符串进行比较所需的测试。这个if语句实现了这个测试:
if(std::strcmp(*itr,testStr) == 0) found = true;
该测试使用了c风格的字符串比较函数strcmp()。该函数接受两个指向字符串数组的指针作为参数,如果存储在两个字符串中的文本相同,则返回0。
由于c++是一种面向对象的语言,它自然提供了一个字符串类。c++字符串类可以用作旧的C风格字符串代码的替代品。用string类替换c风格字符数组的好处之一是,string类允许我们编写更自然的代码,并且使我们不必了解strlen、strcpy和strcmp等函数的细节。
下面是使用string类重写的示例程序:
#include <iostream> #include <vector> #include <string> int main(int argc, const char * argv[]) {std::vector<std::string> strs;std::向量< std:: string > testStrs;std::向量< std:: string > diffStrs;std:: ifstream;字符缓冲区[64];//读取单词列表in.open("words.txt");while(in.getline(buffer,63)) {std::string newStr(buffer);strs.push_back (newStr);} in.close ();//读取test. open("test.txt")中的测试单词;while(in.getline(buffer,63)) {std::string newStr(buffer);testStrs.push_back (newStr);} in.close ();//查找没有出现在单词列表中的赢博体育测试单词for(auto otr = testStrs.begin();otr != testStrs.end();otr++) {std::string testStr = *otr;Bool found = false;For (auto itr = strs.begin();!)= strs.end();itr++) if(testStr == *itr) found = true;如果发现(!)diffStrs.push_back (testStr);} //打印没有出现在单词列表中的测试单词,for(auto itr = diffStrs.begin();itr != diffStrs.end();itr++) std::cout << *itr << std::endl;返回0;}
string类提供了三个直接的好处。首先,string类的构造函数接受一个指向c风格字符串的指针作为参数,它会自动为我们创建一个c风格字符串的副本。这简化了从文件中读取字符串的代码:
in.open(“words.txt”);while(in.getline(buffer,63)) {std::string newStr(buffer);strs.push_back (newStr);} in.close ();
第二个好处是string类定义了一个比较操作符==的版本,允许我们比较两个字符串。这使得字符串搜索的代码更简单:
For (auto itr = strs.begin();!)= strs.end();itr++) if(testStr == *itr) found = true;
第三个好处是string类管理自己的存储。当包含字符串的vector在main函数结束时消失时,vector将自动销毁它们包含的赢博体育字符串,而字符串将释放用于存储其文本的内存。
c++ string类使得编写更简洁的代码成为可能。然而,像c++标准库中的赢博体育类一样,c++字符串类有点像黑盒。为了开始深入了解std::string之类的类的实际工作原理,我们将编写自己的字符串类。
c++中的类和Java中的类之间的一个明显区别是,Java中的类定义通常出现在单个文件中。最常见的是,Java类定义将出现在名称与类名称匹配的源代码文件中。c++通常将一个类分成两部分。第一部分是类声明。类声明通常出现在头文件中,这是一个文件扩展名为.h的源代码文件。头文件可以与类具有相同的名称,但不要求具有相同的名称。在某些情况下,您会在单个头文件中看到多个类声明。
类声明由成员变量列表和类中方法的原型组成。
下面是我们的string类的类声明。这个类声明出现在一个名为String.h的头文件中:
#include <iostream> class String {friend bool operator==(const String&,const String&);operator<<(std::ostream&,const String&);公众:String ();字符串(const char* otherStr);String(const String& other);String(String&& other) noexcept;~字符串();String& operator=(const String& other);String& operator=(String&& other);Int length() const;静态无效initString();静态int memoryUsage();maxMemoryUsage();静态int copyCount();静态int moveCount();private: static int memoryUsed;静态int maxMemoryUsed;静态int拷贝;static int movesMade;char* allocMemory(int size);int len;char * str;};bool operator==(const String& 1,const String& 2);std::ostream& operator<<(std::ostream& out,const String& str);
这里有一些需要注意的事情:
类字符串
.公共
或私人
。Java通过附加公共
和私人
每个方法或成员变量的限定符。c++将类划分为公共和私有部分。~
),后面跟着类名。析构函数是一个方法,在删除String对象或String变量超出作用域时自动调用。正如我们将在下面看到的,析构函数通常包括在对象消失之前释放对象使用的任何内存或其他资源的代码。常量
。一个常量
声明是这样一种声明:所讨论的方法不会做任何事情来改变调用该方法的对象。您可能还注意到,其中一些方法接受已声明的参数常量
。你只能打电话常量
这些方法常量
参数。打电话给非-常量
已声明的参数或变量上的方法常量
导致编译器错误。如上所述,c++类声明通常只列出类中的方法,而不提供这些方法的任何代码。这些方法的实现通常写在一个.cpp文件中,该文件的名称与头文件的名称相匹配。
在本例中,我创建了一个包含方法代码的文件String.cpp。以下是该文件的内容:
#include "String.h" int String::memoryUsed;int字符串:maxMemoryUsed;int字符串:copiesMade;int字符串:movesMade;String::String(): len(0),str(nullptr) {} String::String(const char* otherStr) {len = 0;while(otherStr[len] != '\0') ++len;str = allocMemory(len);for(int n = 0;n < len;n++) str[n] = otherStr[n];} String::String(const String& other) {copyesmade++;Len = other.len;str = allocMemory(len);(int n = 0; n < len; n + +) str [n] = other.str [n];} String::String(String&& other) noexcept {movesMade++;Len = other.len;其他。Len = 0;STR = other.str;其他。STR = nullptr;} String::~String() {if(str != nullptr) {memoryUsed -= len;删除[]str;Len = 0;STR = nullptr;}} String& String::operator=(const String& other) {if(str != nullptr) {memoryUsed -= len;删除[]str;} copiesMade + +;Len = other.len;str = allocMemory(len);(int n = 0; n < len; n + +) str [n] = other.str [n];返回*;} String& String::operator=(String&& other) {if(str != nullptr) {memoryUsed -= len;删除[]str;} movesMade + +;Len = other.len;其他。Len = 0;STR = other.str;其他。STR = nullptr;返回*;} int String::length() const{返回len;}无效字符串::initString() {memoryUsed = 0;maxMemoryUsed = 0;copyesmade = 0;movesMade = 0;} int String::memoryUsage(){返回memoryUsed;} int String::maxMemoryUsage(){返回maxMemoryUsed;} int String::copyCount(){返回copyesmade;} int String::moveCount(){返回movesMade;} char* String::allocMemory(int size) {memoryUsed += size;if(memoryUsed >) maxMemoryUsed = memoryUsed;返回新char[size];} bool操作符==(const String& one,const String& two) {if(one.length() != two.length())返回false;Int Max = one.length();For (int n = 0;n < max;n++)Str [n] != two.str[n])返回false;返回true;} std::ostream& operator<<(std::ostream& out,const String& str) {for(int n = 0;n < str.len;n++) out << str.str[n];返回;}
这里有一些关于这段代码的注意事项。
::
,以及函数的名称。memoryUsed
,maxMemoryUsed
,copiesMade
,movesMade
。每次String类必须分配一个新的内存块来保存一些字符时,它都会将该数组的大小添加到memoryUsed
变量。任何时候析构函数释放一个字符数组时,都将从对象中减去该数组的大小memoryUsed
变量。==
和<<
操作符。下面我将更详细地讨论这两个函数。使用这个类的最后一步是在程序中使用它。
下面是一个程序的源代码,该程序使用String类来解决我们在该程序的早期版本中解决的问题。
#include <iostream> #include <fstream> #include <vector> #include "String.h" int main(int argc, const char * argv[]) {std::vector<String> strs;std::向量<字符串> testStrs;std::向量<字符串> diffStrs;std:: ifstream;字符缓冲区[64];字符串:initString ();//读取单词列表in.open("words.txt");while(in.getline(buffer,64)) {String newStr(buffer);strs.push_back (newStr);} in.close ();//读取test. open("test.txt")中的测试单词;while(in.getline(buffer,64)) {String newStr(buffer);testStrs.push_back (newStr);} in.close ();//查找没有出现在单词列表中的赢博体育测试单词for(auto otr = testStrs.begin();otr != testStrs.end();otr++) {String testStr(*otr);Bool found = false;For (auto itr = strs.begin();!)= strs.end();itr++) if(*itr == testStr) found = true;如果发现(!)diffStrs.push_back (testStr);} //打印没有出现在单词列表中的测试单词,for(auto itr = diffStrs.begin();itr != diffStrs.end();itr++) std::cout << *itr << std::endl;//显示内存使用统计std::cout << "Using memory: " << String::memoryUsage() << std::endl;std::cout << “最大内存:” << String::maxMemoryUsage() << std::endl;std::cout << “拷贝数:” << String::copyCount() << std::endl;返回0;}
我在这里添加的另一个特性利用了我可以完全控制String类的设计这一事实。由于String类被设计为在运行时自动跟踪其内存使用情况,因此程序结束时输出一些关于内存使用情况的信息。
Java和c++都有静态成员变量和静态成员函数的概念。在Java中,静态成员函数通过语法调用
<类名>。<功能>
c++与之相似,只是调用静态成员函数的语法不同
<类名>::<函数>
另一个小区别是静态成员变量的实现方式。在Java中,类定义将与类有关的赢博体育内容合并到一个文件中。另一方面,在c++中,类通常被分解为类声明和成员函数定义,类声明和成员函数定义分别放在头文件和单独的cpp文件中。
静态成员变量通常用于存储有关类及其行为的全局信息。在本例中,String类中的静态成员变量用于跟踪有关String类的赢博体育实例使用了多少内存的信息。
当您在类声明中放置静态成员变量时,您只是声明存在这样一个变量。实际设置变量的语句稍后出现,通常在相关的cpp文件中。如果你看一下String.cpp文件的顶部,你会看到这些语句:
int字符串:memoryUsed;int字符串:maxMemoryUsed;int字符串:copiesMade;int字符串::movesMade
这些语句设置了在类声明中声明的四个静态成员变量。如果您忘记将这些语句放入cpp文件中,您的程序将无法编译,并出现一个链接器错误,提示没有定义这些变量。
与往常一样,静态成员函数只允许访问静态成员变量。
Java和c++类都支持构造函数的概念。构造函数是一种专门的成员函数,其目的是根据一些输入数据正确初始化对象。上面的示例String类具有四个不同的构造函数。本节的注释将依次引导您了解这些构造函数。
第一个构造函数是默认构造函数。
String::String(): len(0),str(nullptr) {}
默认构造函数用于在没有可用数据来初始化对象的情况下初始化String对象。例如,如果我们声明一个变量
字符串myString;
编译器将调用默认构造函数来初始化myString。
另一种使用默认构造函数的情况是构造String对象的vector:
std::向量V <字符串> (100);
该声明设置了一个包含100个字符串的向量。编译器将调用默认构造函数来初始化vector中100个string中的每一个。
这个构造函数使用特殊的构造函数初始化语法。出现在冒号之后的初始化表达式len(0)和str(nullptr)指定了如何初始化len和str这两个成员变量。len初始化为0,str初始化为值nullptr,这表示指针当前没有可指向的对象。(nullptr在c++ 11中被引入c++。)由于初始化表达式完成了构造函数需要完成的赢博体育工作,因此构造函数体为空。
第二个构造函数用于从c风格字符数组初始化String对象:
字符串::String(const char* otherStr) {len = 0;while(otherStr[len] != '\0') ++len;str = allocMemory(len);for(int n = 0;n < len;n++) str[n] = otherStr[n];}
正如我们在上面的第一个示例程序中看到的,处理c风格数组时最明智的做法是立即复制该数组。这段代码正是这样做的。作为复制的副作用,代码还确定了原始c风格字符串的长度,并将该长度信息存储在成员变量len中。
为了分配存储副本所需的内存,此构造函数调用私有成员函数allocMemory。该函数创建所需大小的字符数组,并返回指向该数组的指针。此外,它记录用于在类的静态成员变量中创建数组的内存,以便我们可以跟踪该类的内存使用情况。
char* String::allocMemory(int size) {memoryUsed += size;if(memoryUsed >) maxMemoryUsed = memoryUsed;返回新char[size];}
第三个构造函数是复制构造函数。
String::String(const String& other) {copyesmade++;Len = other.len;str = allocMemory(len);(int n = 0; n < len; n + +) str [n] = other.str [n];}
在明确需要复制其他对象的情况下调用复制构造函数。例如,代码
字符串(“Hello”);字符串2 = 1;
将调用复制构造函数将字符串1复制到字符串2。
触发复制构造函数的另一种情况是将String作为参数传递给函数。
std::向量<字符串>矢量;字符串s(“Hello”);vec.push_back(年代);
在本例中,我们想要将刚创建的String对象压入vector对象进行存储。将该String传递给vector的push_back方法将触发通过复制构造函数复制该String。
第四个构造函数是移动构造函数。
String::String(String&& other) noexcept {movesMade++;Len = other.len;其他。Len = 0;STR = other.str;其他。STR = nullptr;}
从这个构造函数的代码中可以看到,这个构造函数的目的是将数据从String other移到正在构造的新String中。将数据移出other后,重置其成员变量,以表明other现在为空。
为了区分移动构造函数和复制构造函数,移动构造函数使用不同的参数类型,即通用引用。形参类型中String后面的一对&符号表明other是对String的通用引用。
Move构造函数是在c++ 11中引入的,被设计为在对象从一个位置移动到另一个位置的情况下调用。涉及到大量移动操作的一种非常常见的情况是vector类的push_back方法。vector类内部将其数据存储在数组中。当该数组在push_back操作中耗尽空间时,vector类将创建一个新的更大的内部数组,然后尝试将其赢博体育数据从旧数组移动到新数组中。如果内部数组包含对象,vector类将使用对象的move构造函数实现从一个数组到另一个数组的移动。vector类进一步要求它使用的任何move构造函数都不能抛出异常:如果move构造函数不能提供这种保证,vector类将恢复使用复制构造函数将对象从旧数组移动到新数组。在将赢博体育对象从旧数组复制到新数组之后,向量将销毁赢博体育原始对象。为了通知vector类move构造函数不会抛出异常,我们向构造函数添加noexcept规范。
任何在构造函数中分配资源的类都需要提供析构函数。析构函数的目的是释放构造函数所声明的任何资源。下面是String类的析构函数
String::~String() {if(str != nullptr) {memoryUsed -= len;删除[]str;Len = 0;STR = nullptr;}}
String类的大多数构造函数都分配内存来存储String中的字符。析构函数将检查str指针是否指向某个对象。如果是,析构函数调用该指针上的delete[]操作,释放该数组使用的内存。
析构函数在对象停止存在时被触发。例如,如果在某个作用域中声明一个初始化String对象
字符串的例子(“Hello”);
当我们退出该作用域时,该String对象将自动销毁。
我们还可以这样显式地触发析构函数:
String *example = new String("Hello");//做一些事情的例子…删除示例;/ / . .那就让它消失。
对delete操作的显式调用调用该对象的析构函数,然后释放与String对象本身关联的内存。
另一种触发析构函数调用的情况发生在容器被销毁时。在上面的示例程序中,我在main中声明并使用了一些vector
要使String类有用,还需要做一件事,那就是定义一组操作符重载。比较单词的程序包括两个语句,它们试图将标准c++操作符赢博体育于string。第一个语句尝试使用比较操作符==来比较两个string对象:
//查找没有出现在单词列表中的赢博体育测试单词for(auto otr = testStrs.begin();otr != testStrs.end();otr++) {String testStr(*otr);Bool found = false;For (auto itr = strs.begin();!)find && itr != strs.end();itr++) if(*itr == testStr) //注意这里使用== found = true;如果发现(!)diffStrs.push_back (testStr);}
第二个语句尝试使用流插入操作符<
//打印没有出现在单词列表中的测试单词for(auto itr = diffStrs.begin();itr != diffStrs.end();itr++) std::cout << *itr << std::endl;
如果不定义与string一起使用时这些操作符的含义,这两个语句都将编译失败。我们通过构造一对称为操作符重载的特殊函数来定义string的这两个操作符:
//定义重载for == for string bool操作符==(const String& one,const String& two) {if(one.length() != two.length())返回false;Int Max = one.length();For (int n = 0;n < max;n++)Str [n] != two.str[n])返回false;返回true;} //定义一个重载方法<< for string std::ostream& operator<<(std::ostream& out,const String& str) {for(int n = 0;n < str.len;n++) out << str.str[n];返回;}
当编译器遇到涉及这些操作符之一的语句时,例如
if(*itr == testStr)
它将尝试将该操作符表达式转换为等效的函数表达式:
如果(操作符= = (* itr, testStr))
然后,它将查找一个名为operator==的函数,该函数接受两个String对象作为其参数。这正是我们上面定义的函数。
关于这些操作符重载的最后一个注释。这些函数被设置为在String类外部声明和定义的自由函数。同时,这两个函数都需要访问它们所使用的String对象中的私有数据。为了使它们能够做到这一点,String类将这两个操作符函数声明为String类的友元,这使它们具有查看存储在任何String对象中的私有数据的显式权限。
类字符串{朋友bool操作符==(const String&,const String&);operator<<(std::ostream&,const String&);public: //任何人都可以看到这些东西private: //只有成员函数和友元可以看到这些东西};
最后一对操作符重载提供了赋值操作符=的不同版本。无论何时编写这样的代码,都会调用此操作符:
字符串1 = 2;
当编译器遇到涉及对象的赋值操作时,它要做的第一件事是将赋值操作转换成等价的语法,该语法涉及调用名为operator=的成员函数:
字符串;one.operator =(两个);
然后,编译器将在类中查找具有该名称的成员函数。下面是String类的操作符=。
String& String::operator=(const String& other) {if(str != nullptr) {memoryUsed -= len;删除[]str;} copiesMade + +;Len = other.len;str = allocMemory(len);(int n = 0; n < len; n + +) str [n] = other.str [n];返回*;}
毫不奇怪,这看起来很像析构函数后面跟着复制构造函数的代码。这是很自然的,因为将一个字符串赋值给另一个字符串的请求要求我们首先删除我们所赋值的字符串中保存的赢博体育数据,然后将数据从另一个字符串复制到这个字符串中。最后,需要operator=返回对被赋值对象的引用。这使得链式赋值成为可能。例如,语句
字符串1 = 2 = 3;
在c++中是合法的。编译器会把这个语句翻译成
字符串;one.operator = (two.operator =(三));
为了使其正常工作,右边将3复制为2的赋值必须返回对2的引用,以便随后将其赋值给1。
就像有复制构造函数和移动构造函数一样,我们也必须同时有复制赋值操作符和移动赋值操作符。下面是move赋值操作符的代码。
String& String::operator=(String&& other) {if(str != nullptr) {memoryUsed -= len;删除[]str;} movesMade + +;Len = other.len;其他。Len = 0;STR = other.str;其他。STR = nullptr;返回*;}
构造一个类是一个非常具有挑战性的过程。当我们构造类似String类的东西时,我们需要构造这样一个类,以确保使用该类的任何程序都是正确和有效的。我上面构造的String类是正确的,但它的效率比可能的要低。这种低效率是由于String类倾向于对它所包含的文本进行大量复制造成的。你可以直接追溯到String类的复制构造函数:
String::String(const String& other) {copyesmade++;Len = other.len;str = allocMemory(len);(int n = 0; n < len; n + +) str [n] = other.str [n];}
每次调用复制构造函数时,它都会复制String对象中的字符数组。现在考虑这个复制构造函数,它看起来更像移动构造函数。
String::String(const String& other) {copyesmade++;Len = other.len;STR = other;//复制指针,但不复制数组}
如果您从上面的程序,并做了这个小的改变,程序将会做更少的不必要的复制。然而,这种变化带来了一个新问题。
由于复制构造函数现在将导致多个String对象共享相同的字符数组,因此析构函数现在将出现问题。当前析构函数
String::~String() {if(str != nullptr) {memoryUsed -= len;删除[]str;Len = 0;STR = nullptr;}}
将尝试删除STR指向的数组。这将在多个String对象共享相同str值的情况下造成麻烦。当一组对象中的第一个对象执行析构函数时,其他对象将留下不再有效的str值。当该组中的第二个String试图执行其析构函数时,它将尝试删除已经删除的str数组,这将导致异常。
我接下来要提出的是对原始String类的修改,这将使它有可能获得新的复制构造函数提供的一些效率增益,同时使构造一个正确的析构函数版本成为可能。
修改的第一步是更改String类的结构。删除String中的len和str成员变量,并用
StringData *数据;
其中StringData是一个新类:
类StringData{公共:char* str;int len;int refCount;StringData(const char* other);};
在这里,我们将str和len成员变量移动到StringData对象中,并添加了第三个成员变量,称为引用计数器。引用计数器的目的是跟踪当前有多少个String对象共享这个StringData。
通过这样的修改,我们重写了String类的复制构造函数,看起来像这样:
String::String(const String& other) {data = other.data;数据- > refCount + +;}
下面的图片说明了这是如何工作的。
下面的第一张图说明了我们执行语句后的情况
字符串1 ("blah, blah");
String对象现在指向StringData对象,该对象包含一个指针,该指针指向保存文本副本的字符数组。StringData对象的引用计数值最初设置为1,因为只有String对象1引用该文本。
接下来,我们执行一条语句
字符串两个(一个);
这就触发了一个拷贝。String类的复制构造函数没有复制文本,而是简单地使String对象two指向同一个StringData对象,同时将StringData对象中的引用计数从1增加到2。
最后,退出定义了one的作用域,这将触发调用String析构函数。析构函数看到一个人指向的StringData对象当前有多个对它的引用,因此它只是将引用计数从2减少到1。
当two的析构函数最终运行时,它将看到two指向一个引用计数为1的StringData对象。此时析构函数将删除包含字符的[]数组,然后也删除两个字符所指向的StringData对象。
重写String类的代码以使用这种新的排列方式。使用String类来解决单词问题的测试程序应该在更改后继续正常运行,并且应该报告使用的内存比原始版本少得多。(最优使用内存的程序应该报告使用了28403字节的内存来存储字符串。)
为了方便起见,这里有一个包含String程序原始版本的源代码和数据文件的归档文件。