C++: string的模拟实现

C++: string的模拟实现

  • 一.前置说明
    • 1.模拟实现string容器的目的
    • 2.我们要实现的大致框架
  • 二.默认成员函数
    • 1.构造函数
    • 2.拷贝构造函数
      • 1.传统写法
      • 2.现代写法
    • 3.析构函数
    • 4.赋值运算符重载
      • 1.传统写法
      • 2.现代写法
  • 三.遍历和访问
    • 1.operator[]运算符重载
    • 2.iterator迭代器
  • 四.容量相关函数
    • 1.size,capacity
    • 2.reserve
    • 3.resize
  • 五.尾插
    • 1.push_back
    • 2.append
    • 3.operator+=运算符重载
  • 六.指定位置插入删除
    • 1.insert
      • 1.插入一个字符的版本
      • 2. 插入一个字符串的版本
    • 2.erase
  • 七.查找,交换,截取操作
    • 1.find
    • 2.swap
    • 3.substr
  • 八.几个小函数
    • 1.c_str
    • 2.clear
    • 3.empty
  • 九.流插入和流提取
    • 1.<<流插入
    • 2.>>流提取
  • 十.完整代码
    • 1.my_string.h:
    • 2.my_string.cpp
    • 3.test.cpp

一.前置说明

注意:本文会用到strcpy,strstr,strncpy,strlen这几个函数
我会说明它们的功能和用法
如果大家想要彻底了解这几个函数
可以看一下我之前的博客:
征服C语言字符串函数(超详细讲解,干货满满)

1.模拟实现string容器的目的

在这里插入图片描述
比如说leetcode字符串相加这道题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
既然string的模拟实现对我们这么重要
那就让我们一起踏上string的模拟实现之旅吧

2.我们要实现的大致框架

我们经过string容器的使用的学习之后
我们知道string容器其实就是一个顺序表,只不过这个顺序表里面存储的都是char类型的数据
而且末尾有一个隐含的’\0’
因此我们的string容器可以这样定义

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
namespace wzs
{
	class string
	{
	public:
		//npos
		static size_t npos;
	private:
		char* _str;//这就是string容器中的那个字符数组
		int _size;
		int _capacity;
	};
	//static size_t string::npos = -1;//err
	size_t string::npos = -1;//yes
}

下面就是我们要实现的框架

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
namespace wzs
{
	class string
	{
	public:
		//npos
		static size_t npos;
		//1.全缺省的构造函数
		string(const char* str = "");
		~string();
		//2.拷贝构造函数
		string(const string& s);
		//3.赋值运算符重载
		string& operator=(const string& s);
		//4.返回C风格的字符串
		const char* c_str() const;
		//5.iterator
		typedef char* iterator;
		iterator begin();
		iterator end();
		typedef const char* const_iterator;
		const_iterator begin() const;
		const_iterator end() const;
		//6.流插入运算符重载
		//friend ostream& operator<<(ostream& out, const string& s);
		//7.operator[]重载
		char& operator[](int index);
		const char& operator[](int index) const;
		//8.size() capacity()
		const int size() const;
		const int capacity() const;
		//9..reserve:这里不允许缩容
		void reserve(int capacity);
		//10.push_back
		void push_back(const char c);
		//11.append
		void append(const char* str);
		//12.+=
		string& operator+=(const char c);
		string& operator+=(const char* str);
		//13.insert
		void insert(size_t pos, const char c);
		void insert(size_t pos, const char* str);
		//14.erase
		void erase(size_t pos = 0, size_t len = npos);
		//15.resize
		void resize(size_t n, char c = '\0');
		//16.swap
		void swap(string& s);
		//17.find
		size_t find(const string& s, size_t pos = 0) const;
		size_t find(const char c, size_t pos = 0) const;
		//18.substr
		string substr(size_t pos = 0, size_t len = npos)const;
		//19.clear
		void clear();
		//20.empty
		bool empty()const;
		//21.>>
		//friend istream& operator>>(istream& in, string& s);
	private:
		char* _str;
		int _size;
		int _capacity;
	};
	//static size_t string::npos = -1;//err
	size_t string::npos = -1;//yes
	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
}

下面我们就逐个完成这些函数

大家会发现,我这里把流插入和流提取的友元给注释掉了
其实对于string类来说,流插入和流提取是不需要在类内用友元的
因为:
首先,友元是一种突破封装的方式,会增加耦合度,所以并不建议使用
其次,我们使用友元是因为我们想要访问类内的私有数据
可是对于string类来说
它的私有数据:size,capacity,_str都已经对外提供接口了
因此流插入和流提取只需要调用那些接口即可
无需使用友元来访问类内私有数据了

二.默认成员函数

1.构造函数

//1.全缺省的构造函数
string(const char* str = "")
{
	//1.申请空间
	int len = strlen(str);
	_str = new char[len + 1];
	//2.拷贝数据
	strcpy(_str, str);
	//3.修改_size和_capacity
	_size = len;
	_capacity = len;
}

在这里我们实现的是一个全缺省的构造函数
注意几点:
1.strlen求字符串长度时不会算上’\0’,因此我们开空间时要开len+1大小,因为我们还要在末尾存储’\0’
2.这里的

strcpy(_str,str);
会把str字符串中的从首元素开始一直延伸到'\0'为止的数据拷贝给_str
'\0'也会被拷贝进去!!!

在这里插入图片描述
对于这个构造函数:
其实就是
1.申请一个空间
2.拷贝数据
3.修改_size和_capacity

2.拷贝构造函数

1.传统写法

注意:string中的_str是开辟在堆区的
所以我们要实现深拷贝,否则会造成同一空间多次释放等问题

//2.拷贝构造函数
string(const string& s)
{
	//1.申请空间
	int len = s._size;
	_str = new char[len + 1];
	//2.拷贝数据
	strcpy(_str, s._str);
	//3.修改_size和_capacity
	_size = len;
	_capacity = len;
}

