为何长尾词在中,能引发思考的涟漪?

2026-04-12 05:251阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计8126个文字,预计阅读时间需要33分钟。

为何长尾词在中,能引发思考的涟漪?

`string` 类为什么需要被做成模板?首先,要了解为什么 `string` 类,它被设计成模板类。

`string` 类作为存储字符序列的容器,它本身需要灵活地处理不同长度的字符串,并且可能需要存储不同编码的字符。以下是一些原因:

1. 类型安全性:模板类允许 `string` 类在不同的数据类型(如 `char`, `wchar_t`, `char16_t`, `char32_t`)之间自动进行选择,保证使用时的类型安全性。

2.多语言支持:不同编码的字符需要不同的存储方式。模板 `string` 类可以根据不同的编码方式存储字符串,例如 `std::string` 在 C++11 中引入了对 UTF-8 的支持。

3.扩展性:模板 `string` 类可以方便地扩展,支持新的字符类型和编码方式,而无需修改原始类的实现。

以下是 `string` 类的示例:

string 我是我们

在这个例子中,`string` 类可以存储包括中文字符在内的各种字符,这正是模板 `string` 类的优势所在。

string类为何要被做成模板

首先要了解一下为什么string类,要被做成模板如下图:

string我们知道是用来储存字符串的,对于英文使用asc2码即可以代表所有的英文符号,但是string这个类为何要被做成模板呢?

这就要涉及到编码的问题了,首先我们知道对于一个整型(浮点型)而言,计算机是通过记录它的补码来记录值的,那么对于一个文字呢?对于一个文字计算机又是怎么储存它的呢?这里就要提及到第一个编码,即ASC编码,这个编码的全程为American Standard Code for Information Interchange,即美国用来表示自己文字的编码。


那么这个编码我们知道是通过映射的方式表示英文字母和符号的。因为计算机的内存肯定是无法储存一个字母A的,内存中储存的只有0和1,由0和1组成了不同的值,在让不同的值映射为不同的符号,例如上面的字符0映射的数字也就是64。这种值和符号一一映射的表也就叫做编码表。

而用来表示美国文字的也就是ASC2码表而。


使用这些ASC2码也就能够将英语的一句话保存进计算机之中了。如下图

以h为例子,0x68转化为10进制为104对应的正是英文字母h。

当然这只是对于英语的编码表。而对于中文而言我们的语言不像英语,如果还是使用一个字节表示汉字的话,一个字节也就8个比特位,而2^8也才256(一个比特位只有两个选择),对于中文而言是远远不够的。如果使用两个字节(表达6w多个子)差不多就可以了。但是除此之外对于每一个不同的国家都要使用不同的编码表这就会显得很麻烦,由此便提出了Unicode编码。

Unicode编码又往下分为好几种。能够解决如我国一样文字多的问题,或是文字很少国家的问题。

为了解决我国汉字的问题,就往下提出了三种编码方式称为UTF系列。

其中UTF-8的特点就是兼容ASC2码,然后对于常见的汉字使用两个字节编写,较不常见的使用三个字节编码,对于生僻字则使用4个字节编写。如下图:

使用代码验证:

每一个常见的汉字都是2个字节一共4个汉字所以大小为8个字节。

但是在windows上一般使用的是GBK系列而不是UTF8。当然除了UTF-8编码之外还有UTF-16,和UTF-32编码,其中UTF-32对于所有的汉字都是使用32个比特位也就是4个字节来表示,这样就能使得所有的汉字表达都拥有了统一性,而UTF-16则在两者之中取中解决了一些问题。这里就不深说了。

而编译器为了更好的兼容UTF-8和UTF-16在c++11出来之前还设置了一个变量也就是wchar,一个wchar使用的是两个字节。

而c++在c++11提出之前也就提供了两种string的模板。

其中string管理的是char* ,而wstring管理的则是wchar*。而在c++11出来之后则又提出了char16_t(2个字节),和char32_t(四个字节)来更好的兼容UTF-16和UTF-32.

而这也正是string被做成模板的原因,你传入不同的参数则实例化成不同的string。来适应不同德编码,由此来表示不同国家的文字。windows使用的GBK编码则是由我国自己搞的编码表。

了解完这些,下面我们就来模拟实现string。这里模拟实现的是UTF-8类型的。并不是实现所有的UTF系列。

模拟string

成员变量

首先肯定要确定类中的成员有哪些:

如下图:

便是成员变量

使用一个命名空间就能够和外部的string(vs内部现成的库)隔离开来。

无参默认构造函数

string(size_t size = 0,size_t capacity = 0,char* str = nullptr)//这是一种错误的写法 :_size(size), _capacity(capacity), _str(str) {}//我这里实现的默认构造函数是一个全缺省的函数并且没有开任何的空间 //运用了初始化列表来初始化但是对于一个无参的构造函数而言这是一个错误的写法

导致错误的原因则是会去访问空指针而导致程序崩溃。那么在什么时候去访问了空指针呢?在下面的reserve函数我会解释,我先把无参构造函数的正确写法写一下

string() :_size(0) ,_capacity(0) { _str = new char[1]; _str[0] = '\0'; } //我这里实现的是一个无参的默认构造函数,开辟了一个空间用以储存\0 //当然这里的容量和长度仍旧为0,因为那一个空间并不是给有效数据开辟的而是给\0开辟的

那么按照要有空间才能储存值得规则,下面我们先来实现reserve函数。

reserve函数

这个函数得功能很简单也就是开辟大小为n的空间。函数实现

