c++STL——string学习的模拟实现
文章目录
- string的介绍
- 学习的意义
- auto关键字和范围for
- string中的常用接口
- 构造和析构
- 对string得容量进行操作
- string的访问
- 迭代器(Iterators):
- 运算符[ ]重载
- string类的修改操作
- 非成员函数
- string的模拟实现
- 不同平台下的实现
- 注意事项
- 模拟实现部分
- 所有的模拟实现函数
- 预先处理的函数
- 构造函数和析构函数
- 简单迭代器
- 容量操作
- 访问操作
- 修改操作
- string的比较关系
- 操作函数
- 流插入/提取运算符重载
- 对于深拷贝的改进
string的介绍
本章节将重点介绍string的模拟实现,其内部的函数使用则需要自行查看文档使用:
string类的使用
本文将对string类中重点的函数进行模拟实现。在使用string类时,必须包含#include string这个头文件以及using namespace std;
学习的意义
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
所以c++引入了string这个类进行管理这个字符串,并且把其对应的操作函数写成其成员函数。这样子十分方便使用。
string其实是c++库中实现的一个类,我们可以理解为其是字符串。只不过底层使用类似于之前学习过的顺序表进行实现的。在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
auto关键字和范围for
这是c++11引入的概念,为了方便学习后续内容需要先进行了解。
auto关键字其实可以理解为类型的自动识别:
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
1.用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
3.auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
4.auto不能直接用来声明数组
5.auto声名变量的同时必须进行初始化
这些概念我们来简单介绍一下:
即我们在声名变量时可以这么写:
#include<iostream>
using namespace std;
int main() {auto a = 10;auto b = 'a';auto c = 10.5;auto d = &a;cout << typeid(a).name() << endl;cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;return 0;
}
可以使用typeid中的name函数打印出变量的类型名称:
我们发现确实是自动识别了。
但是注意千万不能不进行初始化,因为auto关键字其实是编译器经过特殊处理的,需要通过赋值的内容进行自行推到。如果没有值赋予,就无法推导出类型。
同一行的声名如auto a = 10, b = 5必须是同一个类型的数据,如果前后有不相同的数据,会导致类型推导失败。
函数的参数部分是不能使用auto关键字的,但是返回值却可以。但是需要慎用。如果有多个函数嵌套调用且返回值均是auto,需要一直往调用本函数的上一个函数进行推导是何类型的返回值。这非常麻烦,且会导致代码逻辑混乱。
当然用的最多的地方是范围for的使用:
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){cout << array[i] << endl;
}
// C++11的遍历
for (auto& e : array)e *= 2;
for (auto e : array)cout << e << " " << endl;
也就是说,auto关键字在for中使用的时候,会自动识别访问的类型并且从头进行访问,不需要我们判断访问范围,这是十分方便的。
对于string类中也是可以使用范围for,但是是使用其内部迭代器进行操作的,具体的会在概念部分进行重点的讲解。
对于auto的使用,最大的特点就是在于对于某些类型代码长度比较长的时候,是可以进行一定程度上简化的,还有就是在范围for中的使用。其余情况下,能确定类型还是尽量写成确定类型,也是为了代码的可读性。
string中的常用接口
现在我们来讲一下string中的一些重要概念。其实严格意义上来说,string出现的比STL库要早,并不是STL库中的一员,但是由于其性质和使用方式和STL中其他的类都被封装成类似的方式和对应的接口进行调用,所以是可以把string当作STL中的一员进行学习的。
只不过由于string出现的更早,内部成员函数会更多也更冗杂(也是为了向前兼容),所以只需要重点掌握几个重点函数即可,其余的使用的时候查一下文档即可。重点函数即需要模拟实现的部分。
string可以理解为字符串,字符串常见的操作即:访问、修改、查找、反转、交换等。其内部各种函数又被重载成针对不同中参数的时候对应得操作。下面我们来看看重点的接口:
构造和析构
既然是一个类,又因为其类似于顺序表得实现,所以是需要自行编写构造函数和析构函数的:
(constructor)函数名称 | 功能说明 |
---|---|
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重点) | 拷贝构造函数 |
~string()(重点) | 析构函数 |
析构函数操作系统会自行调用,但是在创建字符串的时候,就需要使用其构造函数。
对string得容量进行操作
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间 |
resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
这里需要说明一点:为了兼容c的用法,string的结尾处也是会加一个‘\0’的。而capacity计算的是不包括‘\0’这个位置以外其他空间的数量,这点需要十分注意。也就是说,假设满容状态下有50个字符,但是为了存储‘\0’会多开一个空间,即总共51个空间,但是真正计算容量的时候其实是50个,需要始终多开一个存储‘\0’。
还有就是字符串有效长度的问题,也是因为string出现的较早,为了兼容c的用法加入了length这个函数,其实在STL库中更多的使用还是size这个函数,计算的就是不包括’0’的字符个数。
clear函数只会把string中的字符数变为0,即清空字符串,但是不会改变其容量。
reserve函数在不同平台下的实现是不一样的,如果传入一个比当前空间数还要小的数给reserve函数,c++并没有明确规定此种行为应该如何操作。在g++编译器下会选择缩容,而vs2022下是不做任何改变的。
resize函数则会改变容量,是可以缩容的,扩容需要填充字符,默认填充的是‘\0’。
string的访问
迭代器(Iterators):
begin和end返回的其实是string的开始位置和结束位置(正向迭代器iterator),这是类似于指针的东西:
注意end()的位置就是‘\0’的位置。
而rbegin和rend是反向迭代器(reverse_iterator):即rbegin指向的是反向开头位置,rend是反向结尾数据:
即对反向迭代器++就是往前走。rend其实指向的是串中第一个元素的前一个位置。
剩下的四个带字母c的迭代器其实是上述四个迭代器对应的常量迭代器,也就是说,使用常量迭代器时,不能对string中的数据进行修改。当然在string类中迭代器用的不算多,因为有更方便的方式,但是在STL其他的容器中(如链表)就会用的比较多。
运算符[ ]重载
毕竟是字符串,在c语言中字符串可以当作数数组使用,也源于[]这个运算符的特性。所以我们希望能够像数组那样访问字符串。
为了方便修改,该运算符重载函数返回该位置的引用。
这里有两种形式,一种是可以修改的,另外一种返回的是不可修改的。如果传入的pos越界会触发断言报错。
string类的修改操作
函数 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在串后追加串str或追加字符c |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind + npos | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr + npos | 在str中从pos位置开始,截取n个字符,然后将其返回 |
我们先来解释一下何为npos,对string类执行修改操作时,会不可避免地涉及到长度的事情。但是长度究竟要多长需要用户输入。但是由于c++中缺省参数的存在,是可以设定默认值的。所以c++在string类中加入静态常量成员变量static const size_t npos,且初始化为-1。由于是size_t类型变量,是无符号整形,所以-1赋值给它其实是赋值的补码,即全1。即size_t对应的是无符号int整形的最大值。
函数讲解:
对于push_back还是很好理解的,就和顺序表的尾插一个字符一样。但是对于尾插其实用的更多的是operator+=的运算符重载,因为更简介也好理解。operator+=可以尾插字符,也可以尾插字符串。
而c_str返回的是string中存储串的c格式的字符串,因为要兼容c的用法。
find和refind其实很好理解,就是从pos位置(不能越界)找传入符号的第一次出现位置。(最常用),只不过一个是向后找,一个向前找。
而substr则是返回string中从pos位置开始长度为len的串,构造一个string并且返回。当然这个len的默认长度是npos。如果后续长度够,则返回len个。如果后续长度不够,则后续的字符全返回。
非成员函数
函数 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
对于+的运算符重载,其实可以理解为拼接两个字符串,将+后的string拼接在前面一个位置上后传值返回。
而流插入流提取符号则是方便输入,使得我们直接输入字符串就能构造一个string类。
但是这样为什么还要getline函数呢?因为对于标准输入流cin,如果在缓冲区中识别空格会忽略掉。而getline不会:
还有就是一些比较关系的运算符重载,这些我们会在模拟实现部分讲解。
string的模拟实现
不同平台下的实现
在这里我们得先知道一个事情就是:在不同平台下,对于string类的实现是不一样的。
对于vs2022来讲,其string类在实现的时候多加入了一个大小为16的buffer数组。也就是当构造的字符串长度小于等于16时,就存储在buffer中,反之才会存储在指针指向的开辟空间上。而对于gcc编译器,则是直接存储在指针指向的空间上。
gcc编译器在扩容的时候严格村寻二倍扩容原则。而vs2022则复杂一些,扩容的时候可能会考虑容量对齐的方式。
对于模拟实现,其实是为了更好的理解这个类的使用,理解其背后的思想。而不是要写出一些更好的实现,所以在后续的模拟实现部分将写最简单的形式,即采用顺序表进行实现,只不过要考虑字符串的一些特殊问题。
注意事项
由于是我们自己写的string类,但是标准库中也有string类。很容易起到命名冲突的问题。在学习命名空间的时候我们就讲到,为了尽可能防止命名冲突,写项目的时候最好将自己部分的代码写入自己的命名空间。所以我设定了命名空间Mystring。
模拟实现部分
所有的模拟实现函数
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;namespace Mystring {class string {public:friend ostream& operator<<(ostream& _cout, const Mystring::string& s);friend istream& operator>>(istream& _cin, Mystring::string& s);typedef char* iterator;//constructorstring(const char* s = "");string(const string& s);string& operator=(const string& s);//Destructor~string();//iteratoriterator begin();iterator end();// modifyvoid push_back(char c);//尾插一个字符string& operator+=(char c);//也是尾插一个字符 只不过是使用运算符重载 还会返回插入后的字符串的引用string& operator+=(const char* str);//尾插字符串void clear();//只进行清空数据,不清空容量void swap(string& s);//交换操作void append(const char* str);//追加字符串// capacitysize_t size() const;//返回字符串长度size_t capacity() const;//返回当前字符串空间数bool empty() const;//判断是否长度为0(空)void resize(size_t n, char c = '\0');//调整字符串长度 加长就补字符cvoid reserve(size_t n);//扩容(不修改内容) 不缩容// access//预算符重载 为了像数组一样获取第index坐标的内容char& operator[](size_t index);const char& operator[](size_t index)const;//relational operators//关系比较运算符bool operator<(const string& s);bool operator==(const string& s);bool operator!=(const string& s);bool operator<=(const string& s);bool operator>(const string& s);bool operator>=(const string& s);//string operationsconst char* c_str()const;//返回字符串的首地址 兼容c使用size_t find(char c, size_t pos = 0) const;//返回c在string中第一次出现的位置 没有就返回npossize_t find(const char* s, size_t pos = 0) const;//返回字串第一次出现的位置 没有也是返回nposstring& erase(size_t pos, size_t len);//删除pos位置开始的,往后数共len个元素 不够就全删string& insert(size_t pos, char c);//在pos位置上插入字符cstring& insert(size_t pos, const char* str);//在pos位置插入字符串//get_npossize_t get_npos();//获取成员变量nposprivate:char* _str;size_t _size; size_t _capacity; static const size_t npos; };//流插入提取运算符重载ostream& operator<<(ostream& _cout, const Mystring::string& s);istream& operator>>(istream& _cin, Mystring::string& s);void TestString_Constructor_Destructor();//测试构造和销毁void TestIterator();//测试迭代器(指针版本)void TestCapacity();//测试容量的成员函数void TestAccess();//测试获取元素操作void TestModify();//测试修改逻辑void TestRelationalOperators();//测试比较逻辑void TestStringOperations();//测试串操作函数void TestMyStringIOstream();//测试IO输入}
注意的是,我们习惯的将经常调用且代码量短小的函数放在类中定义,因为类中函数默认为inline,这样能提高效率。而其余的函数最好声名与定义分离,放在另外一个文见的同一个命名空间中进行定义,同时定义时需要指定类域。这些都是命名空间以及类域的知识,需要熟悉。
预先处理的函数
有一些函数可能会被频繁的被复用,所以可以预先进行实现。这一点后续会体验到。
1.reserve函数
reserve函数是预留容量的函数,因为使用的是vs2022平台进行编译,所以我就学习这个编译器的实现,只扩不缩。还需要注意的是,为了兼容c的用法,末尾应该是要有‘\0’这个字符作为终止字符的。否则一些c库中的函数可能用不了。传入reserve函数的参数n代表为n个有效字符,所以需要多开一个空间,存储‘\0’:
void string::reserve(size_t n) {if (n <= _size) return;char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp; _capacity = n;
}
由于只是处理容量,所以只需要改动_capacity的值。又由于c++中没有realloc这样类似功能的函数,所以需要自行调整空间然后赋值。这里复制直接使用c库函数即可,可读性高、方便。
2.get_npos函数
当然可以把这个值变为公有的成员变量,就不需要通过函数的方式获取。
size_t get_npos() {return npos;
}
这样是更加规范的(个人认为)。
3.c_str函数
为了方便查看某些功能的结果,往往是需要打印出来的。很多人有疑问,使用string类的时候不是可以直接使用标准输入输出流吗?为什么不用那个呢?
我们得直到,那个是string库中实现的,里面包含了对流插入和提取运算符的重载。而我们当前实现的string是我们自己写的,和std中的是不一样的。但是又由于对流插入和运算符重载的知识点比较陌生,通常都是放在后面来讲。而对于字符串类型的数据,c++早已经实现重载,可以直接使用。所以我们可以先使用string返回的字符串类型进行打印查看
const char* string::c_str() const {return _str;
}
构造函数和析构函数
我们打开文档查看会发现,构造函数有很多种形式,但其实很冗余。真正使用的就那么一两个,所以我们只对常用的进行实现。
又由于构造函数和析构函数会被频繁的调用,所以不妨就把构造函数和析构函数写在类中。
1.输入字符串进行构造,不输入默认为空串
我们可以把这个认为不传参时这个就是默认构造函数,传参了就是调用这个构造函数。
string(const char* s = "") {_size = strlen(s);_str = new char[_size + 1];strcpy(_str, s);_capacity = _size;
}
对于此处的缺省参数,其实就是一个空串。c字符串只有一个‘\0’的时候就是空串,长度为0。
使用strcpy函数就可以直接复制了,因为strcpy会把‘\0’一起复制过来。
2.拷贝构造函数
当然拷贝构造也是很常见的,所以也得写一下:
string(const string& s) {_size = s._size;_str = new char[_size + 1];strcpy(_str, s._str);_capacity = _size;
}
3.赋值运算符重载函数
string& operator=(const string& s) {//防止有自己给自己赋值出问题if (*this != s) {delete[] _str;_size = s._size;_str = new char[_size + 1];strcpy(_str, s._str);_capacity = _size;}return *this;
}
赋值重载就是针对于两个已存在的对象进行赋值操作,那么 被赋值的那个可能有很多种情况。所以得先清空资源,再来开空间进行深拷贝。
注意这里的所有赋值和构造都是深拷贝,因为值拷贝会导致析构两次,程序会崩溃。且不符合要求。这点在类和对象的只是讲解就已经提到了。
4.析构函数
因为这是有开辟资源的类,所以默认的析构函数肯定是会导致内存泄露的,所以必须自行写析构函数。
~string() {delete[] _str;_str = nullptr;_capacity = _size = 0;
}
然后就可以自行编写void TestString_Constructor_Destructor();这个函数的实现。这个因人而异,但总体就是检查是否构造成功,是否完成深拷贝,容量大小等是否正确即可。
简单迭代器
虽然前面说了迭代器是类似指针的东西,但实际上并不是。特别是对于STL中其他容器来讲,如果是用指针实现,那指针向后走一步都不是下一个节点的位置。只不过基于string这个类毕竟是用顺序表实现的,所以用指针来实现迭代器也未尝不可:
typedef char* iterator;
//iterator
iterator begin() { return _str;
}
iterator end() {return _str + _size;
}
在自己模拟实现的时候直接可以认为就是char*指针即可。
然后就是编写void TestIteraor();这个函数,测试一下范围for等功能即可。
容量操作
我们可能需要直到当前串的长度,容量为多少,又或是是否为空:
size_t string::size() const {return _size;
}size_t string::capacity() const {return _capacity;
}bool string::empty() const {return (_size == 0);
}
这些逻辑都十分简单。就不多说了。
重点来看一下resize函数:
void string::resize(size_t n, char c) {if (n < _size) {_size = n;}else if (n > _size) {reserve(n);for (size_t i = _size; i < n; i++) { _str[i] = c;}_size = n;}_str[_size] = '\0';
}
如果传入的n小于当前长度,那么直接缩小长度即可,并且记得处理‘\0’的问题。又或是当n大于当前长度,那就需要扩容,所以reserve函数就有用了,直接扩容即可。然后将后续扩容的所有位置都赋值为字符c。默认为‘\0’,修改长度。也是要处理‘\0’问题。鉴于两种情况都要处理,所以就合并写。
而reserve函数已经实现过了,就不再多说。
然后编写void TestCapacity();函数,测试一下reserve和reszie是否正确操作,是否能正确返回对应长度容量等。
访问操作
访问操作有很多种,但是真正用的多的还是对[]的运算符重载:
//access
char& string::operator[](size_t index) {assert(index < _size);//不能越界return _str[index];
}const char& string::operator[](size_t index) const {assert(index < _size);//不能越界 return _str[index];
}
只不过一个是能修改,一个不能修改。
然后编写void TestAccess();函数,测试一下是否能正常访问和修改。如果是const的变量是否做到了只能访问。
修改操作
修改操作即为清空、尾插、头插、任意位置插入等。
清空比较简单,直接把字符串变为空串,长度为0即可。在此我们不进行缩容。
void string::clear() {_size = 0;_str[_size] = '\0';
}
然后就是尾插函数push_back,尾插一个字符char c:
当然是需要判断是否需要扩容的:
void string::push_back(char c) {//判断是否需要扩容if (_size == _capacity) {reserve(_size * 2);}_str[_size++] = c;_str[_size] = '\0';
}
我们总体还是执行二倍扩容的原则,所以满容的情况下直接将当前容量翻倍即可。这件事情直接交给reserve函数完成就好了。内部会完成的复制,容量修改操作。pushback函数就直接修改大小即可,然后需要处理‘\0’的问题。
当然尾插更常用的还是运算符+=的重载,有两种版本,一种是尾插一个字符,一种是尾插字符串:
//尾插一个字符
string& string::operator+=(char c) {push_back(c);return *this;
}
返回的是string的引用,减少拷贝。
string& string::operator+=(const char* str) {size_t len = _size + strlen(str);if(len >= _capacity) reserve((len > 2 * _capacity) ? len : 2 * _capacity);strcpy(_str + _size, str);_size = len;return *this;
}
对于尾插字符串,就得判断一下尾插字符串后的有效长度是否超出当前容量。如果超出就需要进行扩容。但是为了防止过度扩容,可以考虑一下容量对齐,如果长度超出了当前容量的2倍就扩至需要长度容量。反之扩大到当前容量的2倍。
使用strcpy进行复制即可,‘\0’会自动处理。然后再修改容量处理返回值即可。
还有一个追加操作append,就是在字符串末尾追加一个字符串:
void string::append(const char* str) {*this += str;
}
直接复用前面写的operator+=函数即可
最后一个是交换两个字符串操作:
对于交换,很多人认为要向以往那样找个中间量,也就是开辟一个string类对象,作为中转接收。但是这样要一直调用构造函数和拷贝构造,会非常影响效率。
而我们又知道,两个string指向的串实际上是在堆上的,只不过它们在栈区上有一个指针指向这个空间。那么让他们两个指向的空间的地址交换不就好了吗?然后再让大小容量交换不就完成交换了。
而且对于交换这个函数,标准库中是有模板的。直接调用即可,只不过要指定是std标准命名空间中的那个交换函数。
void string::swap(string& s) {std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);
}
还是一样的,需要自行编写void TestModify();函数进行测试相关功能是否正确。
string的比较关系
其实就是字符串的比较大小(如strcmp的功能)。这个部分很简单,在日期类模拟实现的时候早有类似情况,只需要写出判断是否相等和大于或者小于,就可以复用逻辑:
//relational operators
bool string::operator<(const string& s) {return (strcmp(_str, s._str) < 0);
}bool string::operator==(const string& s) {return (strcmp(_str, s._str) == 0);
}bool string::operator!=(const string& s) {return !(*this == s);
}bool string::operator<=(const string& s) {return (*this < s) || (*this == s);
}bool string::operator>(const string& s) {return !(*this <= s);
}bool string::operator>=(const string& s) {return !(*this < s);
}
只需要调用c库中的strcmp函数就可以了。剩下的就是逻辑的复用。
自行编写void TestRelationalOperations();函数进行测试相关功能是否正确。
操作函数
1.返回c串形式的指针
即c_str函数,这个已经实现过了,就不再多说。
2.查找操作find
查找操作find是从pos位置开始向后查找第一个需要查找的字符或者字符串的位置。倒着找就是rfind。但是逻辑基本相同,只不过是查找方向的问题。所以只实现一下正向查找就可。
查找字符:
size_t string::find(char c, size_t pos) const { assert(pos < _size);size_t i = pos;while (i < _size) {if (_str[i] == c) return i;++i;}return npos;
}
逻辑很简单,一个一个找,找到就返回即可。
查找字符串:
size_t string::find(const char* s, size_t pos) const {assert(pos < _size);int sublen = strlen(s);if (sublen > _size) return npos; for (size_t begin = pos; begin <= _size - sublen; begin++) { //匹配过程int i = 0;while (s[i] == _str[begin + i] && s[i] != '\0' && _str[i] != '\0') {++i;}if (i == sublen) return begin;}return npos;
}
可以使用c库中的函数strstr寻找字串。但是其原理也不是很难就自行实现也可以。
当字串长度大于被查找串长度,这肯定找不到的,所以直接返回npos即可。反之需要查找,一直到剩余长度小于字串长度就停止查找即可。每匹配成功一个字符,i就自增,直到i的值与查找的子串长度相同的时候就返回当前位置。但是需要注意的是,匹配过程不能包括字符‘\0’,否则匹配效果会出错。
2.删除操作erase
erase操作主要实现的功能就是从pos位置开始,删除长度为len的字符。(pos不能越界)
当前默认的长度len = npos,即从pos位置开始全删(因为字符串一般达不到那么长)。
所以分两种情况,一种是删除长度len > _size - pos(从pos位置开始的有效元素个数),则将后续的全删。
反之则需要挪动数据:
string& string::erase(size_t pos, size_t len) {assert(pos < _size);if (len >= _size - pos) {_size = pos;_str[_size] = '\0';}else {int poslen = pos + len;while (poslen < _size) {_str[pos] = _str[poslen];++pos;++poslen;}_str[pos] = '\0';_size = pos;}return *this;
}
具体的操作流程可以通过画图来感受。
2.插入操作insert
和erase相对,在pos位置插入字符或者字符串。
一旦涉及到插入操作,就需要进行判断是否需要扩容,所以reserve函数就又派上用场了。
插入一个字符:
string& string::insert(size_t pos, char c) {assert(pos <= _size);if (_size == _capacity) {reserve(_capacity * 2);}if (pos == _size) *this += c;//挪动数据else {for (size_t i = _size + 1; i > pos; i--) {_str[i] = _str[i - 1];}_str[pos] = c;++_size;}return *this;
}
注意i开始的位置,从插入字符后‘\0’放在的位置开始往前走,将前一个位置的值赋值到当前i的位置。这样子到pos位置就能停下。
如果从_size位置开始,将当前的值赋值到后面去,那就要走过pos这个位置才能停下。这会出问题。因为假设pos的位置是0,那么i - 1 的值不是1,而是无符号整形最大值。因为i是size_t类型,不可能小于0。所以从_szie + 1位置开始向后移动数据。
对于pos的位置如果是当前‘\0’的位置,则使用尾插。
插入一个串:
string& string::insert(size_t pos, const char* str) {assert(pos <= _size);if (pos == _size) *this += str;else {size_t sublen = strlen(str);size_t len = sublen + _size; if (len >= _capacity) {reserve((len >= _capacity * 2) ? len : _capacity * 2);}for (size_t i = _size + sublen; i > pos + sublen - 1; i--) {_str[i] = _str[i - sublen];}memcpy(_str + pos, str, sublen);_size = len;}return *this;
}
也是从新串的‘\0’放在的位置开始,不断地将数据向后移动。我们发现其实和上面的过程是相似的,只不过移动数据的位置之间差了一个插入串的长度sublen,当sublen == 1的时候其实就是插入一个字符。然后使用memcpy函数将str的sublen个字节赋值给要插入的位置即可。
对于pos的位置如果是当前‘\0’的位置,则使用尾插。
然后就是自行编写void TestStringOperations();函数进行测试相关功能是否正确。
流插入/提取运算符重载
因为流插入和流提取运算符c++中只对内置类型进行了重载,对于自定义类型是没有的。虽然标准库中确实进行了重载,但那是标准库的。
而我们在自己的命名空间内写的string还没有进行重载,没有办法做到直接将string对象插入到标准输出流,也没有办法直接从标准输入流中提取内容构造string对象。所以我们需要自行进行重载。
在之前模拟实现日期类的时候就说到了,流插入和提取运算符应该重载为全局函数,并且在类中声明为友元函数,因为重载成成员函数第一个参数必须是类对象,这样子会与平常的使用相反。
对于流插入是很简单的,因为只需要打印字符串。那直接将指向串空间的那个指针插入流中就好了,这个标准库中是已经完成重载的了:
ostream& operator<<(ostream& _cout, const Mystring::string& s) {_cout << s._str; return _cout;
}
返回的是流的引用,为了连续赋值。
而流的提取就需要注意的是:
在上面我们已经讲到了,标准输入流是会自动忽略空格的,从而导致不进行输入空格到串中。所以一旦输入空格就会导致串读取错误,不是想要的串。所以需要使用另外一个函数。
即istream中的一个函数get,这个除了‘\n’都能提取到流中。
然后就可以通过这个函数一直从缓冲区内读取,直到‘\n’,不断地尾插到string中即可。
但是如果字符串比较长的情况下,一个一个尾插效率还是非常低地,所以可以考虑自行设置一个缓冲区buffer,大小为256个元素,设置内部所有元素全部为‘\0’。
然后将读取到地内容放在buffer中,直到放够255个后,就一次性尾插到串中。重新开始放在第一个位置。
然后出循环后buffer中可能还剩下一些元素没有插入,个数正好是i个,所以可以直接使用memcpy函数复制i个字节到指定位置即可。然后处理’\0’。
istream& operator>>(istream& _cin, Mystring::string& s) { s.clear();//先清空 要不然读取会错乱int i = 0;char buffer[256] = { '\0' };char ch;ch = _cin.get();while (ch != '\n') {buffer[i++] = ch;if (i == 255) {s += buffer;i = 0;}ch = _cin.get();}s.reserve(s._size + i);memcpy(s._str + s._size, buffer, i);s._size += i;s._str[s._size] = '\0';return _cin;
}
最后还是一样,写一个void TestMyStringIOstream();函数测试一下即可。重点是测试当输入字符串较长的情况下是否能正常输出一样的结果。
对于深拷贝的改进
深拷贝主要就是拷贝构造部分和赋值运算符重载部分,每次都要自己开空间还是很麻烦的,也灭有办法能够让其他地方开空间呢?
答案是有的,需要用到string的交换操作。
对于拷贝构造:
我们调用构造函数,将tmp构造为一个和s有着一样大空间、容量、内容的对象。然后这是个动态成员函数,有this指针,直接将this指向的地址,容量,大小进行交换即可。
但是需要注意的是,由于拷贝构造是构造,当前this指针的三个内容其实是为定义的。所以最好给定缺省值。防止交换后,tmp要调用析构函数释放的是随机值。
即在类定义处给定缺省值。
代码实现:
string(const string& s) {string tmp(s._str);swap(tmp);
}
代码逻辑就简化了很多,不用自己开空间。
而对于赋值重载,也是可以使用这个逻辑的,我们把参数改为string的对象,而不是引用。这样子参数可以直接构造出一个对象,那么直接让this和其交换即可:
string& operator=(string tmp) {swap(tmp);return *this;
}
这样子写虽然效率上没有太多提升,但是写起来会简洁很多。
相关文章:
c++STL——string学习的模拟实现
文章目录 string的介绍学习的意义auto关键字和范围forstring中的常用接口构造和析构对string得容量进行操作string的访问迭代器(Iterators):运算符[ ]重载 string类的修改操作非成员函数 string的模拟实现不同平台下的实现注意事项模拟实现部分所有的模拟实现函数预…...
【寻找Linux的奥秘】第四章:基础开发工具(下)
请君浏览 前言1. 自动化构建1.1 背景1.2 基本语法1.3 make的运行原理1.4通用的makefile 2. 牛刀小试--Linux第一个小程序2.1 回车与换行2.2 行缓冲区2.3 倒计时小程序2.4 进度条小程序原理代码 3. 版本控制器git3.1 认识3.2 git的使用三板斧 3.3 其他 4. 调试器gdb/cgdb4.1 了解…...
RK3588上Linux系统编译C/C++ Demo时出现BUG:The C/CXX compiler identification is unknown
BUG的解决思路 BUG描述:解决方法:首先最重要的一步:第二步:正确设置gcc和g的路径方法一:使用本地系统中安装的 aarch64-linux-gnu-gcc 和 aarch64-linux-gnu-g方法二:下载使用官方指定的交叉编译工具方法三…...
记录一次/usr/bin/ld: 找不到 -lOpenSSL::SSL
1、cmake 报错内容如下: /usr/bin/ld: 找不到 -lOpenSSL::SSL /usr/bin/ld: 找不到 -lOpenSSL::Crypto2、一开始以为库没有正确安装 sudo yum install openssl-devel然后查看openssl 结果还是报错! 3、尝试卸载安装都不管用,网上搜了好多…...
[16届蓝桥杯 2025 c++省 B] 水质检测
思路:分类讨论,从左到右枚举,判断当前的河床和下一个河床的距离是第一行更近还是第二行更近还是都一样近,分成三类编写代码即可 #include<iostream> using namespace std; int main(){string s1,s2;cin>>s1>>…...
基于PySide6与pycatia的CATIA绘图比例智能调节工具开发全解析
引言:工程图纸自动化处理的技术革新 在机械设计领域,CATIA图纸的比例调整是高频且重复性极强的操作。传统手动调整方式效率低下且易出错。本文基于PySide6pycatia技术栈,提出一种支持智能比例匹配、实时视图控制、异常自处理的图纸批处理方案…...
四、Appium Inspector
一、介绍 Appium Inspector 是一个用于移动应用自动化测试的图形化工具,主要用于检查和交互应用的 UI 元素,帮助生成和调试自动化测试脚本。类似于浏览器的F12(开发者工具),Appium Inspector 的主要作用包括: 1.检查 UI 元素 …...
玩转Docker | 使用Docker部署MicroBin粘贴板
玩转Docker | 使用Docker部署MicroBin粘贴板 前言一、MicroBin介绍MicroBin 简介主要特点二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署MicroBin服务下载镜像创建容器检查容器状态检查服务端口安全设置四、访问MicroBin服务访问MicroBin首页登录管理后台…...
BGP分解实验·23——BGP选路原则之路由器标识
在选路原则需要用到Router-ID做选路决策时,其对等体Router-ID较小的路由将被优选;其中,当路由被反射时,包含起源器ID属性时,该属性将代替router-id做比较。 实验拓扑如下: 实验通过调整路由器R1和R2的rout…...
MQTT:单片机中MQTTClient-C移植定时器功能
接下来我们完善MQTTTimer.c和MQTTTimer.h两个功能 MQTTTimer.h void TimerInit(Timer* timer); 功能:此函数用于对 Timer 结构体进行初始化。在 MQTT 客户端里,定时器被用于追踪各种操作的时间,像连接超时、心跳包发送间隔等。初始化操作会…...
可拖动的关系图谱原型案例
关系图谱是一种以图结构形式组织和呈现实体间复杂关联关系的可视化数据模型。它通过节点和线构建多维度网络,能直观揭示隐藏的群体特征和传播路径。在社交网络分析、智能推荐系统、知识图谱构建等领域广泛应用。 软件版本:Axure RP 9 作品类型…...
CST1016.基于Spring Boot+Vue高校竞赛管理系统
计算机/JAVA毕业设计 【CST1016.基于Spring BootVue高校竞赛管理系统】 【项目介绍】 高校竞赛管理系统,基于 DeepSeek Spring AI Spring Boot Vue 实现,功能丰富、界面精美 【业务模块】 系统共有两类用户,分别是学生用户和管理员用户&a…...
从三次方程到复平面:复数概念的奇妙演进(二)
注:本文为 “复数 | 历史 / 演进” 相关文章合辑。 因 csdn 篇幅限制分篇连载,此为第二篇。 生料,不同的文章不同的点。 机翻,未校。 History of Complex Numbers 复数的历史 The problem of complex numbers dates back to …...
PCL 点云投影至指定平面
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 之前的文章中介绍过一个点在平面上的投影坐标,其主要的思路就是利用投影垂线与平面法向量平行的特性,通过推导出的投影公式可以很容易的计算出在某点在某一平面内的投影点。因此只需要重复该过程就可以将整个点云…...
批量将文件名称、文件路径、文件扩展名提取到 Excel 清单
在数字化时代,文件的高效管理至关重要。当我们想要对磁盘中的文件进行整理,想要获取多个文件夹中的文件和路径信息,就需要现将这些文件的名称及路径信息提取出来。本文将介绍一种实用的批量提取技术,帮助用户优化文件管理流程&…...
KWDB创作者计划—KWDB场景创新:多模态数据融合与边缘智能的产业实践
引言:AIoT时代的数据基座重构 在工业物联网设备数量突破千亿、边缘计算节点覆盖率达75%的2025年,传统数据库面临多模态数据处理效率低下、边缘端算力利用率不足、跨域数据协同困难等核心挑战。KWDB(KaiwuDB Community Edition)通过…...
计算机系统概论
1. 计算机系统的基本组成 计算机系统由 硬件系统 和 软件系统 两大部分协同工作: 硬件系统: 基于冯诺依曼体系结构(存储程序原理),包含五大核心部件: 运算器(ALU):执行算…...
Android Cmake构建的项目,需不需要配置指定ndk及版本
在 CMake 构建的 Android 项目中,是否需要显式配置 NDK 及其版本,取决于项目的具体需求和环境。以下是详细分析和建议: 1. 是否需要显式配置 NDK 及版本? 情况 1:Android Studio 自动管理 NDK(推荐&#x…...
国内AI大模型卷到什么程度了?
目录 1.开源大模型更有前景吗? 2.参数越大真的越牛逼吗? 3.榜单排名有意义吗? 大家好这里是AIWritePaper官方账号,官网👉AIWritePaper~ 大模型开源更有前景? 参数越大真的越牛逼吗? 榜单排…...
【HDFS入门】Hadoop 2.0+ HDFS核心架构深度解析:高可用设计揭秘
目录 1 HDFS核心架构概述 2 高可用设计背景 3HDFS核心组件 3.1 Active与Standby NameNode 3.2 JournalNode 3.3 ZKFailoverController(ZKFC) 3.4 DataNode 4 高可用设计的工作流程 写入阶段: 元数据同步: 健康监测&…...
RabbitMQ安装
RabbitMQ安装 Ubuntu环境安装 一、安装Erlang #更新软件包 sudo apt-get update #安装erlang sudo apt-get install erlang 二、安装RabbitMQ #更新软件包 sudo apt-get update #安装rabbitmq sudo apt-get install rabbitmq-server #确认安装结果 systemctl status rabbitmq-…...
2022 CCPC Henan Provincial Collegiate Programming Contest K 复合函数
补题链接 看网上题解很少,来写一份,这题个人觉得思维难度不是特别大,难度主要在于代码准确度,首先将问题转化成 x x x 向 f ( x ) f(x) f(x) 连边,这一步转化应该是比较容易想到的,通过手模样例,会有类…...
Linux : 多线程互斥
目录 一 前言 二 线程互斥 三 Mutex互斥量 1. 定义一个锁(造锁) 2. 初始化锁 3. 上锁 4. 解锁 5. 摧毁锁 四 锁的使用 五 锁的宏初始化 六 锁的原理 1.如何看待锁? 2. 如何理解加锁和解锁的本质 七 c封装互斥锁 八 可重入…...
【数学建模】佳点集(Good Point Set)在智能优化算法中的应用与实现
佳点集(Good Point Set)在智能优化算法中的应用与实现 文章目录 佳点集(Good Point Set)在智能优化算法中的应用与实现1. 佳点集概述2. 佳点集的数学原理3. 佳点集在智能优化算法中的应用3.1 改进麻雀搜索算法(SSA)3.2 改进量子粒子群优化算法(QPSO)3.3 自适应分组差分变异狼群…...
redis linux 安装简单教程(redis 3.0.4)
redis.3.0.4.tar.gz 下载地址 链接: https://pan.baidu.com/s/19VAcrA6XS4mIesH6e5Jftg 提取码: bn2r (1)以安装目录:/home/zsl (2)将redis-3.0.4.tar.gz 拷贝到/home/zsl (3)tar xzvf redis-3.…...
探秘 Python 网络编程:构建简单聊天服务器
在计算机网络的世界里,网络编程是实现不同设备之间通信的关键技术。Python 凭借其简洁的语法和强大的库支持,在网络编程领域有着广泛的应用。无论是构建简单的聊天服务器,还是开发复杂的网络应用,Python 都能轻松胜任。 1 理论基础…...
debian转移根目录
如何在 BIOS 启动的 Debian 虚拟机中将根目录转移到 /dev/sda 设备上?本文将从硬盘分区,根目录复制,重新启动等几个方面介绍。 硬盘分区 1.检查磁盘:查看当前的磁盘和分区情况,确认新添加的磁盘设备名称。 parted -…...
vue3 element-plus表单验证
第一准备一个表单 form.vue <template><div><el-form><el-form-item label"姓名" prop"name"><el-input v-model"data.name" placeholder"请输入姓名"></el-input></el-form-item></e…...
Deepseek IP-Adapter与InstantID的区别
IP-Adapter与InstantID均为基于扩散模型的图像生成控制技术,但两者的算法设计目标、核心模块及应用场景存在显著差异。以下从技术架构、特征处理、条件控制等维度对比两者的差异: 1. 核心设计目标 IP-Adapter 由腾讯团队提出(2023年8月&…...
OSI 七层模型与 TCP/IP 协议栈详解
OSI 七层模型与 TCP/IP 协议栈详解 网络协议模型是理解计算机网络和通信的基础,而 OSI 七层模型和 TCP/IP 协议栈是最常见的两种网络通信模型。虽然这两者有些不同,但它们都提供了一种分层的结构,帮助我们理解和设计网络通信。本文将详细介绍…...
synchronize 或者lock 锁常见的使用场景
在 Java 多线程编程中,synchronized 和 Lock(如 ReentrantLock)是两种常见的线程同步机制。以下是它们的核心区别和典型使用场景,结合代码示例说明: 一、synchronized 的常见场景 1. 简单的临界区保护 public class …...
Redis之缓存更新策略
缓存更新策略 文章目录 缓存更新策略一、策略对比二、常见的缓存更新策略三、如何选择策略四、实际应用示例五、使用 Cache-Aside TTL 的方式,实现缓存商铺信息详情1.引入StringRedisTemplate2.将查询商铺信息加入缓存3.更新商铺信息时移除缓存总结 六、注意事项 一…...
【操作系统学习篇-Linux】进程
1. 什么是进程 课本概念:程序的一个执行实例,正在执行的程序等 内核观点:担当分配系统资源(CPU时间,内存)的实体。 如果你就看这个来理解进程,那么恭喜你,作为初学者,你…...
Docker 前瞻
一、namespace 指令 1.1 dd 命令 dd 命令用于读取、转换并输出数据。 dd 命令可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出。 语法 dd option if 文件名:输入文件名,默认为标准输入…...
【maxENT】最大熵模型(Maximum Entropy Model)R语言实现
文章目录 一、相关package介绍1.1 dismo 包1.2 raster包1.3 常见问题与解决 二、代码示例 🟢🟠先看:【maxENT】最大熵模型(Maximum Entropy Model)介绍与使用(maxENT软件) ASCII文件太大&#…...
高负载WEB服务器--Tomcat
高负载WEB服务器–Tomcat Tomcat介绍 Tomcat 是一个开源的轻量级应用服务器,在 Java Web 应用开发中被广泛使用。 发展历程:Tomcat 最初由 Sun Microsystems 开发,后来成为 Apache 软件基金会的一个项目。它的发展与 Java 技术的发展密切相…...
分页查询列表每页1000条的优化
项目中有一个客户列表,要求每页显示1000条,并且字段很多,接口返回大概要10秒钟,进行优化. 原本逻辑:使用mybatisplus构建查询条件,分页查询客户表,查出数据库DO对象,然后for循环转化成回显的VO对象.在转化的过程中出现了查库代码,导致当每页条数1000时,每一个客户转化都需要查询…...
深入浅出一下Python面向对象编程的核心概念与实践应用
本篇技术博文摘要 🌟 本文系统讲解了Python面向对象编程的核心概念与实践应用。通过电商系统用户订单模拟、动态权限账户系统等案例,深入剖析了类与对象、属性方法、实例方法等基础要素。重点解析了__init__构造方法、__str__对象描述、__lt__比较运算符…...
2025阿里云AI 应用-AI Agent 开发新范式-MCP最佳实践-78页.pptx
2025阿里云AI 应用-AI Agent 开发新范式-MCP最佳实践,包含以下内容: 1、AI 应用架构新范式 2、云原生API网关介绍 3、云原生API网关底座核心优势 4、流量网关最佳实践 5、AI 网关代理 LLM 最佳实践 6、MCP网关最佳实践 7、MSE Nacos MCP Server 注册中心…...
github进阶使用教程
目录索引 一、基本内容 repository fork star codespaces issue 在一个仓库创建话题讨论,可以由仓库主人选择开始和结束话题的讨论 pull request(也称 pr) 协同其他仓库开发,请求仓库主人拉取自己的代码合并到仓库的主分支&…...
【C++】 —— 笔试刷题day_16
刷题_day16,继续加油啊 一、字符串替换 题目解析 这道题是一道简单的字符题目,题目给我们一个字符串A,和n表示A字符串的长度,再给出一个字符数组arg,m表示arg中是数据个数。 然我们在字符串A中找到%s然后替换成arg中的…...
5.3 GitHub订阅系统核心架构解密:高并发设计与SQLite优化实战
GitHub Sentinel 分析报告功能实现:订阅管理核心逻辑解析 关键词:GitHub API 订阅管理, SQLite 数据库设计, RESTful API 开发, 原子操作封装, 异常处理机制 1. 订阅管理功能架构设计 订阅管理模块采用分层架构设计,通过清晰的接口隔离实现高内聚低耦合: #mermaid-svg-bW…...
P5738 【深基7.例4】歌唱比赛
P5738 【深基7.例4】歌唱比赛 题目描述 n ( n ≤ 100 ) n(n\le 100) n(n≤100) 名同学参加歌唱比赛,并接受 m ( m ≤ 20 ) m(m\le 20) m(m≤20) 名评委的评分,评分范围是 0 0 0 到 10 10 10 分。这名同学的得分就是这些评委给分中去掉一个最高分,去掉一个最低分,剩下 …...
从三次方程到复平面:复数概念的奇妙演进(三)
注:本文为 “复数 | 历史 / 演进” 相关文章。 因 csdn 篇幅限制分篇连载,此为第三篇。 生料,不同的文章不同的点。 机翻,未校。 Complex Numbers History: Complex numbers were first introduced by G. Cardano (1501-1576)…...
2025年七星棋牌跨平台完整源码解析(200+地方子游戏+APP+H5+小程序支持,附服务器镜像导入思路)
目前市面上成熟的棋牌游戏源码很多,但能做到平台全覆盖、地方玩法丰富、交付方式标准化的系统却不多。今天这套七星棋牌2023完整源码具备安卓/iOS/H5/微信小程序端四端互通能力,附带200多款地方子游戏,还配备了后台管理与自动热更系统&#x…...
从三次方程到复平面:复数概念的奇妙演进(四)
注:本文为 “复数 | 历史 / 演进” 相关文章合辑。 因 csdn 篇幅限制分篇连载,此为第四篇。 生料,不同的文章不同的点。 机翻,未校。 Complex number and its discovery history 复数及其发现历史 Wenhao Chen, †, Dazheng …...
UE5角色状态机中跳跃落地移动衔接问题
UE5系列文章目录 文章目录 UE5系列文章目录前言一、状态机设置二、主要蓝图 前言 先说说遇到的问题,在我按空格键跳跃落地以后,角色落地再按WSAD键移动就出现了画面中角色抽搐的情况 一、状态机设置 在Unreal Engine 5中创建角色时,处理跳…...
25软考中级*高项网课+历年真题+笔记+电子书+刷题【计算机软考】
两个月逆袭25年软考程序员?这份高效备考指南请收好 25软考中级*高项网课download 📂 软考中级科目备考资料介绍 ✅ 【01】2025 年软件测评师 聚焦软件测试全流程,涵盖 需求分析、测试设计、用例编写、缺陷管理 等核心技能。 📘 备…...
C++STL——容器-list(含模拟实现,即底层原理)(含迭代器失效问题)(所有你不理解的问题,这里都有解答,最详细)
目录 1.迭代器的分类 2.list的使用 2.1 list的构造 2.2 list iterator 2.3 list capacity 2.4 list element access 编辑 2.5 list modifiers 编辑2.5.1 list插入和删除 2.5.2 insert /erase 2.5.3 resize/swap/clear 编辑 2.6 list的一些其他接口…...
Linux系统编程之虚拟内存
概述 计算机内存是临时存储数据的地方,它比硬盘快得多,但容量有限。现代操作系统通过虚拟内存技术,使得每个进程都感觉自己独占整个地址空间,这不仅提高了安全性,也简化了内存管理。 物理内存:实际安装在计…...