跟刚才的构造函数基本相同

2.现代写法

string(const string& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	string tmp(s._str);
	swap(tmp);//this->swap(tmp)
}

使用s._str调用构造函数来构造tmp
然后再去把tmp和*this交换

非常巧妙

3.析构函数

~string()
{
	//释放空间
	delete[] _str;
	_str=nullptr;
	//重置_size和_capacity
	_size = 0;
	_capacity = 0;
}

1.释放空间
2.重置_size和_capacity

4.赋值运算符重载

1.传统写法

//3.赋值运算符重载
string& operator=(const string& s)
{
	if (this != &s)//这里是为了防止自己给自己拷贝的冗余操作
	{
		//1.开辟一块新空间
		int len = s._size;
		char* tmp = new char[len + 1];
		//2.拷贝数据  把s._str的数据拷贝给tmp
		strcpy(tmp, s._str);
		//3.释放原有空间
		delete[] _str;
		_str = tmp;
		tmp = nullptr;
		//4.修改_size和_capacity
		_size = len;
		_capacity = len;
	}
	return *this;
}

其实就是
1.开辟一块新空间
2.拷贝数据
3.释放原有空间
4.修改_size和_capacity

2.现代写法

//现代写法1
string& operator = (const string& s)
{
	if (this != &s)
	{
		//调用拷贝构造函数,然后交换
		string tmp(s);
		swap(tmp);
	}
	return *this;
}
//现代写法2
//直接传值传参,然后交换
string& operator = (string s)
{
	swap(s);
	return *this;
}

三.遍历和访问

1.operator[]运算符重载

//7.operator[]重载
//同STL库中的operator[]重载:有两个重载版本
//一个没有被const修饰
//另一个是被const修饰的
char& operator[](int index)
{
	//STL中的string允许[]访问最后的'\0'
	assert(index >= 0 && index <= _size);
	return _str[index];
}
const char& operator[](int index) const
{
	//STL中的string允许[]访问最后的'\0'
	assert(index >= 0 && index <= _size);
	return _str[index];
}

就是直接返回对应位置的引用的即可

2.iterator迭代器

我们在C++ 带你吃透string容器的使用末尾提到了iterator迭代器
这里就不赘述了

同STL库,iterator有两个版本:const和非const
//5.iterator
typedef char* iterator;
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{
	return _str;
}
const_iterator end() const
{
	return _str + _size;
}

四.容量相关函数

1.size,capacity

因为size和capacity对于类外是只读属性,因此用const修饰

//8.size() capacity()
const int size() const
{
	return _size;
}
const int capacity() const
{
	return _capacity;
}

2.reserve

我们在这里实现的版本是不允许缩容的
能不能缩容取决于编译器的具体实现
这一点我们这篇博客: C++ 带你吃透string容器的使用也提到过

//9.reserve:这里不允许缩容
void reserve(int capacity)
{
	//只有需要扩容时才会进行扩容
	if (_capacity < capacity)
	{
		//1.开辟新空间
		char* tmp = new char[capacity + 1];
		//2.将原有数据拷贝至新空间
		strcpy(tmp, _str);
		//3.释放原有空间
		delete[] _str;
		//4.将_str指向新空间
		_str = tmp;
		tmp = nullptr;
		//5.修改_capacity
		_capacity = capacity;
	}
}

1.开辟新空间
2.将原有数据拷贝至新空间
3.释放原有空间
4.将_str指向新空间
5.修改_capacity

3.resize

这里我复用了erase和push_back
大家可以最后再来看这个resize,因为这个resize不会再其他接口中调用,最后看也无妨

//15.resize
//这里给字符设置了缺省参数'\0',将两个版本合二为一
void resize(size_t n, char c = '\0')
{
	//1.n<_size  :  删除多余字符,修改_size,但不修改_capacity
	if (n < _size)
	{
		erase(n);//erase负责修改_size
	}
	//2._size<=n<=_capacity  :  尾插字符c直到_size == n
	else if (n <= _capacity)
	{
		//尾插数据
		while (_size < n)
		{
			push_back(c);//push_back负责修改_size
		}
	}
	//3.n>_capacity:  需要reserve
	else
	{
		//扩容:将容量扩为n
		reserve(n);//reserve负责修改_capacity
		//尾插数据
		while (_size < n)
		{
			push_back(c);//push_back负责修改_size
		}
	}
}

分为三种情况:
在这里插入图片描述

五.尾插

1.push_back

//10.push_back
void push_back(const char c)
{
	//1.容量不够了的话:扩容
	if(_capacity==_size)
	{
		int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}
	//2.尾插
	_str[_size] = c;
	//3.修改_size
	_size++;
	//4.不要忘了末尾的'\0'
	_str[_size] = '\0';
}

1.容量不够了的话:扩容
2.尾插
3.修改_size
4.不要忘了末尾的’\0’

2.append

//11.append
void append(const char* str)
{
	//1.容量不够的话: 扩容
	int len = strlen(str);
	int newcapacity = _size + len;
	//如果容量够,reserve不会执行任何语句(具体原因请看reserve函数)
	reserve(newcapacity);
	//2.拷贝数据
	strcpy(_str + _size, str);
	//3.修改_size
	_size += len;
}

1.容量不够的话: 扩容
2.拷贝数据
3.修改_size

注意:
_str+_size就指向了_str的末尾(‘\0’)
然后strcpy从_str末尾(‘\0’)开始进行拷贝

问题来了:
能不能用strcat呢?
不建议用
因为strcat使用时会先遍历目标字符串找到’\0’
然后再去进行拷贝
这样的话就多了一个遍历目标字符串的消耗
而我们直接_str+_size就找到目标字符串末尾(‘\0’)了

可以看出:当我们掌握了一些库函数的底层实现之后就是可以优化掉很多不必要的消耗

