当前位置: 首页 > news >正文

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;


		//constructor
		string(const char* s = "")string(const string& s);
		string& operator=(const string& s);

		


		//Destructor
		~string();


		//iterator
		iterator begin();
		iterator end();


		// modify
		void push_back(char c);//尾插一个字符
		string& operator+=(char c);//也是尾插一个字符 只不过是使用运算符重载 还会返回插入后的字符串的引用
		string& operator+=(const char* str);//尾插字符串
		void clear();//只进行清空数据,不清空容量
		void swap(string& s);//交换操作
        void append(const char* str);//追加字符串


        // capacity
		size_t size()  const;//返回字符串长度
		size_t capacity() const;//返回当前字符串空间数
		bool empty() const;//判断是否长度为0(空)
		void resize(size_t n, char c = '\0');//调整字符串长度 加长就补字符c
		void 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 operations
		const char* c_str()const;//返回字符串的首地址 兼容c使用
	    size_t find(char c, size_t pos = 0) const;//返回c在string中第一次出现的位置 没有就返回npos
		size_t find(const char* s, size_t pos = 0) const;//返回字串第一次出现的位置 没有也是返回npos
		string& erase(size_t pos, size_t len);//删除pos位置开始的,往后数共len个元素 不够就全删
		string& insert(size_t pos, char c);//在pos位置上插入字符c
		string& insert(size_t pos, const char* str);//在pos位置插入字符串

		//get_npos
		size_t get_npos();//获取成员变量npos


	private:
		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();函数,测试一下reservereszie是否正确操作,是否能正确返回对应长度容量等。

访问操作

访问操作有很多种,但是真正用的多的还是对[]的运算符重载:

//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函数将strsublen个字节赋值给要插入的位置即可。

对于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;
}

这样子写虽然效率上没有太多提升,但是写起来会简洁很多。

相关文章:

  • opencv 识别运动物体
  • springboot解析
  • Ubuntu 下通过 Docker 部署 WordPress 服务器
  • SpringBoot3-web开发笔记(下)
  • Rockchip 显示架构
  • python基础:数据类型转换、运算符(算术运算符、比较运算符、逻辑运算符、三元运算符、位运算符)
  • 【力扣hot100题】(084)零钱兑换
  • Ubuntu24.04装机安装指南
  • Elasticsearch生态
  • C++ 编程指南34 - C++ 中 ABI 不兼容的典型情形
  • cursor+高德MCP:制作一份旅游攻略
  • NModbus 库在 C# 中的使用
  • 深入理解linux操作系统---第4讲 用户、组和密码管理
  • Dify + Stable Diffusion实现文生图工作流【两种方式】
  • ffmpeg 切割视频失败 ffmpeg 命令参数 -vbsf 在新版本中已经被弃用,需要使用 -bsf:v 替代
  • SD+融合ControlNet,扩散模型V1.5+约束条件边缘图+颜色图实现服装图像生成:定量对比试验结果+分析
  • 【测试】-- 测试分类
  • pg_rman备份pg数据库(在备库执行)
  • 权限管控与数据安全:衡石ChatBot在钉钉中的合规部署指南
  • 基于Cline和OpenRouter模型进行MCP实战
  • 公示!17个新职业、42个新工种亮相
  • 上海市政府常务会议部署提升入境旅游公共服务水平,让国际友人“无障碍”畅游上海
  • 美CIA发布视频“招募中国官员窃取机密”,外交部:赤裸裸的政治挑衅
  • 新华每日电讯:上海“绿色大民生”撑起“春日大经济”
  • “模”范生上海,如何再进阶?
  • 黔西游船倾覆事故84名落水人员已全部找到,10人不幸遇难