void reserve(size_t n = 0) { if (n > _capacity)//当我们需要的空间大小大于此时的容量时就需要扩容,否则不做任何处理 { char* tmp = new char[n+1];//创建n+1个大小的空间,最后一个空间用于储存\0 _capacity = n; strcpy(tmp, _str); delete[] _str;//手动释放原先的空间 _str = tmp;//将新空间的地址传过去 } }

在这里就可以知道如果我们在写无参默认构造函数的时候,传给_str的一个空指针,那么在strncpy的时候就会解引用空指针导致野指针错误。

push_back函数

这是库实现的声明。

我们仿造这个声明来实现这个函数

void push_back(char c) { if (_size == _capacity)//容量满了或者刚开始根本没有创建空间 { reserve(_capacity == 0 ? 4 : 2 * _capacity);//这里如果_capacity为0则给与默认的4个字节的大小的空间 //否则就按照原来容量的2倍来增加空间 } _str[_size] = c; _size++; }

size()和capacity()函数

这两个函数的功能很简单,能让外面的人读取到这个对象的长度和容量。

int size() const { return _size; }//返回当前的有效长度 int capacity() const { return _capacity; }//返回当前的容量

这两个函数都要使用const修饰,如果不加那么一个const string的对象,就会无法访问这两个函数

,因为权限只能缩小和平移而不能放大权限,对于const权限的对象而言,这两个是非const函数,如果调用了等于权限放大了,自然不会让你访问,但是加上const之后,无论是const对象还是非const函数都可以访问这两个函数,非const对象访问const函数权限缩小,所以可以访问。

重载[]函数

我们现在要将[]给重载了让其string,能够和数组一样去访问数据。

我们先来看文档上的关于这个函数的声明

可以看到这个函数有两个声明。这两个函数一个是能够让用户对于数据做到既能读取数据,又能写数据。而const修饰的那个声明用户对于数据只有读的功能而没有写的功能。

下面是实现

char& operator[](size_t n) { assert(n < _size);//防止出现越界访问 return _str[n]; }//这个函数能够让用户既能读数据又能写数据 char operator[](size_t n) const { assert(n < _size); return _str[n]; }//这样写的函数用户只能读取数据,而不能修改string中的数据

下面我们来验证上面的这些函数是否有问题:

出一个测试用例

void printf_arr(const string& c)//为了防止有人在函数里面修改a所以使用const修饰 { for (int i = 0; i <c.size(); i++) { cout << c[i] << " "; } cout << endl; } void teststring1() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_back('d'); a.push_back('e'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl;//以上我们来测试[]和push_back的读和写的功能 for (int i = 0; i < a.size(); i++) { a[i] = 'f'; cout << a[i] << " "; } cout << endl;//这里测试[]的写入功能 printf_arr(a);//假设这里还存在一个函数功能为打印 }

从这个图里就可以看到直接报错误了

取出赋值之后的正确运行结果:

完善构造函数

上面我们已经写过了无参的构造函数,但是大部分时候,我们会将一个字符串直接初始化进对象中,所以我们也要支持这个构造函数。首先我们先来看官方的文档是如何声明的。

可以看到这之中有很多种,我就只实现其中的两种了

string(const char* s ="") :_size(strlen(s)), _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, s);//将s完全拷贝过来,并且不用手动赋值\0,因为strcpy会将\0也一起拷贝过来 }//当然如果这里你选择了全缺省的参数,那么之前所写的那个无参的构造就可以删除了 //这个函数完美兼容无参构造函数的功能。 //string(const string& c) // :_size(c._size) // ,_capacity(c._capacity) //{ // delete[] _str;//释放掉原空间元素 // //既然是拷贝首先需要确定原先的_str空间中不存在元素了 // for (int i = 0; i < c.size(); i++) // { // push_back(c[i]); // }//使用push_back直接将其赋值给*this // _str[_size] = '\0';//最后手动增加\0 //}//当然这是一种传统的写法 //下面还有一种写法但是这种写法需要首先完成swap函数 void swap(string& c)//记住这里传的是引用,如果不传引用,就会造成交换失败,不传引用那么形参的改变不会 //影响实参 { std::swap(_size, c._size); std::swap(_capacity, c._capacity); std::swap(_str, c._str); }//要交换两个对象的内容,无非也就是将成员变量交换而已 string(const string& c) { string tmp(c._str);//首先拷贝一个tmp对象 swap(tmp);//将*this和tmp交换,即可完成,这样当构造完成之后原本的空间也会交给编译器底层自己去释放 }

下面就来测试一下这些功能


void teststring2() { string a("abcdefg");//测试使用一个字符串初始化 for (int i = 0; i < a.size(); i++) { cout << "a:" << a[i] << " "; } cout << endl; string b(a);//使用a初始化 for (int i = 0; i < b.size(); i++) { cout << "b:" << b[i] << " "; } cout << endl; }

实现析构函数

~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; }

迭代器的实现

string既然是一种容器那么一定会存在迭代器,首先解释一下什么是迭代器。

迭代器(Iterator)是一种用于遍历容器(如数组、链表、字符串等)中元素的对象。它提供了一种统一的访问容器元素的方式,使得可以通过相同的方式遍历不同类型的容器。

首先让我们来看一下迭代器的使用

void testiterator() { string a("abcdefghijkl"); string::iterator it = a.begin(); while (it != a.end()) { cout << *it << " "; it++; } cout << endl;//这是string迭代器的使用 vector<int> c; c.push_back(1); c.push_back(2); c.push_back(3); c.push_back(4); c.push_back(5); vector<int>::iterator it1 = c.begin(); while (it1 != c.end()) { cout << *it1 << " "; it1++; } cout << endl;//这里是vector的迭代器可以看到和string迭代器的使用可以说是一摸一样 }

从中就可以看到了迭代器的使用了。

那么string的迭代器要如何实现呢?其实对于这些内存空间开辟是连续的容器(string,vector)而言,原生指针就可以充当迭代器的使用了。例如对于string而言char*就可以作为迭代器了,但是对于申请的空间不是连续的容器而言(list)迭代器的实现就不能使用原生指针了。

我们先来看string的迭代器如何实现。

typedef char* iterator;//将char*命名为iterator typedef const char* const_iterator;//适应于const对象的const迭代器 iterator begin() { return _str;//begin返回的自然是数组的首位元素的地址 } iterator end() { return _str + _size; }//同上 //下面是适用于const迭代器的函数 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }

在实现完成迭代器之后我们就可以使用范围for了,因为范围for底层也就是使用了迭代器。

下面是测试实例:

void teststring3() { string a("abcdefg"); string::iterator it = a.begin(); while (it != a.end()) { cout << *it << " "; it++; } cout << endl; cout << "使用范围for" << endl; for (auto e : a) { cout << e << " "; } cout << endl; }

当然在使用string要遍历的时候还是很少使用迭代器的,还是使用[]比较方便。

实现insert和erase

首先我们还是先来看较为官方的insert声明

在这里我会实现两个insert函数。


首先第一个在pos位置插入一个元素

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size - 1; while (end >= pos) { _str[end + 1] = _str[end]; end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

下面是测试后运行的结果,虽然现在是正确了,但是如果你将n设为0,那么程序就会崩溃。下面测试的图是在n为2,插入m。

那么当n为0的时候为什么会崩溃呢?

因为在循环条件里面,当end小于pos的时候循环才会停止,但是当pos为0的时候,只有end为-1,程序才会停止,但是pos为无符号整型,那么end在和pos比较的时候也会被转换为无符号整型,当end为0减一之后会变成无符号整型的最大值,而不是-1。

对于这种问题有两种解决方法。

解决方法一:

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size - 1; while (end >= (int)pos)//将size_t的pos强转为int { _str[end + 1] = _str[end]; end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

解决方法二:

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size;//这里就不能是_size-1了,如果还是_size-1那么最后一个元素就会被 //覆盖导致数据丢失 while (end > pos)//解决办法二也就是让end等于pos的时候就要退出循环 { _str[end] = _str[end - 1];//同时这里为了能够让pos位置的值也能移动所以这里是 //str[end] = str[end-1]; //如果这里依旧采用的是end+1 = end的话那么最后一个元素可以被移动但是pos位置的值又会 //无法移动导致元素缺失 end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

下面是当n为0的测试用例:

void teststring4() { string a("abcd"); a.insert(0, 'm'); for (auto e : a) { cout << e << " "; } }

当然除了这种移动的方法之外还可以使用库函数完成使用思路:使用strncpy将pos前面包括pos的数据拷贝到一个新空间,再将pos后面的数据拷贝到新空间,在这两个拷贝数据的中间留够插入的空间即可。

这里就不实现了。

下面实现将一段字符串插入到一个string对象中(pos位置)

string& insert(size_t pos, const char* s) { //首先第一步肯定还是判断pos和空间是否足够 assert(pos < _size); int len = strlen(s); reserve(_size + len); //依旧使用移动的思想 int end = _size; while(end > pos)//这里依旧会发生上面的那个问题所以这里依旧要解决 //那个问题 { _str[end+len-1] = _str[end - 1]; end--; } memcpy(_str + pos, s, len);//最后将中间的字符拷贝进去即可 _size += len; _str[_size] = '\0'; return *this; }

下面时测试实例:

void teststring4() { string a("abcd"); a.insert(0, "hijk"); for (auto e : a) { cout << e << " "; } }

有了这两个函数那么我们在实现头插和尾插的时候就可以复用了。

void push_back(char c) { insert(0,c); } void push_front(char c) { insert(_size, c); }

下面是测试实例:

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

下面实现erase函数。依旧实现来看官方的声明。

下面我们就来实现第一个删除从pos位置开始的len个元素的值。

string& erase(size_t pos = 0, size_t len = npos) { if (pos + len > _size)//如果大于了这些 { _str[pos] = '\0'; _size = pos; }//直接将从pos开始往后的所有元素删除 else { while (pos < _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; }//否则就将pos到pos+len之间的元素删除 return *this; }//这里使用了全缺省在不传参数的情况下这个函数会将整个string都删除

测试用例:

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; a.erase(0, 2); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

运行截图:

下一个是删除当前迭代器指向的那个值。

void erase(iterator pos) { int end = _size;//和删除单独pos位置上的值是一样的依旧使用的是移动 while (pos < _str+end) { *pos = *(pos + 1); pos++; } }

以及删除一段迭代器区间的值

void erase(iterator first, iterator last) //删除从first到last迭代器中间的值 { int len = last - first;//两个指针相减从而得到两者之间相距离的元素个数 while (first < _str + _size) { *first = *(first + len); first++; } _size -= len; }

有了erase之后头删和尾删的函数也就可以复用了

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; a.pop_back(); a.pop_front(); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

实现find函数

首先find函数的功能是从对象中寻找符合字符串或字符的元素,并返回下标。

即假设一个string对象中的值为abcde,需要你寻找c那么这个函数最后会返回c的下标。

如果你寻找的是bc字符串,最后会返回b的下标。

实现这个函数可以使用kmp算法,但是限于篇幅这里就不说了。

我们下面来看官方的声明

我们这里就是实现一个find函数

size_t find(const char* c)const//从对象中寻找字符c/字符串并且返回第一个c的下标 { char* tmp = strstr(_str, c);//直接使用strstr函数 //这个函数能够从_str中寻找c找到返回指向那个值的指针,找不到返回nullptr if (tmp) { return tmp - _str;//找到返回下标两个指针相减得到两个指针之间的元素个数 } else { return npos;//找不到直接返回npos } }

运行结果及测试用例

实现append函数

append函数的功能也即是追加字符串

string& append(const char* str) { int len = strlen(str);//先算出str的长度 reserve(_size + len);//判断空间 for (int i = 0; i < len; i++) { _str[_size] = str[i];//直接从_size处开始往后赋值 _size++; } _str[_size] = '\0';//手动增加\0 return *this; }

测试及运行截图

操作符重载

首先先重载一个+=

string& operator+=(char ch)//适用于增加一个字符 { push_back(ch); return *this; } string& operator+=(const char* str)//适用于增加一个字符串 { append(str);//直接复用即可 return *this; }

下面还有>,<,==和!=的重载,思路也都是复用了strcmp,因为我们写的这个只是针对于char*的所以使用这个,如果你要将string写为模板的话,这个是不合适的。

bool operator>(const string& b) const { return strcmp(_str, b._str)>0; } bool operator<(const string& b) const { return strcmp(_str, b._str) < 0; } bool operator==(const string& b) const { return strcmp(_str, b._str) == 0; } bool operator>=(const string& b) const { return !(*this < b); } bool operator<=(const string& b) const { return !(*this>b); }

这里我就只测试了一个

实现resize函数

首先我们要知道string中的resize函数用于改变字符串的大小。它接受一个参数,表示新的大小。如果新的大小大于当前字符串的大小,那么字符串会被扩展,多出的部分会被填充为指定的字符(默认为空格)。如果新的大小小于当前字符串的大小,那么字符串会被截断,只保留前面的部分。

下面来实现这个函数,

string& resize(size_t n, char c = '\0') { //第一步判断n是否大于_size //如果n小于了_size那么就要删除n以后所有的元素 if (n < _size) { _str[n] = '\0'; _size = n; } else//这里就是大于了_size需要我们增加元素 { reserve(n);//如果n大于了_capacity,自然也会扩容 while (_size < n) { _str[_size] = c; _size++; } _str[_size] = '\0';//最后手动增加\0 } return *this; }//我这里添加的是\0

流插入(<<)和流提取(>>)函数的实现

流插入(<<)

ostream& operator<<(ostream& out, const string& b)//这里并不需要将这个函数作为友元函数,可以复用 //类中的函数以达到读取数据的效果 { for (int i = 0; i < b.size(); i++) { out << b[i]; } return out;//让其能够输出读多个string对象 }//需要注意的是这个函数并没有写到对象class中而是写到了class外作为一个独立的函数, //而不是类的成员函数

测试及运行截图:

流提取(>>)

istream& operator>>(istream& in, string& b) { char ch; in >> ch;//先将这个值放到ch中去 while (ch != '\n' && ch != ' ') { b += ch;//在增加到b中 in >> ch; } return in; }//如果你使用这种写法那么最后会无法结束循环 //这是错误写法

为什么按照上面的写法会无法结束while循环呢?因为in这个对象不会接收空格和换行符(\n)也就导致了ch不可能等于\n或是等于空格。

那么为了解决这个问题就要使用in对象中的get函数,能够让cin接收换行和空格。

istream& operator>>(istream& in, string& b) { char ch; in.get(ch);//先将这个值放到ch中去,使用get让cin能够接收换行和空格 while (ch != '\n' && ch != ' ') { b += ch;//在增加到b中 in.get(ch); } return in; }

。但是这么写还有一个很不好的点也就是,如果我输入的字符串非常的长,那么就会造成多次的扩容,如果你预先开了空间那么又会造成可能开的空间过大,而输入过少浪费的情况。为了解决这个问题我们可以这么写。

按照下面这么写那么就能够解决多次扩容的问题,不用担心buff数组,在函数结束之后会自动地释放。

istream& operator>>(istream& in, string& b) { //第一步创建一个临时的静态数组 char buff[128]; char ch; int i = 0;//作为buffi的下标 ch = in.get();//将读取到的值放到ch中 while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { buff[i] = '\0';//手动为i增加一个\0 b += buff;//复用函数 i = 0;//将i改为0 } ch = in.get();//将读取到的值放到ch中 } //当循环结束的时候如果i不是0代表buff中储存有我们需要的值 if (i != 0) { buff[i] = '\0';//依旧手动增加一个\0 b += buff;//复用函数完成 } return in; }


实现substr函数

这个函数的功能为取出string对象中从pos位置开始的一个字符串。

string substr(size_t pos, size_t len = npos) { string s;//用于储存取出的字符串 //首先先判断npos是否大于_size如果大于的话就将从pos位置开始往后的所有内容都提取出来 if (pos + len >= _size) { for (int i = pos; i < _size; i++) { s += _str[i];//将从pos位置开始的所有元素放到s中 } } else//在这种情况下就不是取出从pos位置开始往后的所有数据 { for (int i = pos; i < pos + len; i++) { s += _str[i]; } } return s;//这里需要一个拷贝构造,不能传引用,因为离开这个函数之后s就被销毁了 }

当然现在还无法测试这个函数因为还差最后一个构造函数

完善=操作符

string& operator=(const string& s) { //第一步先清空原先的空间 delete[] _str; _capacity = s._capacity; reserve(_capacity);//开辟足够大的空间 for (int i = 0; i < s._size; i++) { _str[_size++] = s[i]; } return *this; }//重载一个等于操作符

当然上面依旧是传统的写法,我们也可以使用新式的写法。

为何长尾词在中,能引发思考的涟漪?

string& operator=(string s) { //首先在传参时就完成了拷贝 swap(s); return *this;//直接和s交换即可,和上面写的拷贝构造时一样的 }//这两种写法选一个即可 string& operator=(const string& s) { //如果不想使用上面的那种 string tmp(s._str);//利用const char* 初始化处一个string对象 swap(tmp);//将tmp和string交换 return *this; }

下面是测试和运行截图

代码合集

namespace LHY { class string//模拟实现string类 { public: /*string() :_size(0) ,_capacity(0) { _str = new char[1]; _str[0] = '\0'; }*/ typedef char* iterator;//将char*命名为iterator typedef const char* const_iterator;//适应于const对象的const迭代器 iterator begin() { return _str;//begin返回的自然是数组的首位元素的地址 } iterator end() { return _str + _size; }//同上 //下面是适用于const迭代器的函数 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; } string(const char* s ="") :_size(strlen(s)), _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, s);//将s完全拷贝过来,并且不用手动赋值\0,因为strcpy会将\0也一起拷贝过来 } ~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; } //string(const string& c) // :_size(c._size) // ,_capacity(c._capacity) //{ // delete[] _str;//释放掉原空间元素 // //既然是拷贝首先需要确定原先的_str空间中不存在元素了 // for (int i = 0; i < c.size(); i++) // { // push_back(c[i]); // }//使用push_back直接将其赋值给*this // _str[_size] = '\0';//最后手动增加\0 //}//当然这是一种传统的写法 //下面还有一种写法但是这种写法需要首先完成swap函数 void swap(string& c) { std::swap(_size, c._size); std::swap(_capacity, c._capacity); std::swap(_str, c._str); }//要交换两个对象的内容,无非也就是将成员变量交换而已 string(const string& c) { string tmp(c._str);//首先拷贝一个tmp对象 swap(tmp);//将*this和tmp交换,即可完成,这样当构造完成之后原本的空间也会交给编译器底层自己去释放 } //这一个也就是在pos位置插入一个字符c //方法一:移动数据 //string& insert(size_t pos, char c) //{ // assert(pos < _size); // reserve(_size + 1);//确保空间足够 // int end = _size - 1; // while (end >= (int)pos)//将int强转为int // { // _str[end + 1] = _str[end]; // end--; // } // _size++; // _str[pos] = c; // _str[_size] = '\0';//手动增加\0 // return *this; //} ////解决方法二: string& insert(size_t pos, char c) { assert(pos <= _size); reserve(_size + 1);//确保空间足够 int end = _size;//这里就不能是_size-1了,如果还是_size-1那么最后一个元素就会被覆盖导致数据丢失 while (end > pos)//解决办法二也就是让end等于pos的时候就要退出循环 { _str[end] = _str[end - 1];//同时这里为了能够让pos位置的值也能移动所以这里是str[end] = str[end-1]; //如果这里依旧采用的是end+1 = end的话那么最后一个元素可以被移动但是pos位置的值又会无法移动导致元素缺失 end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; } //我这里实现的是一个无参的默认构造函数,开辟了一个空间用以储存\0 string& insert(size_t pos, const char* s) { //首先第一步肯定还是判断pos和空间是否足够 assert(pos < _size); int len = strlen(s); reserve(_size + len); //依旧使用移动的思想 int end = _size; while(end > pos)//这里依旧会发生上面的那个问题所以这里依旧要解决 //那个问题 { _str[end+len-1] = _str[end - 1]; end--; } memcpy(_str + pos, s, len);//最后将中间的字符拷贝进去即可 _size += len; _str[_size] = '\0'; return *this; } string& erase(size_t pos = 0, size_t len = npos) { if (pos + len > _size)//如果大于了这些 { _str[pos] = '\0'; _size = pos; }//直接将从pos开始往后的所有元素删除 else { while (pos < _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; }//否则就将pos到pos+len之间的元素删除 return *this; }//这里使用了全缺省在不传参数的情况下这个函数会将整个string都删除 void erase(iterator pos) { int end = _size;//和删除单独pos位置上的值是一样的依旧使用的是移动 while (pos < _str+end) { *pos = *(pos + 1); pos++; } _size -= 1; } void erase(iterator first, iterator last) //删除从first到last迭代器中间的值 { int len = last - first;//两个指针相减从而得到两者之间相距离的元素个数 while (first < _str + _size) { *first = *(first + len); first++; } _size -= len; } void pop_front() { erase(0,1); } void pop_back() { erase(_size - 1, 1); } void reserve(size_t n = 0) { if (n > _capacity)//当我们需要的空间大小大于此时的容量时就需要扩容,否则不做任何处理 { char* tmp = new char[n+1];//创建n+1个大小的空间,最后一个空间用于储存\0 _capacity = n; strcpy(tmp, _str); delete[] _str;//手动释放原先的空间 _str = tmp;//将新空间的地址传过去 } } size_t find(char c) { for (int i = 0; i < _size; i++) { if (_str[i] == c) { return i;//找到返回下标 } } return npos;//找不到返回-1 } size_t find(const char* c)const//从对象中寻找字符c/字符串并且返回第一个c的下标 { char* tmp = strstr(_str, c); if (tmp) { return tmp - _str;//找到返回下标两个指针相减得到两个指针之间的元素个数 } else { return npos;//找不到直接返回npos } } //void push_back(char c) //{ // if (_size == _capacity)//容量满了或者刚开始根本没有创建空间 // { // reserve(_capacity == 0 ? 4 : 2 * _capacity);//这里如果_capacity为0则给与默认的4个字节的大小的空间 // //否则就按照原来容量的2倍来增加空间 // } // _str[_size] = c; // _size++; // _str[_size] = '\0'; //} void push_back(char c) { insert(_size,c); } void push_front(char c) { insert(0, c); } int size() const { return _size; }//返回当前的有效长度 int capacity() const { return _capacity; }//返回当前的容量 char& operator[](size_t n) { assert(n < _size);//防止出现越界访问 return _str[n]; }//这个函数能够让用户既能读数据又能写数据 char operator[](size_t n) const { assert(n < _size); return _str[n]; }//这样写的函数用户只能读取数据,而不能修改string中的数据 string& append(const char* str) { int len = strlen(str);//先算出str的长度 reserve(_size + len);//判断空间 for (int i = 0; i < len; i++) { _str[_size] = str[i];//直接从_size处开始往后赋值 _size++; } _str[_size] = '\0';//手动增加\0 return *this; } string& operator+=(char ch)//适用于增加一个字符 { push_back(ch); return *this; } string& operator+=(const char* str)//适用于增加一个字符串 { append(str);//直接复用即可 return *this; } bool operator>(const string& b) const { return strcmp(_str, b._str)>0; } bool operator<(const string& b) const { return strcmp(_str, b._str) < 0; } bool operator==(const string& b) const { return strcmp(_str, b._str) == 0; } bool operator>=(const string& b) const { return !(*this < b); } bool operator<=(const string& b) const { return !(*this>b); } //string& operator=(const string& s) //{ // //第一步先清空原先的空间 // delete[] _str; // _capacity = s._capacity; // reserve(_capacity);//开辟足够大的空间 // for (int i = 0; i < s._size; i++) // { // _str[_size++] = s[i]; // } // return *this; //}//重载一个等于操作符 string& operator=(string s) { //首先在传参时就完成了拷贝 swap(s); return *this;//直接和s交换即可,和上面写的拷贝构造时一样的 } string& operator=(const string& s) { //如果不想使用上面的那种 string tmp(s._str);//利用const char* 初始化处一个string对象 swap(tmp);//将tmp和string交换 return *this; } string& resize(size_t n, char c = '\0') { //第一步判断n是否大于_size //如果n小于了_size那么就要删除n以后所有的元素 if (n < _size) { _str[n] = '\0'; _size = n; } else//这里就是大于了_size需要我们增加元素 { reserve(n);//如果n大于了_capacity,自然也会扩容 while (_size < n) { _str[_size] = c; _size++; } _str[_size] = '\0';//最后手动增加\0 } return *this; }//我这里添加的是\0 string substr(size_t pos, size_t len = npos) { string s;//用于储存取出的字符串 //首先先判断npos是否大于_size如果大于的话就将从pos位置开始往后的所有内容都提取出来 if (pos + len >= _size) { for (int i = pos; i < _size; i++) { s += _str[i];//将从pos位置开始的所有元素放到s中 } } else//在这种情况下就不是取出从pos位置开始往后的所有数据 { for (int i = pos; i < pos + len; i++) { s += _str[i]; } } return s; } private: size_t _size;//指向最后一个有效字符的下一位 size_t _capacity;//代表string类此时的容量 char* _str;//指向实际储存字符空间的指针 static const size_t npos; }; const size_t string::npos = -1; ostream& operator<<(ostream& out, const string& b)//这里并不需要将这个函数作为友元函数,可以复用类中的函数以达到 //读取数据的效果 { for (int i = 0; i < b.size(); i++) { out << b[i]; } return out;//让其能够输出读多个string对象 } istream& operator>>(istream& in, string& b) { //第一步创建一个临时的静态数组 char buff[5]; char ch; int i = 0;//作为buffi的下标 ch = in.get();//将读取到的值放到ch中 while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 4) { buff[i] = '\0';//手动为i增加一个\0 b += buff;//复用函数 i = 0;//将i改为0 } ch = in.get();//将读取到的值放到ch中 } //当循环结束的时候如果i不是0代表buff中储存有我们需要的值 if (i != 0) { buff[i] = '\0';//依旧手动增加一个\0 b += buff;//复用函数完成 } return in; } };

最后希望这篇博客能对您有所帮助,如果发现了任何的错误,请提出我一定改正。


标签:string

本文共计8126个文字,预计阅读时间需要33分钟。

为何长尾词在中,能引发思考的涟漪?

`string` 类为什么需要被做成模板?首先,要了解为什么 `string` 类,它被设计成模板类。

`string` 类作为存储字符序列的容器,它本身需要灵活地处理不同长度的字符串,并且可能需要存储不同编码的字符。以下是一些原因:

1. 类型安全性:模板类允许 `string` 类在不同的数据类型(如 `char`, `wchar_t`, `char16_t`, `char32_t`)之间自动进行选择,保证使用时的类型安全性。

2.多语言支持:不同编码的字符需要不同的存储方式。模板 `string` 类可以根据不同的编码方式存储字符串,例如 `std::string` 在 C++11 中引入了对 UTF-8 的支持。

3.扩展性:模板 `string` 类可以方便地扩展,支持新的字符类型和编码方式,而无需修改原始类的实现。

以下是 `string` 类的示例:

string 我是我们

在这个例子中,`string` 类可以存储包括中文字符在内的各种字符,这正是模板 `string` 类的优势所在。

string类为何要被做成模板

首先要了解一下为什么string类,要被做成模板如下图:

string我们知道是用来储存字符串的,对于英文使用asc2码即可以代表所有的英文符号,但是string这个类为何要被做成模板呢?

这就要涉及到编码的问题了,首先我们知道对于一个整型(浮点型)而言,计算机是通过记录它的补码来记录值的,那么对于一个文字呢?对于一个文字计算机又是怎么储存它的呢?这里就要提及到第一个编码,即ASC编码,这个编码的全程为American Standard Code for Information Interchange,即美国用来表示自己文字的编码。


那么这个编码我们知道是通过映射的方式表示英文字母和符号的。因为计算机的内存肯定是无法储存一个字母A的,内存中储存的只有0和1,由0和1组成了不同的值,在让不同的值映射为不同的符号,例如上面的字符0映射的数字也就是64。这种值和符号一一映射的表也就叫做编码表。

而用来表示美国文字的也就是ASC2码表而。


使用这些ASC2码也就能够将英语的一句话保存进计算机之中了。如下图

以h为例子,0x68转化为10进制为104对应的正是英文字母h。

当然这只是对于英语的编码表。而对于中文而言我们的语言不像英语,如果还是使用一个字节表示汉字的话,一个字节也就8个比特位,而2^8也才256(一个比特位只有两个选择),对于中文而言是远远不够的。如果使用两个字节(表达6w多个子)差不多就可以了。但是除此之外对于每一个不同的国家都要使用不同的编码表这就会显得很麻烦,由此便提出了Unicode编码。

Unicode编码又往下分为好几种。能够解决如我国一样文字多的问题,或是文字很少国家的问题。

为了解决我国汉字的问题,就往下提出了三种编码方式称为UTF系列。

其中UTF-8的特点就是兼容ASC2码,然后对于常见的汉字使用两个字节编写,较不常见的使用三个字节编码,对于生僻字则使用4个字节编写。如下图:

使用代码验证:

每一个常见的汉字都是2个字节一共4个汉字所以大小为8个字节。

但是在windows上一般使用的是GBK系列而不是UTF8。当然除了UTF-8编码之外还有UTF-16,和UTF-32编码,其中UTF-32对于所有的汉字都是使用32个比特位也就是4个字节来表示,这样就能使得所有的汉字表达都拥有了统一性,而UTF-16则在两者之中取中解决了一些问题。这里就不深说了。

而编译器为了更好的兼容UTF-8和UTF-16在c++11出来之前还设置了一个变量也就是wchar,一个wchar使用的是两个字节。

而c++在c++11提出之前也就提供了两种string的模板。

其中string管理的是char* ,而wstring管理的则是wchar*。而在c++11出来之后则又提出了char16_t(2个字节),和char32_t(四个字节)来更好的兼容UTF-16和UTF-32.

而这也正是string被做成模板的原因,你传入不同的参数则实例化成不同的string。来适应不同德编码,由此来表示不同国家的文字。windows使用的GBK编码则是由我国自己搞的编码表。

了解完这些,下面我们就来模拟实现string。这里模拟实现的是UTF-8类型的。并不是实现所有的UTF系列。

模拟string

成员变量

首先肯定要确定类中的成员有哪些:

如下图:

便是成员变量

使用一个命名空间就能够和外部的string(vs内部现成的库)隔离开来。

无参默认构造函数

string(size_t size = 0,size_t capacity = 0,char* str = nullptr)//这是一种错误的写法 :_size(size), _capacity(capacity), _str(str) {}//我这里实现的默认构造函数是一个全缺省的函数并且没有开任何的空间 //运用了初始化列表来初始化但是对于一个无参的构造函数而言这是一个错误的写法

导致错误的原因则是会去访问空指针而导致程序崩溃。那么在什么时候去访问了空指针呢?在下面的reserve函数我会解释,我先把无参构造函数的正确写法写一下

string() :_size(0) ,_capacity(0) { _str = new char[1]; _str[0] = '\0'; } //我这里实现的是一个无参的默认构造函数,开辟了一个空间用以储存\0 //当然这里的容量和长度仍旧为0,因为那一个空间并不是给有效数据开辟的而是给\0开辟的

那么按照要有空间才能储存值得规则,下面我们先来实现reserve函数。

reserve函数

这个函数得功能很简单也就是开辟大小为n的空间。函数实现

void reserve(size_t n = 0) { if (n > _capacity)//当我们需要的空间大小大于此时的容量时就需要扩容,否则不做任何处理 { char* tmp = new char[n+1];//创建n+1个大小的空间,最后一个空间用于储存\0 _capacity = n; strcpy(tmp, _str); delete[] _str;//手动释放原先的空间 _str = tmp;//将新空间的地址传过去 } }

在这里就可以知道如果我们在写无参默认构造函数的时候,传给_str的一个空指针,那么在strncpy的时候就会解引用空指针导致野指针错误。

push_back函数

这是库实现的声明。

我们仿造这个声明来实现这个函数

void push_back(char c) { if (_size == _capacity)//容量满了或者刚开始根本没有创建空间 { reserve(_capacity == 0 ? 4 : 2 * _capacity);//这里如果_capacity为0则给与默认的4个字节的大小的空间 //否则就按照原来容量的2倍来增加空间 } _str[_size] = c; _size++; }

size()和capacity()函数

这两个函数的功能很简单,能让外面的人读取到这个对象的长度和容量。

int size() const { return _size; }//返回当前的有效长度 int capacity() const { return _capacity; }//返回当前的容量

这两个函数都要使用const修饰,如果不加那么一个const string的对象,就会无法访问这两个函数

,因为权限只能缩小和平移而不能放大权限,对于const权限的对象而言,这两个是非const函数,如果调用了等于权限放大了,自然不会让你访问,但是加上const之后,无论是const对象还是非const函数都可以访问这两个函数,非const对象访问const函数权限缩小,所以可以访问。

重载[]函数

我们现在要将[]给重载了让其string,能够和数组一样去访问数据。

我们先来看文档上的关于这个函数的声明

可以看到这个函数有两个声明。这两个函数一个是能够让用户对于数据做到既能读取数据,又能写数据。而const修饰的那个声明用户对于数据只有读的功能而没有写的功能。

下面是实现

char& operator[](size_t n) { assert(n < _size);//防止出现越界访问 return _str[n]; }//这个函数能够让用户既能读数据又能写数据 char operator[](size_t n) const { assert(n < _size); return _str[n]; }//这样写的函数用户只能读取数据,而不能修改string中的数据

下面我们来验证上面的这些函数是否有问题:

出一个测试用例

void printf_arr(const string& c)//为了防止有人在函数里面修改a所以使用const修饰 { for (int i = 0; i <c.size(); i++) { cout << c[i] << " "; } cout << endl; } void teststring1() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_back('d'); a.push_back('e'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl;//以上我们来测试[]和push_back的读和写的功能 for (int i = 0; i < a.size(); i++) { a[i] = 'f'; cout << a[i] << " "; } cout << endl;//这里测试[]的写入功能 printf_arr(a);//假设这里还存在一个函数功能为打印 }

从这个图里就可以看到直接报错误了

取出赋值之后的正确运行结果:

完善构造函数

上面我们已经写过了无参的构造函数,但是大部分时候,我们会将一个字符串直接初始化进对象中,所以我们也要支持这个构造函数。首先我们先来看官方的文档是如何声明的。

可以看到这之中有很多种,我就只实现其中的两种了

string(const char* s ="") :_size(strlen(s)), _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, s);//将s完全拷贝过来,并且不用手动赋值\0,因为strcpy会将\0也一起拷贝过来 }//当然如果这里你选择了全缺省的参数,那么之前所写的那个无参的构造就可以删除了 //这个函数完美兼容无参构造函数的功能。 //string(const string& c) // :_size(c._size) // ,_capacity(c._capacity) //{ // delete[] _str;//释放掉原空间元素 // //既然是拷贝首先需要确定原先的_str空间中不存在元素了 // for (int i = 0; i < c.size(); i++) // { // push_back(c[i]); // }//使用push_back直接将其赋值给*this // _str[_size] = '\0';//最后手动增加\0 //}//当然这是一种传统的写法 //下面还有一种写法但是这种写法需要首先完成swap函数 void swap(string& c)//记住这里传的是引用,如果不传引用,就会造成交换失败,不传引用那么形参的改变不会 //影响实参 { std::swap(_size, c._size); std::swap(_capacity, c._capacity); std::swap(_str, c._str); }//要交换两个对象的内容,无非也就是将成员变量交换而已 string(const string& c) { string tmp(c._str);//首先拷贝一个tmp对象 swap(tmp);//将*this和tmp交换,即可完成,这样当构造完成之后原本的空间也会交给编译器底层自己去释放 }

下面就来测试一下这些功能


void teststring2() { string a("abcdefg");//测试使用一个字符串初始化 for (int i = 0; i < a.size(); i++) { cout << "a:" << a[i] << " "; } cout << endl; string b(a);//使用a初始化 for (int i = 0; i < b.size(); i++) { cout << "b:" << b[i] << " "; } cout << endl; }

实现析构函数

~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; }

迭代器的实现

string既然是一种容器那么一定会存在迭代器,首先解释一下什么是迭代器。

迭代器(Iterator)是一种用于遍历容器(如数组、链表、字符串等)中元素的对象。它提供了一种统一的访问容器元素的方式,使得可以通过相同的方式遍历不同类型的容器。

首先让我们来看一下迭代器的使用

void testiterator() { string a("abcdefghijkl"); string::iterator it = a.begin(); while (it != a.end()) { cout << *it << " "; it++; } cout << endl;//这是string迭代器的使用 vector<int> c; c.push_back(1); c.push_back(2); c.push_back(3); c.push_back(4); c.push_back(5); vector<int>::iterator it1 = c.begin(); while (it1 != c.end()) { cout << *it1 << " "; it1++; } cout << endl;//这里是vector的迭代器可以看到和string迭代器的使用可以说是一摸一样 }

从中就可以看到了迭代器的使用了。

那么string的迭代器要如何实现呢?其实对于这些内存空间开辟是连续的容器(string,vector)而言,原生指针就可以充当迭代器的使用了。例如对于string而言char*就可以作为迭代器了,但是对于申请的空间不是连续的容器而言(list)迭代器的实现就不能使用原生指针了。

我们先来看string的迭代器如何实现。

typedef char* iterator;//将char*命名为iterator typedef const char* const_iterator;//适应于const对象的const迭代器 iterator begin() { return _str;//begin返回的自然是数组的首位元素的地址 } iterator end() { return _str + _size; }//同上 //下面是适用于const迭代器的函数 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }

在实现完成迭代器之后我们就可以使用范围for了,因为范围for底层也就是使用了迭代器。

下面是测试实例:

void teststring3() { string a("abcdefg"); string::iterator it = a.begin(); while (it != a.end()) { cout << *it << " "; it++; } cout << endl; cout << "使用范围for" << endl; for (auto e : a) { cout << e << " "; } cout << endl; }

当然在使用string要遍历的时候还是很少使用迭代器的,还是使用[]比较方便。

实现insert和erase

首先我们还是先来看较为官方的insert声明

在这里我会实现两个insert函数。


首先第一个在pos位置插入一个元素

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size - 1; while (end >= pos) { _str[end + 1] = _str[end]; end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

下面是测试后运行的结果,虽然现在是正确了,但是如果你将n设为0,那么程序就会崩溃。下面测试的图是在n为2,插入m。

那么当n为0的时候为什么会崩溃呢?

因为在循环条件里面,当end小于pos的时候循环才会停止,但是当pos为0的时候,只有end为-1,程序才会停止,但是pos为无符号整型,那么end在和pos比较的时候也会被转换为无符号整型,当end为0减一之后会变成无符号整型的最大值,而不是-1。

对于这种问题有两种解决方法。

解决方法一:

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size - 1; while (end >= (int)pos)//将size_t的pos强转为int { _str[end + 1] = _str[end]; end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

解决方法二:

string& insert(size_t pos, char c) { assert(pos < _size); reserve(_size + 1);//确保空间足够 int end = _size;//这里就不能是_size-1了,如果还是_size-1那么最后一个元素就会被 //覆盖导致数据丢失 while (end > pos)//解决办法二也就是让end等于pos的时候就要退出循环 { _str[end] = _str[end - 1];//同时这里为了能够让pos位置的值也能移动所以这里是 //str[end] = str[end-1]; //如果这里依旧采用的是end+1 = end的话那么最后一个元素可以被移动但是pos位置的值又会 //无法移动导致元素缺失 end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; }

下面是当n为0的测试用例:

void teststring4() { string a("abcd"); a.insert(0, 'm'); for (auto e : a) { cout << e << " "; } }

当然除了这种移动的方法之外还可以使用库函数完成使用思路:使用strncpy将pos前面包括pos的数据拷贝到一个新空间,再将pos后面的数据拷贝到新空间,在这两个拷贝数据的中间留够插入的空间即可。

这里就不实现了。

下面实现将一段字符串插入到一个string对象中(pos位置)

string& insert(size_t pos, const char* s) { //首先第一步肯定还是判断pos和空间是否足够 assert(pos < _size); int len = strlen(s); reserve(_size + len); //依旧使用移动的思想 int end = _size; while(end > pos)//这里依旧会发生上面的那个问题所以这里依旧要解决 //那个问题 { _str[end+len-1] = _str[end - 1]; end--; } memcpy(_str + pos, s, len);//最后将中间的字符拷贝进去即可 _size += len; _str[_size] = '\0'; return *this; }

下面时测试实例:

void teststring4() { string a("abcd"); a.insert(0, "hijk"); for (auto e : a) { cout << e << " "; } }

有了这两个函数那么我们在实现头插和尾插的时候就可以复用了。

void push_back(char c) { insert(0,c); } void push_front(char c) { insert(_size, c); }

下面是测试实例:

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

下面实现erase函数。依旧实现来看官方的声明。

下面我们就来实现第一个删除从pos位置开始的len个元素的值。

string& erase(size_t pos = 0, size_t len = npos) { if (pos + len > _size)//如果大于了这些 { _str[pos] = '\0'; _size = pos; }//直接将从pos开始往后的所有元素删除 else { while (pos < _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; }//否则就将pos到pos+len之间的元素删除 return *this; }//这里使用了全缺省在不传参数的情况下这个函数会将整个string都删除

测试用例:

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; a.erase(0, 2); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

运行截图:

下一个是删除当前迭代器指向的那个值。

void erase(iterator pos) { int end = _size;//和删除单独pos位置上的值是一样的依旧使用的是移动 while (pos < _str+end) { *pos = *(pos + 1); pos++; } }

以及删除一段迭代器区间的值

void erase(iterator first, iterator last) //删除从first到last迭代器中间的值 { int len = last - first;//两个指针相减从而得到两者之间相距离的元素个数 while (first < _str + _size) { *first = *(first + len); first++; } _size -= len; }

有了erase之后头删和尾删的函数也就可以复用了

void teststring5() { string a; a.push_back('a'); a.push_back('b'); a.push_back('c'); a.push_front('0'); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; a.pop_back(); a.pop_front(); for (int i = 0; i < a.size(); i++) { cout << a[i] << " "; } cout << endl; }

实现find函数

首先find函数的功能是从对象中寻找符合字符串或字符的元素,并返回下标。

即假设一个string对象中的值为abcde,需要你寻找c那么这个函数最后会返回c的下标。

如果你寻找的是bc字符串,最后会返回b的下标。

实现这个函数可以使用kmp算法,但是限于篇幅这里就不说了。

我们下面来看官方的声明

我们这里就是实现一个find函数

size_t find(const char* c)const//从对象中寻找字符c/字符串并且返回第一个c的下标 { char* tmp = strstr(_str, c);//直接使用strstr函数 //这个函数能够从_str中寻找c找到返回指向那个值的指针,找不到返回nullptr if (tmp) { return tmp - _str;//找到返回下标两个指针相减得到两个指针之间的元素个数 } else { return npos;//找不到直接返回npos } }

运行结果及测试用例

实现append函数

append函数的功能也即是追加字符串

string& append(const char* str) { int len = strlen(str);//先算出str的长度 reserve(_size + len);//判断空间 for (int i = 0; i < len; i++) { _str[_size] = str[i];//直接从_size处开始往后赋值 _size++; } _str[_size] = '\0';//手动增加\0 return *this; }

测试及运行截图

操作符重载

首先先重载一个+=

string& operator+=(char ch)//适用于增加一个字符 { push_back(ch); return *this; } string& operator+=(const char* str)//适用于增加一个字符串 { append(str);//直接复用即可 return *this; }

下面还有>,<,==和!=的重载,思路也都是复用了strcmp,因为我们写的这个只是针对于char*的所以使用这个,如果你要将string写为模板的话,这个是不合适的。

bool operator>(const string& b) const { return strcmp(_str, b._str)>0; } bool operator<(const string& b) const { return strcmp(_str, b._str) < 0; } bool operator==(const string& b) const { return strcmp(_str, b._str) == 0; } bool operator>=(const string& b) const { return !(*this < b); } bool operator<=(const string& b) const { return !(*this>b); }

这里我就只测试了一个

实现resize函数

首先我们要知道string中的resize函数用于改变字符串的大小。它接受一个参数,表示新的大小。如果新的大小大于当前字符串的大小,那么字符串会被扩展,多出的部分会被填充为指定的字符(默认为空格)。如果新的大小小于当前字符串的大小,那么字符串会被截断,只保留前面的部分。

下面来实现这个函数,

string& resize(size_t n, char c = '\0') { //第一步判断n是否大于_size //如果n小于了_size那么就要删除n以后所有的元素 if (n < _size) { _str[n] = '\0'; _size = n; } else//这里就是大于了_size需要我们增加元素 { reserve(n);//如果n大于了_capacity,自然也会扩容 while (_size < n) { _str[_size] = c; _size++; } _str[_size] = '\0';//最后手动增加\0 } return *this; }//我这里添加的是\0

流插入(<<)和流提取(>>)函数的实现

流插入(<<)

ostream& operator<<(ostream& out, const string& b)//这里并不需要将这个函数作为友元函数,可以复用 //类中的函数以达到读取数据的效果 { for (int i = 0; i < b.size(); i++) { out << b[i]; } return out;//让其能够输出读多个string对象 }//需要注意的是这个函数并没有写到对象class中而是写到了class外作为一个独立的函数, //而不是类的成员函数

测试及运行截图:

流提取(>>)

istream& operator>>(istream& in, string& b) { char ch; in >> ch;//先将这个值放到ch中去 while (ch != '\n' && ch != ' ') { b += ch;//在增加到b中 in >> ch; } return in; }//如果你使用这种写法那么最后会无法结束循环 //这是错误写法

为什么按照上面的写法会无法结束while循环呢?因为in这个对象不会接收空格和换行符(\n)也就导致了ch不可能等于\n或是等于空格。

那么为了解决这个问题就要使用in对象中的get函数,能够让cin接收换行和空格。

istream& operator>>(istream& in, string& b) { char ch; in.get(ch);//先将这个值放到ch中去,使用get让cin能够接收换行和空格 while (ch != '\n' && ch != ' ') { b += ch;//在增加到b中 in.get(ch); } return in; }

。但是这么写还有一个很不好的点也就是,如果我输入的字符串非常的长,那么就会造成多次的扩容,如果你预先开了空间那么又会造成可能开的空间过大,而输入过少浪费的情况。为了解决这个问题我们可以这么写。

按照下面这么写那么就能够解决多次扩容的问题,不用担心buff数组,在函数结束之后会自动地释放。

istream& operator>>(istream& in, string& b) { //第一步创建一个临时的静态数组 char buff[128]; char ch; int i = 0;//作为buffi的下标 ch = in.get();//将读取到的值放到ch中 while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { buff[i] = '\0';//手动为i增加一个\0 b += buff;//复用函数 i = 0;//将i改为0 } ch = in.get();//将读取到的值放到ch中 } //当循环结束的时候如果i不是0代表buff中储存有我们需要的值 if (i != 0) { buff[i] = '\0';//依旧手动增加一个\0 b += buff;//复用函数完成 } return in; }


实现substr函数

这个函数的功能为取出string对象中从pos位置开始的一个字符串。

string substr(size_t pos, size_t len = npos) { string s;//用于储存取出的字符串 //首先先判断npos是否大于_size如果大于的话就将从pos位置开始往后的所有内容都提取出来 if (pos + len >= _size) { for (int i = pos; i < _size; i++) { s += _str[i];//将从pos位置开始的所有元素放到s中 } } else//在这种情况下就不是取出从pos位置开始往后的所有数据 { for (int i = pos; i < pos + len; i++) { s += _str[i]; } } return s;//这里需要一个拷贝构造,不能传引用,因为离开这个函数之后s就被销毁了 }

当然现在还无法测试这个函数因为还差最后一个构造函数

完善=操作符

string& operator=(const string& s) { //第一步先清空原先的空间 delete[] _str; _capacity = s._capacity; reserve(_capacity);//开辟足够大的空间 for (int i = 0; i < s._size; i++) { _str[_size++] = s[i]; } return *this; }//重载一个等于操作符

当然上面依旧是传统的写法,我们也可以使用新式的写法。

为何长尾词在中,能引发思考的涟漪?

string& operator=(string s) { //首先在传参时就完成了拷贝 swap(s); return *this;//直接和s交换即可,和上面写的拷贝构造时一样的 }//这两种写法选一个即可 string& operator=(const string& s) { //如果不想使用上面的那种 string tmp(s._str);//利用const char* 初始化处一个string对象 swap(tmp);//将tmp和string交换 return *this; }

下面是测试和运行截图

代码合集

namespace LHY { class string//模拟实现string类 { public: /*string() :_size(0) ,_capacity(0) { _str = new char[1]; _str[0] = '\0'; }*/ typedef char* iterator;//将char*命名为iterator typedef const char* const_iterator;//适应于const对象的const迭代器 iterator begin() { return _str;//begin返回的自然是数组的首位元素的地址 } iterator end() { return _str + _size; }//同上 //下面是适用于const迭代器的函数 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; } string(const char* s ="") :_size(strlen(s)), _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, s);//将s完全拷贝过来,并且不用手动赋值\0,因为strcpy会将\0也一起拷贝过来 } ~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; } //string(const string& c) // :_size(c._size) // ,_capacity(c._capacity) //{ // delete[] _str;//释放掉原空间元素 // //既然是拷贝首先需要确定原先的_str空间中不存在元素了 // for (int i = 0; i < c.size(); i++) // { // push_back(c[i]); // }//使用push_back直接将其赋值给*this // _str[_size] = '\0';//最后手动增加\0 //}//当然这是一种传统的写法 //下面还有一种写法但是这种写法需要首先完成swap函数 void swap(string& c) { std::swap(_size, c._size); std::swap(_capacity, c._capacity); std::swap(_str, c._str); }//要交换两个对象的内容,无非也就是将成员变量交换而已 string(const string& c) { string tmp(c._str);//首先拷贝一个tmp对象 swap(tmp);//将*this和tmp交换,即可完成,这样当构造完成之后原本的空间也会交给编译器底层自己去释放 } //这一个也就是在pos位置插入一个字符c //方法一:移动数据 //string& insert(size_t pos, char c) //{ // assert(pos < _size); // reserve(_size + 1);//确保空间足够 // int end = _size - 1; // while (end >= (int)pos)//将int强转为int // { // _str[end + 1] = _str[end]; // end--; // } // _size++; // _str[pos] = c; // _str[_size] = '\0';//手动增加\0 // return *this; //} ////解决方法二: string& insert(size_t pos, char c) { assert(pos <= _size); reserve(_size + 1);//确保空间足够 int end = _size;//这里就不能是_size-1了,如果还是_size-1那么最后一个元素就会被覆盖导致数据丢失 while (end > pos)//解决办法二也就是让end等于pos的时候就要退出循环 { _str[end] = _str[end - 1];//同时这里为了能够让pos位置的值也能移动所以这里是str[end] = str[end-1]; //如果这里依旧采用的是end+1 = end的话那么最后一个元素可以被移动但是pos位置的值又会无法移动导致元素缺失 end--; } _size++; _str[pos] = c; _str[_size] = '\0';//手动增加\0 return *this; } //我这里实现的是一个无参的默认构造函数,开辟了一个空间用以储存\0 string& insert(size_t pos, const char* s) { //首先第一步肯定还是判断pos和空间是否足够 assert(pos < _size); int len = strlen(s); reserve(_size + len); //依旧使用移动的思想 int end = _size; while(end > pos)//这里依旧会发生上面的那个问题所以这里依旧要解决 //那个问题 { _str[end+len-1] = _str[end - 1]; end--; } memcpy(_str + pos, s, len);//最后将中间的字符拷贝进去即可 _size += len; _str[_size] = '\0'; return *this; } string& erase(size_t pos = 0, size_t len = npos) { if (pos + len > _size)//如果大于了这些 { _str[pos] = '\0'; _size = pos; }//直接将从pos开始往后的所有元素删除 else { while (pos < _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; }//否则就将pos到pos+len之间的元素删除 return *this; }//这里使用了全缺省在不传参数的情况下这个函数会将整个string都删除 void erase(iterator pos) { int end = _size;//和删除单独pos位置上的值是一样的依旧使用的是移动 while (pos < _str+end) { *pos = *(pos + 1); pos++; } _size -= 1; } void erase(iterator first, iterator last) //删除从first到last迭代器中间的值 { int len = last - first;//两个指针相减从而得到两者之间相距离的元素个数 while (first < _str + _size) { *first = *(first + len); first++; } _size -= len; } void pop_front() { erase(0,1); } void pop_back() { erase(_size - 1, 1); } void reserve(size_t n = 0) { if (n > _capacity)//当我们需要的空间大小大于此时的容量时就需要扩容,否则不做任何处理 { char* tmp = new char[n+1];//创建n+1个大小的空间,最后一个空间用于储存\0 _capacity = n; strcpy(tmp, _str); delete[] _str;//手动释放原先的空间 _str = tmp;//将新空间的地址传过去 } } size_t find(char c) { for (int i = 0; i < _size; i++) { if (_str[i] == c) { return i;//找到返回下标 } } return npos;//找不到返回-1 } size_t find(const char* c)const//从对象中寻找字符c/字符串并且返回第一个c的下标 { char* tmp = strstr(_str, c); if (tmp) { return tmp - _str;//找到返回下标两个指针相减得到两个指针之间的元素个数 } else { return npos;//找不到直接返回npos } } //void push_back(char c) //{ // if (_size == _capacity)//容量满了或者刚开始根本没有创建空间 // { // reserve(_capacity == 0 ? 4 : 2 * _capacity);//这里如果_capacity为0则给与默认的4个字节的大小的空间 // //否则就按照原来容量的2倍来增加空间 // } // _str[_size] = c; // _size++; // _str[_size] = '\0'; //} void push_back(char c) { insert(_size,c); } void push_front(char c) { insert(0, c); } int size() const { return _size; }//返回当前的有效长度 int capacity() const { return _capacity; }//返回当前的容量 char& operator[](size_t n) { assert(n < _size);//防止出现越界访问 return _str[n]; }//这个函数能够让用户既能读数据又能写数据 char operator[](size_t n) const { assert(n < _size); return _str[n]; }//这样写的函数用户只能读取数据,而不能修改string中的数据 string& append(const char* str) { int len = strlen(str);//先算出str的长度 reserve(_size + len);//判断空间 for (int i = 0; i < len; i++) { _str[_size] = str[i];//直接从_size处开始往后赋值 _size++; } _str[_size] = '\0';//手动增加\0 return *this; } string& operator+=(char ch)//适用于增加一个字符 { push_back(ch); return *this; } string& operator+=(const char* str)//适用于增加一个字符串 { append(str);//直接复用即可 return *this; } bool operator>(const string& b) const { return strcmp(_str, b._str)>0; } bool operator<(const string& b) const { return strcmp(_str, b._str) < 0; } bool operator==(const string& b) const { return strcmp(_str, b._str) == 0; } bool operator>=(const string& b) const { return !(*this < b); } bool operator<=(const string& b) const { return !(*this>b); } //string& operator=(const string& s) //{ // //第一步先清空原先的空间 // delete[] _str; // _capacity = s._capacity; // reserve(_capacity);//开辟足够大的空间 // for (int i = 0; i < s._size; i++) // { // _str[_size++] = s[i]; // } // return *this; //}//重载一个等于操作符 string& operator=(string s) { //首先在传参时就完成了拷贝 swap(s); return *this;//直接和s交换即可,和上面写的拷贝构造时一样的 } string& operator=(const string& s) { //如果不想使用上面的那种 string tmp(s._str);//利用const char* 初始化处一个string对象 swap(tmp);//将tmp和string交换 return *this; } string& resize(size_t n, char c = '\0') { //第一步判断n是否大于_size //如果n小于了_size那么就要删除n以后所有的元素 if (n < _size) { _str[n] = '\0'; _size = n; } else//这里就是大于了_size需要我们增加元素 { reserve(n);//如果n大于了_capacity,自然也会扩容 while (_size < n) { _str[_size] = c; _size++; } _str[_size] = '\0';//最后手动增加\0 } return *this; }//我这里添加的是\0 string substr(size_t pos, size_t len = npos) { string s;//用于储存取出的字符串 //首先先判断npos是否大于_size如果大于的话就将从pos位置开始往后的所有内容都提取出来 if (pos + len >= _size) { for (int i = pos; i < _size; i++) { s += _str[i];//将从pos位置开始的所有元素放到s中 } } else//在这种情况下就不是取出从pos位置开始往后的所有数据 { for (int i = pos; i < pos + len; i++) { s += _str[i]; } } return s; } private: size_t _size;//指向最后一个有效字符的下一位 size_t _capacity;//代表string类此时的容量 char* _str;//指向实际储存字符空间的指针 static const size_t npos; }; const size_t string::npos = -1; ostream& operator<<(ostream& out, const string& b)//这里并不需要将这个函数作为友元函数,可以复用类中的函数以达到 //读取数据的效果 { for (int i = 0; i < b.size(); i++) { out << b[i]; } return out;//让其能够输出读多个string对象 } istream& operator>>(istream& in, string& b) { //第一步创建一个临时的静态数组 char buff[5]; char ch; int i = 0;//作为buffi的下标 ch = in.get();//将读取到的值放到ch中 while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 4) { buff[i] = '\0';//手动为i增加一个\0 b += buff;//复用函数 i = 0;//将i改为0 } ch = in.get();//将读取到的值放到ch中 } //当循环结束的时候如果i不是0代表buff中储存有我们需要的值 if (i != 0) { buff[i] = '\0';//依旧手动增加一个\0 b += buff;//复用函数完成 } return in; } };

最后希望这篇博客能对您有所帮助,如果发现了任何的错误,请提出我一定改正。


标签:string