3.operator+=运算符重载

直接复用push_back和append即可
也是两个版本

//12.+=
string& operator+=(const char c)
{
	push_back(c);
	return *this;
}
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

六.指定位置插入删除

1.insert

1.插入一个字符的版本

注意:下面就有坑了
大家看一下下面这个代码有什么问题?
提示:
1.while循环位置的错误
2.而且这个错误只有当我进行头插的时候才会出现

void insert(size_t pos, const char c)
{
	assert(pos >= 0 && pos <= _size);
	//1.容量不够的话要扩容
	if (_capacity == _size)
	{
		int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}
	//2.将[pos,_size]的数据往后挪动
	//这里把_size位置的数据(也就是'\0')也往后挪动是为了
	//让我们最后的时候就不用单独再去在末尾添加'\0'了
	//因为'\0'已经被我们挪动到最后了
	size_t end = _size;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
	//3.在pos位置插入字符c
	_str[pos]=c;
	//4.修改_size
	_size++;
}

1.容量不够的话要扩容
2.将[pos,_size]的数据往后挪动
3.在pos位置插入字符c
4.修改_size

下面我们来调试看一下
在这里插入图片描述
一开始时大家可以重点看_str的变化(理解数据后移的过程)
等到end快要减为0时再去看end的变化
在这里插入图片描述
因为end是size_t类型(也就是无符号整数)
永远大于等于0
当pos==0时,while(end>=pos)永远都成立
因此陷入死循环

那么怎么办呢?
第一种解决方案:强制类型转换

//把[pos,_size]的数据右移
//解决方案1:强制类型转换
size_t end = _size;
while ((int)end >= (int)pos)
{
	_str[end + 1] = _str[end];
	end--;
}

此时insert就正常运行了
在这里插入图片描述
尽管我end还是会减为-1时变为42亿多
但是while循环中我end强制类型转换转为了int类型(其实是产生了int类型的临时变量,然后用临时变量去比较的)
所以真实的比较场景是while(-1>=0),此时就退出while循环了

不过还有一个小点:
如果我这样写呢? 能不能顺利运行呢?

int end = _size;
while (end >= pos)
{
	_str[end + 1] = _str[end];
	end--;
}

在这里插入图片描述
答案是:还是不行
因为int类型的end跟size_t类型的pos比较时
int类型会进行整形提升产生size_t类型的临时变量,然后用临时变量跟pos去比较
也就是size_t类型的-1,-2,-3…跟size_t类型的pos去比
我们都知道size_t类型的-1,-2,-3那可是无符号整型的前几个最大值啊
肯定比我这个pos大嘛
所以还是死循环

只能强制类型转换吗?
当然不是,条条大路通罗马
还可以这样挪动数据

//13.insert
void insert(size_t pos, const char c)
{
	assert(pos >= 0 && pos <= _size);
	if (_capacity == _size)
	{
		int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}
	//把[pos,_size]的数据右移
	//解决方案2
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = c;
	_size++;
}

此时当end减为0时就已经挪动完数据了
并且while循环也可以正常退出
在这里插入图片描述
我们在这里就采用第二种解决方案啦
大家也可以用一下第一种解决方案

2. 插入一个字符串的版本

跟插入字符的操作差不多
只不过挪动的步长不是1而是字符串的长度

void insert(size_t pos, const char* str)
{
	assert(pos >= 0 && pos <= _size);
	//1.容量不够的话要扩容
	int len = strlen(str);
	int newcapacity = _size + len;
	reserve(newcapacity);
	//2.将[pos,_size]的数据往后挪动len个位置
	int end = _size + 1;
	while (end > pos)
	{
		_str[end + len - 1] = _str[end - 1];
		end--;
	}
	//3.在pos位置插入字符串的前len个字符(也就是不插入'\0')
	strncpy(_str + pos, str, len);//使用strncpy,而不能使用strcpy
	//因为我们不要拷贝'\0'
	//而strcpy是拷贝到'\0'才结束
	//4.修改_size
	_size += len;
}

1.容量不够的话要扩容
2.将[pos,_size]的数据往后挪动len个位置
3.在pos位置插入字符串的前len个字符(也就是不插入’\0’)
4.修改_size

strncpy就是可以指定拷贝n个字符
而不是跟strcpy一样直接拷贝到’\0’
在这里插入图片描述

2.erase

//14.erase
void erase(size_t pos = 0, size_t len = npos)
{
	assert(pos >= 0 && pos <= _size);
	//1.判断要删除的长度和[pos,_size)长度的大小
	//要删除的长度大于等于[pos,_size)的长度
	//此时直接将pos位置置为'\0'并且修改_size即可
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	//要删除的长度小于[pos,_size)的长度
	//那么把[pos+len,_size]的数据往前移动len位置来覆盖我们要删除的字符
	//然后修改_size即可
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

1.判断要删除的长度和[pos,_size)长度的大小
2.如果要删除的长度大于等于[pos,_size)的长度
那么直接将pos位置置为’\0’并且修改_size即可
3.如果要删除的长度小于[pos,_size)的长度
那么把[pos+len,_size]的数据往前移动len位置来覆盖我们要删除的字符
并且修改_size即可

注意:这里if语句中要使用len>=_size-pos
而不能使用len+pos>=_size
因为如果len为npos的话
那么len+pos就会溢出
会出现一些意想不到的错误
比如这样:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后的结果:
在这里插入图片描述
我们注意到len+pos溢出之后变为了1
而我们的pos是2
因此:
strcpy(_str + pos, _str + pos + len);
就是把1位置的数据往2位置去拷贝,直到遇到’\0’为止
这就搞得非常荒诞了,因此这里使用的是len>=_size-pos

使用len>=_size-pos的话
因为len本身是npos
所以就会执行if语句的那个分支,而不是进入else执行strcpy导致出现千奇百怪的错误
在这里插入图片描述

七.查找,交换,截取操作

1.find

跟STL库一样,我们实现两个版本
查找字符串和查找字符

//17.find
size_t find(const char c, size_t pos = 0) const
{
	assert(pos >= 0 && pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return npos;
}

这个查找字符就很简单了,遍历一遍即可

但是这个查找字符串我们要用到strstr字符串匹配函数
在这里插入图片描述

size_t find(const string& s, size_t pos = 0) const
{
	assert(pos >= 0 && pos < _size);
	char* index = strstr(_str + pos, s._str);
	if (index == nullptr)
	{
		return npos;
	}
	else
	{
		return index - _str;
	}
}

唯一需要注意的一点就是:
返回空指针的话代表没有查找到,需要加一个if判断
如果查找到了
返回index-_str即可(指针-指针的问题,是C语言的基础知识)

2.swap

//16.swap
void swap(string& s)
{
	//这里调用的是std(标准库中的那个swap函数)
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

这里的swap的实现其实就是
交换指针,交换_size和_capacity
这样做的好处是无需再去拷贝string了,而是直接交换指针即可,大大优化了swap的效率
可见这个设计是非常巧妙的

3.substr

大家看一下这个代码有没有什么问题

string& substr(size_t pos = 0, size_t len = npos)const
{
	assert(pos >= 0 && pos < _size);
	string s;
	//1.判断要截取的长度和[pos,_size)长度的大小
	//要截取的长度大于[pos,_size)的长度  ->  那么修改len方便后续形成子串
	if (len >= _size - pos)
	{
		len = _size - pos;
	}
	//2.插入数据形成子串
	for (size_t i = 0; i < len; i++)
	{
		s += _str[i + pos];
	}
	//3.返回子串
	return s;
}

1.判断要截取的长度和[pos,_size)长度的大小 如果大了,那么就修改len
2.插入数据形成子串
3.返回子串

其实这个代码是有问题的,只不过并不是我们实现逻辑的问题
而是这个引用做返回值的问题
在这里插入图片描述
在这里插入图片描述

因为我们的这个s串是substr函数中的局部变量
出了作用域后会自动销毁(调用析构函数)
但是因为我们是引用作为返回值
因此s1.substr(1,7)这个对象就是我们刚才析构后的s串
然后用这个已经析构了的串给s2进行拷贝构造
在这里插入图片描述
于是拷贝构造时
strcpy访问s._str导致出现了对空指针的解引用操作
因此报错

怎么办呢?
把引用做返回值改为值做返回值
就好了
但是好的前提一定是你的拷贝构造,赋值运算符重载都是用的深拷贝,而不是浅拷贝
否则就会出现同一空间多次释放的错误
在这里插入图片描述
下面我把拷贝构造函数给干掉
那么就会用编译器默认形成的拷贝构造函数(默认形成的是浅拷贝)
在这里插入图片描述
此时,尽管你是值拷贝,但是你是浅拷贝
也就是说返回是我指向已经析构后的空间的那个指针
因此在s2析构的时候会对那个空间再次进行析构

也就是发生了同一空间多次释放的问题
也就是野指针的问题

下面我把拷贝构造函数恢复

string substr(size_t pos = 0, size_t len = npos)const
{
	assert(pos >= 0 && pos < _size);
	string s;
	//1.判断要截取的长度和[pos,_size)长度的大小
	//要截取的长度大于[pos,_size)的长度  ->  那么修改len方便后续形成子串
	if (len >= _size - pos)
	{
		len = _size - pos;
	}
	//2.插入数据形成子串
	for (size_t i = 0; i < len; i++)
	{
		s += _str[i + pos];
	}
	//3.返回子串
	return s;
}

此时就正常运行了
在这里插入图片描述
整个流程是:
s串返回值进行拷贝构造形成临时变量
然后返回那个临时变量
然后再由那个临时变量给这个s2进行拷贝构造

关于引用的知识点:
C++入门-引用
深浅拷贝的知识点
C++类和对象中(构造函数,析构函数,拷贝构造函数)详解

八.几个小函数

1.c_str

//4.返回C风格的字符串
const char* c_str() const
{
	return _str;
}

2.clear

//19.clear
void clear()
{
	_str[0] = '\0';//关于这里为什么要把第一个字符置为'\0'
	//是因为我们要保证
	//此时即使使用cout<<c_str()来打印string对象时也不会跟使用流插入打印string对象产生歧义
	//因为使用cout<<c_str()是打印到'\0'才会停止
	//而流插入是按照size来打印的
	_size = 0;
}

3.empty

//20.empty
bool empty()const
{
	return _size == 0;
}

九.流插入和流提取

对于流插入和流提取我们之前就在日期类接触了。
不能重载成成员,因为会和this指针抢占第一个位置。
所以需要定义成全局的
不过无需使用友元,因为可以调用接口来访问类内数据

1.<<流插入

按照_size打印

ostream& operator<<(ostream& out, const string& s)
{
	for (int i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}

对于<<和c_str()的区别:<<按照size进行打印,跟\0没有关系,而c_str()遇到\0结束
在这里插入图片描述
因此推荐使用<<

2.>>流提取

cin和scanf一样,都是读到’ ‘或者’\n’后就会停止
因此我们需要一个字符一个字符地读取

所以使用get()函数来读取字符
第一个版本:

istream& operator>>(istream& in, string& s)
{
//注意:使用流提取时,会覆盖原有数据
//因此需要先clear一下
	s.clear();
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

但是第一个版本有一个缺陷:
当我们想要读取的数据特别特别多的时候
就会频繁扩容,造成不必要的麻烦
因此我们可以设置一个"缓冲区"似的数组
第二个版本:

istream& operator>>(istream& in, string& s)
{
	s.clear();
	char tmp[128] = { '\0' };
	char ch = in.get();
	size_t index = 0;
	while (ch != ' ' && ch != '\n')
	{
		if(index == 127)
		{
			s += tmp;
			index = 0;
		}
		tmp[index++] = ch;
		ch = in.get();
	}
	if (index >= 0)
	{
		tmp[index] = '\0';
		s += tmp;
	}
	return in;
}

十.完整代码

1.my_string.h:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <string>
#include <iostream>
using namespace std;
#include <assert.h>
//小函数定义在.h里面,弄成内联函数
namespace wzs
{
	class string
	{
	public:
		//npos
		static size_t npos;
		//1.全缺省的构造函数
		string(const char* str = "");
		~string();
		//2.拷贝构造函数
		string(const string& s);
		//18.substr
		string substr(size_t pos = 0, size_t len = npos)const;
		//3.赋值运算符重载
		string& operator=(string s);
		//4.返回C风格的字符串
		const char* c_str() const
		{
			return _str;
		}
		//5.iterator
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		typedef const char* const_iterator;
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}
		//6.流插入运算符重载
		//friend ostream& operator<<(ostream& out, const string& s);
		//7.operator[]重载
		char& operator[](int index);
		const char& operator[](int index) const;
		//8.size() capacity()
		const int size() const
		{
			return _size;
		}
		const int capacity() const
		{
			return _capacity;
		}
		//9..reserve:这里不允许缩容
		void reserve(int capacity);
		//10.push_back
		void push_back(const char c);
		//11.append
		void append(const char* str);
		//12.+=
		string& operator+=(const char c);
		string& operator+=(const char* str);
		//13.insert
		void insert(size_t pos, const char c);
		void insert(size_t pos, const char* str);
		//14.erase
		void erase(size_t pos = 0, size_t len = npos);
		//15.resize
		void resize(size_t n, char c = '\0');
		//16.swap
		void swap(string& s);
		//17.find
		size_t find(const string& s, size_t pos = 0) const;
		size_t find(const char c, size_t pos = 0) const;
		//19.clear
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
		//20.empty
		bool empty()const
		{
			return _size == 0;
		}
		//21.>>
		//friend istream& operator>>(istream& in, string& s);
	private:
		char* _str;
		int _size;
		int _capacity;
	};
	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
	void test1();
	void test2();
	void test3();
	void test4();
	void test5();
	void test6();
	void test7();
	void test8();
	void test9();
	void test10();
	void test11();
	void test12();
	void test13();
	void test14();
	void test15();
	void test16();
	void test17();
}

2.my_string.cpp

#include "my_string.h"
namespace wzs
{
		//1.全缺省的构造函数
		string::string(const char* str)
		{
			int len = strlen(str);
			_str = new char[len + 1];
			strcpy(_str, str);
			_size = len;
			_capacity = len;
		}
		string::~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
		//2.拷贝构造函数
		/*
		string(const string& s)
		{
			int len = s._size;
			_str = new char[len + 1];
			strcpy(_str, s._str);
			_size = len;
			_capacity = len;
		}
		*/
		//拷贝构造的现代写法
		//有些编译器上有问题
		//因为如果_str没有被初始化为nullptr
		//那么就完了,因为tmp除了这个构造函数就会调用析构
		//所以我们可以先给缺省值或者初始化列表来让这个_str初始化为nullptr
		string::string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			//先构造
			string tmp(s.c_str());
			//再交换
			swap(tmp);
		}
		//18.substr
		string string::substr(size_t pos, size_t len)const
		{
			assert(pos >= 0 && pos < _size);
			string s;
			if (len >= _size - pos)
			{
				len = _size - pos;
			}
			for (size_t i = 0; i < len; i++)
			{
				s += _str[i + pos];
			}
			return s;
		}
		//3.赋值运算符重载
		/*
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				int len = s._size;
				char* tmp = new char[len + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				tmp = nullptr;
				_size = len;
				_capacity = len;
			}
			return *this;
		}
		*/
		//赋值运算符重载的现代写法1
		/*
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				//先拷贝构造
				string tmp(s);
				//再交换
				swap(tmp);
				return *this;
			}
		}
		*/
		//赋值运算符重载的现代写法2
		//直接传值调用
		//传参的时候值传参  ->  调用拷贝构造函数传参
		string& string::operator=(string s)
		{
			//没有必要判断是否是自己给自己赋值
			//因为这个对象已经拷贝出来了,覆水已经难收
			swap(s);
			//*this原来的空间是这个形参s释放的
			return *this;
		}
		//7.operator[]重载
		char& string::operator[](int index)
		{
			//stl中的string允许[]访问最后的'\0'
			assert(index >= 0 && index <= _size);
			return _str[index];
		}
		const char& string::operator[](int index) const
		{
			//stl中的string允许[]访问最后的'\0'
			assert(index >= 0 && index <= _size);
			return _str[index];
		}
		//9..reserve:这里不允许缩容
		void string::reserve(int capacity)
		{
			if (_capacity < capacity)
			{
				char* tmp = new char[capacity + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				tmp = nullptr;
				_capacity = capacity;
			}
		}
		//10.push_back
		void string::push_back(const char c)
		{
			if (_capacity == _size)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = c;
			_size++;
			_str[_size] = '\0';
		}
		//11.append
		void string::append(const char* str)
		{
			int len = strlen(str);
			int newcapacity = _size + len;
			reserve(newcapacity);
			strcpy(_str + _size, str);
			_size += len;
		}
		//12.+=
		string& string::operator+=(const char c)
		{
			push_back(c);
			return *this;
		}
		string& string::operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		//13.insert
		void string::insert(size_t pos, const char c)
		{
			assert(pos >= 0 && pos <= _size);
			if (_capacity == _size)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			//把[pos,_size]的数据右移
			//size_t end = _size;
			//while (end >= pos)
			//{
			//	_str[end + 1] = _str[end];
			//	end--;
			//}
			//解决方案1:强制类型转换
			//int end = _size;
			//while (end >= pos)
			//{
			//	_str[end + 1] = _str[end];
			//	end--;
			//}

			//解决方案2
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}

			_str[pos] = c;
			_size++;
		}
		void string::insert(size_t pos, const char* str)
		{
			assert(pos >= 0 && pos <= _size);
			int len = strlen(str);
			int newcapacity = _size + len;
			reserve(newcapacity);
			//[pos,_size]的数据右移len位
			int end = _size + 1;
			while (end > pos)
			{
				_str[end + len - 1] = _str[end - 1];
				end--;
			}
			strncpy(_str + pos, str, len);//使用strncpy,而不能使用strcpy
			//因为我们不要拷贝'\0'
			//而strcpy是拷贝到'\0'才结束
			_size += len;
		}
		//14.erase
		void string::erase(size_t pos, size_t len)
		{
			assert(pos >= 0 && pos <= _size);
			//要删除的长度大于[pos,_size)的长度
			if (len >= _size - pos)
				//if (len + pos >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			//把[pos+len,_size]的数据往前移动len位置
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}
		//15.resize
		void string::resize(size_t n, char c)
		{
			//1.n<_size  :  删除多余字符,修改_size,但不修改_capacity
			if (n < _size)
			{
				erase(n);//erase负责修改_size
			}
			//2._size<=n<=_capacity  :  尾插字符c直到_size == n
			else if (n <= _capacity)
			{
				while (_size < n)
				{
					push_back(c);//push_back负责修改_size
				}
			}
			//3.n>_capacity:  需要reserve
			else
			{
				reserve(n);//reserve负责修改_capacity
				while (_size < n)
				{
					push_back(c);//push_back负责修改_size
				}
			}
		}
		//16.swap
		void string::swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//17.find
		size_t string::find(const string& s, size_t pos) const
		{
			assert(pos >= 0 && pos < _size);
			char* index = strstr(_str + pos, s._str);
			if (index == nullptr)
			{
				return npos;
			}
			else
			{
				return index - _str;
			}
		}
		size_t string::find(const char c, size_t pos) const
		{
			assert(pos >= 0 && pos < _size);
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == c)
				{
					return i;
				}
			}
			return npos;
		}
	//static size_t string::npos = -1;//err
	size_t string::npos = -1;//yes
	ostream& operator<<(ostream& out, const string& s)
	{
		for (int i = 0; i < s.size(); i++)
		{
			out << s[i];
		}
		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		//库里的流提取输入的时候会覆盖之前的旧内容!!
		//运行看一下
		//因此要clear
		s.clear();//clear一般不会释放空间,就是把第一个元素置为'\0'并且把size置为0
		//如果clear不把第一个元素置为'\0'
		//那么>>和c_str就不一样了!!!!
		//因为>>是按size打印,c_str打印到'\0'之前为止
		char tmp[128] = { '\0' };
		//get是istream类型调用的成员函数
		char ch = in.get();
		//get()是C++的,getchar()是C语言的,C++和C语言的缓冲区是不一样的,(C++是可以设计成不兼容C语言的)
		//
		//尽管C++是兼容C语言的,不过尽量还是用C++的
		size_t index = 0;
		//getline其实是当ch遇到'\n'之后才会停止while循环!!!!
		while (ch != ' ' && ch != '\n')
		{
			//index是下一个要插入数据的下标
			//注意:这里是127时就要放s里面并清空
			//因为tmp的最后一个数据一定要保证是'\0'!!!!!!
			if (index == 127)
			{
				s += tmp;
				index = 0;
			}
			tmp[index++] = ch;
			ch = in.get();
		}
		if (index >= 0)
		{
			tmp[index] = '\0';
			s += tmp;
		}
		return in;
	}
}

3.test.cpp

#include "my_string.h"
namespace wzs
{
	void test1()
	{
		const string s1("hello world");
		string s2;
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		string s3(s1);
		cout << s3.c_str() << endl;
		s2 = s1;
		cout << s2.c_str() << endl;
	}
	void test2()
	{
		//string s2 = "hello iterator";
		//string::iterator it = s2.begin();
		//while (it != s2.end())//注意:我们使用iterator访问和遍历时要注意左闭右开使用[begin,end)
		//{
		//	cout << *it << " ";//这里可以暂时理解为像是指针解引用的用法一样
		//	it++;//这里可以暂时理解为像是指针自增(也就是后移)的用法一样
		//}
		//cout << endl;
		//cout << s2 << endl;
		string s2 = "hello iterator";
		string::iterator it = s2.begin();
		while (it != s2.end())//注意:我们使用iterator访问和遍历时要注意左闭右开使用[begin,end)
		{
			*it += 1;//(*it)++;这样也可以,不过不要忘了加小括号(运算符优先级的问题)
			cout << *it << " ";//这里可以暂时理解为像是指针解引用的用法一样
			it++;//这里可以暂时理解为像是指针自增(也就是后移)的用法一样
		}
		cout << endl;
		cout << s2 << endl;


	}
	void test3()
	{
		std::string s("hello world");
		char c = s[s.size()];

		cout << s[s.size()] << endl;
	}
	void test4()
	{
		string s1("hello world");
		s1.reserve(100);

	}
	void test5()
	{
		string s1;
		int old_capacity = s1.capacity();
		cout << old_capacity << endl;
		cout << s1 << endl;
		for (int i = 0; i < 100; i++)
		{
			s1.push_back('w');//将'w'这个字符尾插进入s1当中
			if (old_capacity != s1.capacity())
			{
				cout << s1.capacity() << endl;
				old_capacity = s1.capacity();
			}
		}
		cout << s1 << endl;
	}
	void test6()
	{
		string s("hello world");
		/*s.push_back('2');
		s.push_back('3');
		s.append(" 1124");*/
		s += "2";
		s += '3';
		s += " 1124";
		cout << s << endl;
	}
	void test7()
	{
		string s("[hello world]");
		s.insert(0, 'w');//从0位置头插一个字符:'w'
		cout << s << endl;
	}
	void test8()
	{
		string s("0123456789");
		s.erase(3, 4);//从3号下标位置开始删除4个字符,也就是删除了3456
		cout << s << endl;
		s.erase(2);//默认从2号下标开始的删除所有字符
		cout << s << endl;
		s.erase();//默认删除所有字符
		cout << s << endl;
	}
	void test9()
	{
		//string s1("hello world");
		1.n<size
		//s1.resize(4);
		//cout << s1 << endl;
		//string s1("hello world");
		//2.size<n<capacity
		//cout << s1.capacity() << endl;
		s1.resize(13,'q');
		//s1.resize(30, '2');
		//cout << s1;

		string s1;
		s1.resize(10, 'w');
		cout << s1 << endl;
	}
	void test10()
	{
		string s1("123");
		string s2("456");
		s1.swap(s2);
		cout << s1 << endl;
		cout << s2 << endl;
	}
	void test11()
	{
		string s1("abcd 1234 xxxx");
		size_t index1 = s1.find("cd", 1);//从1号下标开始查找字符串cd
		cout << index1 << endl;
		size_t index3 = s1.find(' ', 1);//从1号下标开始查找字符' '
		cout << index3 << endl;

		//查不到的情况:
		size_t index4 = s1.find("abcd", 1);//从1号下标开始查找字符串abcd  ->  查不到,返回npos
		cout << index4 << endl;//无符号整形最大值

	}
	void test12()
	{
		string s1("hello world");
		string s2 = s1.substr(1, 7);
		cout << s2 << endl;
	}
	void test13()
	{
		string s("http://www.baidu.com/index.html?name=mo&age=25#dowell");
		string substr1, substr2, substr3;
		//我们的目标是:
		//substr1:http
		//substr2:www.baidu.com
		//substr3:index.html?name=mo&age=25#dowell
		size_t pos1 = s.find(':', 0);//从0下标开始出发查找':'
		substr1 = s.substr(0, pos1 - 0);//[0,pos1):左闭右开的区间:长度是右区间-左区间 也就是pos1-0
		size_t pos2 = s.find('/', pos1 + 3);//pos1位置此时是':'  我们下一次要从pos1+3的位置开始查找 也就是第一个'w'的位置
		substr2 = s.substr(pos1 + 3, pos2 - (pos1 + 3));//[pos1+3,pos2):这个区间内的子串
		substr3 = s.substr(pos2 + 1);//从pos2+1开始一直截取到最后即可
		//substr传值返回,是临时变量,临时变量具有常性
		cout << substr1 << endl;
		cout << substr2 << endl;
		cout << substr3 << endl;

	}
	void test14()
	{
		//char ch1, ch2;
		//cin >> ch1 >> ch2;
		//因为输入多个值时,' '和'\n'默认是分割符,不是有效数据!!!!!!
		//scanf也是拿不到的
		string s1, s2, s3;
		cin >> s1 >> s2 >> s3;
		cout << s1 << endl << s2 << endl << s3 << endl;
	}
	void test15()
	{
		string s1("012345");
		cout << s1 << endl;
		cout << s1.c_str() << endl;

		s1.insert(2, '\0');
		cout << s1 << endl;
		cout << s1.c_str() << endl;
	}
	void test16()
	{
		string s1("hello world");
		cout << s1 << endl;
		cout << s1.c_str() << endl;
		//此时clear没有把第一个元素置为'\0'
		s1.clear();
		cout << s1 << endl;
		cout << s1.c_str() << endl;
	}

	//整形转字符串:to_string
	//stoi:字符串转整形
	void test17()
	{
		//整形转字符串:to_string
		std::string s = std::to_string(123);
		//stoi:字符串转整形
		int i = stoi(s);
		cout << s << endl;
		cout << i << endl;
	}
}

以上就是C++: string的模拟实现的全部内容,希望能对大家有所帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/204879.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue3+Vite】解决build后空白页的问题

目录 Hash 模式 HTML5 模式&#xff08;历史模式&#xff09; 配置Nginx 配置Spring Boot Hash 模式 build后空白页的问题可能是使用的是历史模式&#xff0c;因为Vue是一个单页的客户端应用&#xff0c;如果没有适当的服务器配置&#xff0c;访问会得到一个 404 错误…

智慧水务系统在流域水环境规划中起到什么作用?

智慧水务系统在流域水环境规划中扮演着越来越重要的角色。作为一款集信息化、自动化、智能化、智慧化于一体的水务管理系统&#xff0c;智慧水务系统不仅能够提高水环境规划的效率&#xff0c;还能为水资源的保护和利用提供有力支持。 在流域水环境规划中&#xff0c;智慧水务系…

计算机硬件(一)

1.机箱 计算机的许多硬件,如主板,硬盘和电源等,都安放在固定机箱中。机箱是一个相对封闭的空间&#xff0c;箱体一般由钢和铝合金等金属制成(其他材料亦可用&#xff0c;但不多见),同时设有许多通风口,以促进箱内空气流动,防止内部温度过高&#xff0c;机箱的颜色,大小乃至形状…

c++ 中名空间中using 引入的细节

如果在引入名空间中的特定成员函数的时候&#xff0c; 全局不能定义同名的函数&#xff0c;但是其实只要参数不同就行 namespace a{int x 1;int fun(){return 0;} }using namespace a; using a::fun;void fun(int x) {} int x 10; int main() {fun(10); } 上面就是一个正确…

TZOJ 1378 发工资咯

答案&#xff1a; #include<stdio.h> int main() {int n 0, m 0, i 0, sum 0;while (scanf("%d", &n) && n ! 0) //多组数据输入并且不等于0{for (i 0; i < n; i) //有n名老师就循环n次{scanf("%d", &m); //该名老…

rabbitMQ对消息不可达处理-备份交换机/备份队列

生产者发送消息&#xff0c;在消息不可达指定队列时&#xff0c;可以借助扇出类型交换机&#xff08;之前写过消息回退的处理方案&#xff0c;扇出交换机处理的方案优先级高于消息回退&#xff09;处理不可达消息&#xff0c;然后放置一个备份队列&#xff0c;供消费者处理不可…

解压压缩包脚本

解压压缩包脚本 我们一般解压一个压缩包&#xff0c;需要三步&#xff1a; 1、打开压缩包。 2、点击解压文件。 3、选择解压目录解压到指定文件夹。 那么&#xff0c;怎么一步完成这些繁琐的操作呢&#xff1f;编写一个bat脚本即可&#xff0c;如下所示&#xff1a; 1、安装解压…

【人工智能Ⅰ】实验5:AI实验箱应用之贝叶斯

实验5 AI实验箱应用之贝叶斯 一、实验目的 1. 用实验箱的摄像头拍摄方块上数字的图片&#xff0c;在图像处理的基础上&#xff0c;应用贝叶斯方法识别图像中的数字并进行分类。 二、实验内容和步骤 1. 应用实验箱机械手臂上的摄像头拍摄图像&#xff1b; 2. Opencv处理图像…

Wish防关联是什么?Wish要怎样避免违规封店?

四大跨境电商平台之一wish&#xff0c;做跨境电商的很多人可能都听过wish。随着wish不断完善平台制度&#xff0c;对于多账号运营的卖家要求越来越严厉&#xff0c;wish和亚马逊、eBay等其它跨境电商平台一样&#xff0c;不支持一个卖家开设多个账号多家店铺。 但是对于各位卖家…

2023/11/24JAVAweb学习(Vue常用指令,Vue.js文件,Ajax,Axios两种请求,Vue-cli脚手架,Vue项目,Element)

age只会执行成立的,show其实都展示了,通过display不展示 使用Vue,必须引入Vue.js文件 假如运行报错,以管理员身份打开vscode,再运行 ------------------------------------------------------------------- 更改端口号

2023/11/30JAVAweb学习

数组json形式 想切换实现类,只需要只在你需要的类上添加 Component 如果在同一层,可以更改扫描范围,但是不推荐这种方法 注入时存在多个同类型bean解决方式

C语言——多种方式打印出1000之内的所有的“水仙花数”

所谓水仙花数,是指一个3位数,其各位数字立方和等于该数本身。水仙花数是指一个三位数&#xff0c;它的每个位上的数字的立方和等于它本身。例如&#xff0c;153是一个水仙花数&#xff0c;因为1^3 5^3 3^3 153。 方法一 #define _CRT_SECURE_NO_WARNINGS 1#include <std…

制造业如何做生产设备管理、分析生产数据?

本文将为大家讲解&#xff1a;1、设备管理的现状与问题&#xff1b;2、设备管理系统功能&#xff1b;3、制造业企业如何做生产设备管理、分析生产数据&#xff1f;4、制造业设备管理的价值。 想要管理好设备&#xff0c;设备档案管理、巡检、报修、保养、分析预警等问题都是必须…

Python list列表添加元素的3种方法及删除元素的3种方法

Python list列表添加元素的3种方法 Python list 列表增加元素可调用列表的 append() 方法&#xff0c;该方法会把传入的参数追加到列表的最后面。 append() 方法既可接收单个值&#xff0c;也可接收元组、列表等&#xff0c;但该方法只是把元组、列表当成单个元素&#xff0c;这…

解决电脑蓝屏问题:SYSTEM_THREAD_EXCEPTION_NOT_HANDLED,回到系统还原点

解决电脑蓝屏问题&#xff1a;SYSTEM_THREAD_EXCEPTION_NOT_HANDLED&#xff0c;回到系统还原点 1&#xff0c;蓝屏显示问题1.1&#xff0c;蓝屏1&#xff0c;清楚显示1.2&#xff0c;蓝屏2&#xff0c;模糊显示 2&#xff0c;排除故障问题3&#xff0c;解决蓝屏的有效方法 1&a…

本地事务和分布式事务

请直接看原文 原文链接:彻底搞清楚什么是分布式事务 - 知乎 (zhihu.com) -------------------------------------------------------------------------------------------------------------------------------- 1、什么是本地事务 多个sql操作,被同一个线程执行, 使用…

MedicalTransformer论文解读

论文是一个分割任务&#xff0c;但这里的方法不局限于分割&#xff0c;运用到检测、分类都可以。 论文下载 https://www.yuque.com/yuqueyonghupjh9oc/ovceh4/onilw42ux6e9n1ne?singleDoc# 《轴注意力机制》 一个问题 为什么transformer一开始都有CNN&#xff1a;降低H、W…

AWS EC2 如何 使用 SSM会话管理器登陆

首先只有特定版本的OS会默认附带SSM Agent。 预安装了 SSM Agent 的 Amazon Machine Images&#xff08;AMIs&#xff09; - AWS Systems Manager 其次EC的instance role必须有一个叫“AmazonSSMManagedInstanceCore”的策略 如何给IAM User赋权&#xff0c;让他们可以使用SSM…

教育企业CRM选择技巧

教育行业的发展一波三折&#xff0c;要想在激烈的赛道脱颖而出&#xff0c;就需要有一套有效的CRM系统&#xff0c;来帮助教育机构提升招生效率、增加学员留存、提高教学质量。下面说说&#xff0c;教育企业选择CRM系统要具备的四大功能。 1、招生管理功能 教育机构的首要目标…

Java的threadd常用方法

常用API 给当前线程命名 主线程 package com.itheima.d2;public class ThreadTest1 {public static void main(String[] args) {Thread t1 new MyThread("子线程1");//t1.setName("子线程1");t1.start();System.out.println(t1.getName());//获得子线程…