C++标准模板库(STL)——string的模拟实现

在C++编程中,std::string是最常用的容器之一。你是否好奇过它的底层逻辑,为什么它的操作能如此高效?当我们自己动手写一个功能完备的string类时,该怎样实现呢?

接下来我将模拟string类的一些模块的实现逻辑,希望该文章对你深入学习STL容器有所帮助

1、 整体结构

1) 命名空间

我们需要将模拟实现的string封装在namespace ST中,避免与标准库中的std::string冲突

2) 核心成员对象

  • std::string以动态字符数组储存字符串,因此我们需要声明_str,让该指针指向一段空间,用这段空间来存放字符。
  • 为了兼容C字符串,ST::string字符串的最后一个字符的下一个位置应该放一个字符’\0’,并且’\0’不计入有效字符长度
  • 我们用_size来记录当前字符串中有字符的数量(不包含‘\0‘
  • 我们用_capacity来记录当前开辟的空间大小,可以存放多少个字符(不包含‘\0‘)
/string.h
#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace ST
{
	class string
	{
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

2、 string的迭代器

  • 迭代器的作用是访问,遍历字符串,其功能类似于指针
  • 为什么不直接用char* 指针,非要用iterator?

因为iterator迭代器是C++容器的通用编译工具,iterator可以遍历string、list、vector等等,iterator为访问容器提供了统一的接口,大家都可以使用。如果直接使用char* 遍历,那就只有string能使用,就会和“容器迭代器“的设计思想脱节

  • 为什么不直接用const iterator begin();

因为const iterator指迭代器本身不能修改(指针本身不能修改,但是指针指向的内容还可以修改),我们需要的是,迭代器指向的内容不能修改

  • 为什么begin() 返回_str,end() 返回_str + _size?

遵循 C++ 迭代器的 “左闭右开” 原则[begin(), end())

class string
{
public:
	/char* 类型重命名成iterator,用指针功能来模拟迭代器
	/普通的迭代器,可以访问修改元素
	typedef char* iterator;

	/常量迭代器,只能读取元素
	typedef const char* const_iterator;

	iterator begin()
	{
		return _str;/指向字符串首字符
	}
	iterator end()
	{
		return _str + _size;/指向字符串最后一个字符的下一个位置‘\0}

		
	const_iterator begin()const
	{
		return _str;/const修饰this指针,返回的this->_str不可修改
	}
	const_iterator end()const
	{
		return _str + _size;/返回只读字符串尾元素
	}

3、 构造和析构

1) 构造函数

  • 初始化字符串
  • 为什么要给构造函数加默认参数str = “”

这样实现了一个构造函数覆盖了空字符串和非空字符串两种场景,减少代码冗余

  • 为什么要_size + 1分配内存?

为了兼容C语言的字符串,保证字符串以‘\0’结尾

  • 为什么使用memcpy,而不是strcpy(_str,str)?

strcpy遇到’\0’就会停下来,并不会将’\0’拷贝过去 memcpy 明确了拷贝长度(_size + 1),就会将长度内的字符全拷贝过去

/构造函数
string(const char* str = "")/如果不传参,默认为空字符串
	:_size(strlen(str))/strlen不包含‘\0{	
	_capacity = _size;
	_str = new char[_size + 1];/size+1体现了为‘\0’预留空间
			
	/将str的_size+1个字符串长度,复制到_str
	/为什么不使用strcpy(_str,str);
	memcpy(_str, str, _size + 1);
}

2) 拷贝构造函数

  • 复制一个已经存在的string对象
  • 为什么要自己写swap,而非直接用std::swap?

因为直接使用std::swap会触发三次拷贝构造(a拷贝给临时对象、b拷贝给a、临时对象拷贝给b),效率很低
自定义的swap,只会交换_str(指针)、_size、_capacity三个成员,只进行指针交换,不需要分配新内存、拷贝数据,效率很高

  • 为什么使用新写法的拷贝构造呢?

新写法不用考虑抛异常问题,而且代码简洁,代码的复用逻辑强

/为啥要自己写swap
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

/拷贝构造函数s2(s1)
/深拷贝,避免浅拷贝让两个对象指向同一块空间导致析构两次
/新写法
string(const string& s)
{
	string tmp(s._str);/先用构造函数构造一个临时对象tmp(深拷贝s的内容)
	swap(tmp);/再调用自己写的swap,交换当前对象和tmp的资源
}

/传统写法
/string(const string& s)
/{
/	_str = new char[s._capacity + 1];
/	memcpy(_str, s._str, s._size + 1);
/	_size = s._size;
/	_capacity = s._capacity;
/}

3) 赋值运算符重载

将一个string对象的值,赋值给另一个string对象

/赋值运算符重载
/s2=s1
string& operator=(string tmp)/传值参数,自动调用拷贝构造,深拷贝s1的内容到tmp
{
	swap(tmp);/交换s2和tmp的资源
	return*this;
}

4) 析构函数

释放动态分配的内存,避免内存泄漏

/析构函数
~string()
{
	delete[]_str;
	_str = nullptr;
	_size = _capacity = 0;
}

代码测试

在这里插入图片描述
在这里插入图片描述

4、 大小与容量管理

1) size/reserve

size()
  • 返回当前字符数
  • 为什么返回 _size 而不是计算 strlen (_str) ?

因为_size是类成员。表示字符串的实际字符个数(不含‘\0’),修改字符串时,_size也会同步变化,直接获取很方便,且时间复杂度为O(1)
每一次使用 strlen, 都要遍历字符到‘\0’才会停止,时间复杂度为O(n)

  • 为什么返回类型是 size_t ?

size_t是无符号整数类型,专门用来表示大小,长度,只返回大于等于0的数

  • 为什么函数声明后加const?

避免类成员对象被函数乱修改,还能确保const 对象可以被调用 (如 const string s = “hello”; )

//返回当前字符串长度
size_t size()const
{
	return _size;
}

reserve(n)
  • 开辟n大小的空间
  • 为什么需要reserve () ?

一次性将需要的空间开好,避免频繁扩容

  • 为什么不修改 _siz e?

再次强调 _capacity,是已经开辟的空间大小,表示最多能储存的字符数
_size,表示的是当前实际储存的字符数
reserve()的语义是 “预留容量”,不改变实际字符数,因此仅更新_capacity,不改变_size

/提前扩容
void reserve(size_t n)
{
	if (n > _capacity)/当n大于当前容量才扩容
	{
		char* tmp = new char[n + 1];/新内存包含‘\0memcpy(tmp, _str, _size + 1);/拷贝旧字符串,把‘\0’也带上
		delete[]_str;
		_str = tmp;
		_capacity = n;
	}
}

2) 常量npos

npos 用来表示“不存在的位置”或“直到字符串末尾”,添加了 const 防止 npos 被修改
在这里插入图片描述
在这里插入图片描述

5、 元素访问(下标运算符重载)

  • 像数组一样访问字符,并进行越界检查
  • 重载普通引用和const引用,编译器会根据实例化对象自动匹配对应版本
/可以修改访问
char& operator[](size_t i)
{
	assert(i < _size);/避免越界访问
	cout << "char& operator[](size_t i)" << endl;
	return _str[i];/返回字符引用
}

/只读访问
const char& operator[](size_t i)const
{
	assert(i < _size);
	cout << "const char& operator[](size_t i)const" << endl;
	return _str[i];
}

6、 修改操作(增删查改)

1) 尾部操作(尾插尾删)

push_back
  • 尾插一个字符
  • 为什么要先检查容量(_size >= _capacity)?

当_size>=_capacity时,已经没有内存空间来插入数据了,为了避免越界插入,所以得检查

/尾插单个字符
void push_back(char ch)
{
	if (_size >= _capacity)/检查容量是否足够
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);/调用reserve扩容
	}
	_str[_size] = ch;/尾插字符
	++_size;/更新长度
	_str[_size] = '\0';/维护'\0'结尾
}

append

尾插字符串

/尾插字符串
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)/检查总长度是否超过容量
	{
		/扩容到“2倍原容量”和“当前长度+待插长度”中的较大值
		size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
		reserve(newcapacity);
	}

	memcpy(_str + _size, str, len + 1);/拷贝含‘\0’
	_size += len;/更新总长度
}
重载+=
  • 推荐使用+=,可以直接+=字符或字符串,操作灵活, 尾插字符/字符串
  • 语法糖复用已有的函数,避免代码冗余,保证了逻辑一致
  • 返回*this,支持了链式操作(s+=’a’+=’b’)
/+=字符
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
/+=字符串
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

pop_back

尾删一个字符

/尾删一个字符
void pop_back()
{
	assert(_size > 0);/检查字符串是否空
	--_size;
	_str[_size] = '\0';
}

2) 插入与删除

insert
  • 在指定pos位置插入字符/字符串
  • 为什么从 end = _size + 1开始 “从后往前” 挪动数据?

从后往前挪动数据,可以避免该字符被覆盖,避免数据丢失

_size+1是‘\0’的下一个位置,先把_str[_size]位置的’\0’,挪到_str[_size+1]的位置,再依次向前,直到end=pos,此时原pos到size位置的字符全都向后挪动一位。_str[pos]的位置被空出,可以安全插入ch。

将‘\0’一起挪动,保证了字符串的完整性

/在pos位置插入字符
string& insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size >= _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		/将pos以及pos之后的字符都向后挪动一位,为ch的插入留空间
		_str[end] = _str[end - 1];
		--end;
	}

	_str[pos] = ch;/插入字符
	++_size;/更新长度

	return *this;
}

  • 为什么用for循环拷贝字符串,而非memcpy?

For循环和memcpy的效果一致,但是for逐个字符的拷贝逻辑,更能贴合自定义string处理字符形式

/在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	/和尾插字符串的开辟空间思路类似
	if (_size + len > _capacity)
	{
		size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
		reserve(newcapacity);
	}
	size_t end = _size + len;/挪动的终点
	while (end > pos + len - 1)
	{
		/将pos以及pos之后的字符向后挪动len位,为str的插入预留空间
		_str[end] = _str[end - len];
		--end;
	}

	/拷贝待插入字符串
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += len;/更新长度

	return *this;
}

erase
  • 在指定pos位置开始删除len个字符
  • 为什么memmove的拷贝长度是_size + 1 - i ?

_size + 1 - i 是 “需要挪动的字节数”, 从_str[i]到_str[_size],包含‘\0’,保证字符串的完整性

/在pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{
	assert(pos < _size);
	/要删的数据大于pos后面的字符个数,pos后边全删掉
	if (len == npos || len >= (_size - pos))
	{
		_size = pos;
		_str[_size] = '\0';/直接补‘\0’,无需挪动数据,效率O(1}
	else
	{
		size_t i = pos + len;
		/部分删除,字符直接向前挪动覆盖原数据
		memmove(_str + pos, _str + i, _size + 1 - i);
		_size -= len;
	}
			
	return *this;
}

3) 查找和子串

find
  • 从pos位置开始查找字ch/查找子串str
  • 字符查找,遍历pos到_size,返回第一个匹配ch的位置,未找到返回npos
  • 子串查找,使用字符串匹配算法,从pos开始查找str的完整匹配,返回起始位置
/查找字符
size_t find(char ch, size_t pos = 0) const
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;/返回该字符位置
		}
	}
	return npos;/找不到返回npos
}

/查找字符串
size_t find(const char* str, size_t pos = 0)const
{
	/strstr是C标准库提供的字符串查找函数,效率高
	const char* p1 = strstr(_str + pos, str);
	if (p1 == nullptr)
	{
		return npos;/找不到
	}
	else
	{
		return p1 - _str;/找到了返回引索
	}
}

substr

提取从pos位置开始的len个字符组成的子串

/提取子串
string substr(size_t pos, size_t len = npos)const
{
	if (len == npos || len >= _size - pos)
	{
		len = _size - pos;
	}
	string ret;
	ret.reserve(len);
	/将提取出的子串储存到新string对象,+=很方便
	for (size_t i = 0; i < len; i++)
	{
		ret += _str[pos + i];
	}
	return ret;
}

4) 清空字符串

clear
  • 清空字符串内容,但保持容量不变
  • 为什么_str[0] = '\0’就能实现清空效果?

字符串的本质是以‘\0’结尾的字符序列,strlen等函数看到‘\0’就会停止运算,其他的字符串操作,比如find也会认为这是一个空字符串

  • 为什么不处理_capacity?

_capacity表示最多能存储的字符数,清空内容不影响这个属性,还能为清空后的其他函数操作预留空间

/清空字符串
void clear()
{
	_str[0] = '\0';/在开头放终止符
	_size = 0;/重置长度为0
}

7、 比较运算符重载

逐字符比较 ASCII 值

/ s1 < s2
/ "hello"  "hello"   -> false
/ "hellox" "hello"   -> false
/ "hello"  "hellox"  -> true
bool operator<(const string& s) const
{
	size_t i1 = 0, i2 = 0;
	/遍历到较短字符结束
	while (i1 < _size && i2 < s._size)
	{
		/高位小,整体小
		if (_str[i1] < s[i2])
		{
			return true;
		}
		/高位大,整体大
		else if (_str[i1] > s[i2])
		{
			return false;
		}
		else
		{
			++i1;
			++i2;
		}
	}
	/如果循环走完了,至少有一个字符已经遍历完了
	/ s还有长度,说明s更大,当前字符串更小
	/"hello"  "hellox"  -> true
	return i2 < s._size;
}
bool operator==(const string& s) const
{
	size_t i1 = 0, i2 = 0;
	/遍历到较短字符串结束
	while (i1 < _size && i2 < s._size)
	{
		/找到不相等字符,直接返回false
		if (_str[i1] != s[i2])
		{
			return false;
		}
		else
		{
			++i1;
			++i2;
		}
	}
	/必须两个字符串都遍历完才相等,长度内容均相等
	return i1 == _size && i2 == s._size;
}
/其他运算符(<=>>=!=)均通过operator<operator==组合而成
/ 避免重复代码
bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}
bool operator>(const string& s) const
{
	return!(*this <= s);
}
bool operator>=(const string& s) const
{
	return!(*this < s);
}
bool operator!=(const string& s) const
{
	return !(*this == s);
}

8、 输入输出运算符重载

operator<<
  • 输出string类字符串
  • 为什么用for循环逐字符输出,而非直接 out << s.c_str()?

s.c_str()会暴露底层_str指针,而逐字符输出通过operator[]访问字符,完全依赖类的公有接口,不泄露底层存储细节,而且operator[]自带越界断言,会更安全

  • 为什么返回ostream&,而不是void?

为了链式输出,比如cout << s1 << s2,本质是(cout << s1) << s2

  • 参数为什么是const string& s?

传引用避免了拷贝字符串,提高效率。用const修饰符合operator<<只读的性质

/输出
ostream& operator<<(ostream& out, const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];/逐个输出字符
	}
	return out;/返回流对象,支持链式输出(cout<<s1<<s2)
}

operator<<
  • 输入string类字符串
  • if (in)判断的意义是什么?

处理 流错误 场景,避免后续push_back无效操作

  • 为什么用isspace(ch)判断空白字符,而非直接对比’ ‘和’\n’?

isspace是 C 标准库函数,能识别所有空白字符(包括空格’ ‘、换行’\n’、制表符’\t’、回车’\r’等),覆盖更全面

/输入,跳过空格、换行
istream& operator>>(istream& in, string& s)
{
	s.clear();/清空原字符串
	char ch;
	/利用istream的默认行为 —— 自动跳过所有空白字符
	/直接读取第一个有效字符
	in >> ch;
	if (in)
	{
		s.push_back(ch);/将第一个非空字符插入ch
		/继续读取(包括空白字符),并且判断是否为空
		while (in.get(ch) && !isspace(ch))
		{
			s.push_back(ch);
		}
	}
	return in;/返回流对象,支持链式输入(如cin >> s1 >> s2)
		
}

getline
  • 读取整行
  • 为什么getline能读取空格,而operator>>不能?

其实是二者的定位不同,operator>>用于读取 “单词”默认跳过空白
而getline用于读取“整行内容” ,用in.get(ch)读取字符,该函数不跳过任何字符

/读取一行(包含空格,到delim为止)
istream& getline(istream& is, string& str, char delim = '\n')
{
	str.clear();
	char ch;
	/逐个读取字符,直到遇到分隔符或流结束
	while (is.get(ch))/ 读取包括空白字符在内的所有字符
	{
		if (ch == delim)/遇到分隔符,停止读取
		{
			break;
		}
		str.push_back(ch);
	}
	return is;
}

9、 检测代码正确性

其实应该写一段代码,就检测一段代码的正确性,为了方便阅读,我将测试用例都写在一起了

#define _CRT_SECURE_NO_WARNINGS 1
/test.pp

#include"string.h"

/构造拷贝构造赋值
void test1()
{
	ST::string s1;
	ST::string s2("hello");
	ST::string s3(s2);
	ST::string s4 = "world";
	ST::string s5 = s4;

	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;
	cout << "s3:" << s3 << endl;
	cout << "s4:" << s4 << endl;
	cout << "s5:" << s5 << endl;
}

/迭代器遍历
void test2()
{
	ST::string s1("hello world");
	ST::string::iterator it = s1.begin();
	for (it = s1.begin(); it != s1.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

/尾部操作
void test3()
{
	ST::string s1;
	s1.push_back('a');
	s1.append("bcd");
	s1 += "hehe";
	s1.pop_back();
	cout << s1 <<" " << s1[0] << endl;
}

/插入与删除
void test4()
{
	ST::string s1("hello");
	s1.insert(2, 'X');
	s1.insert(5, "AB");
	s1.erase(0, 1);
	cout << s1 << endl;
}

/查找与子串
void test5()
{
	ST::string s1("abcabcabc");
	size_t pos1 = s1.find('b');
	size_t pos2 = s1.find('b', 3);/从索引3开始找
	size_t pos3 = s1.find("abc");
	size_t pos4 = s1.find("abc",4);
	cout << pos1 <<" " << pos2 <<" " << pos3 <<" " << pos4 << endl;

	ST::string s2 = s1.substr(3, 3);
	ST::string s3 = s1.substr(6);
	cout << s2 << endl;
	cout << s3 << endl;

}

/关系运算符
void test6()
{
	ST::string s1("haha");
	ST::string s2("hehe");
	ST::string s3("xixixi");
	cout << (s1 == s2) << endl;
	cout << (s1 != s2) << endl;
	cout << (s1 < s3) << endl;
}

void test7()
{
	ST::string s1("hello world");
	cout << s1.size() << endl;
	s1.clear();
	cout << s1.size() << endl;
	s1 += "hehe";
	cout <<s1<<" " << s1.size() << endl;
}

/输入输出
void test8()
{
	ST::string s1, s2;
	/输入hhh hhh,只输出hhh,跳过空格
	/cin >> s1;
	/cout << s1 << endl;

	/输入hhh hhh,输出hhh hhh,不跳过空格
	getline(cin, s2);
	cout << s2 << endl;
}
int main()
{
	/test1();
	/test2();
	/test3();
	/test4();
	/test5();
	/test6();
	/test7();
	test8();
	return 0;
}

10、完整代码

string.h

#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace ST
{
	class string
	{
	public:
		//将char* 类型重命名成iterator,用指针功能来模拟迭代器
		//普通的迭代器,可以访问修改元素
		typedef char* iterator;

		//常量迭代器,只能读取元素
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;//指向字符串首字符
		}
		iterator end()
		{
			return _str + _size;//指向字符串最后一个字符的下一个位置‘\0’
		}

		const_iterator begin()const
		{
			return _str;//const修饰this指针,返回的this->_str不可修改
		}
		const_iterator end()const
		{
			return _str + _size;//返回只读字符串尾元素
		}

		//构造函数
		string(const char* str = "")//如果不传参,默认为空字符串
			:_size(strlen(str))//strlen不包含‘\0’
		{
			_capacity = _size;
			_str = new char[_size + 1];//_size+1体现了为‘\0’预留空间

			//将str的_size+1个字符串长度,复制到_str
			//为什么不使用strcpy(_str,str);
			memcpy(_str, str, _size + 1);
		}

		//为啥要自己写swap
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		//拷贝构造函数s2(s1)
		//深拷贝,避免浅拷贝让两个对象指向同一块空间导致析构两次
		//新写法
		string(const string& s)
		{
			string tmp(s._str);//先用构造函数构造一个临时对象tmp(深拷贝s的内容)
			swap(tmp);//再调用自己写的swap,交换当前对象和tmp的资源
		}

		////传统写法
		//string(const string& s)
		//{
		//	_str = new char[s._capacity + 1];
		//	memcpy(_str, s._str, s._size + 1);
		//	_size = s._size;
		//	_capacity = s._capacity;
		//}



		//赋值运算符重载
		//s2=s1
		string& operator=(string tmp)//传值参数,自动调用拷贝构造,深拷贝s1的内容到tmp
		{
			swap(tmp);//交换s2和tmp的资源
			return*this;
		}

		//析构函数
		~string()
		{
			delete[]_str;
			_str = nullptr;
			_size = _capacity = 0;
		}

		//返回当前字符串长度
		size_t size()const
		{
			return _size;
		}

		//提前扩容
		void reserve(size_t n)
		{
			if (n > _capacity)//当n大于当前容量才扩容
			{
				char* tmp = new char[n + 1];//新内存包含‘\0’
				memcpy(tmp, _str, _size + 1);//拷贝旧字符串,把‘\0’也带上
				delete[]_str;
				_str = tmp;
				_capacity = n;
			}
		}
		static const size_t npos;


		//可以修改访问
		char& operator[](size_t i)
		{
			assert(i < _size);//避免越界访问
			return _str[i];//返回字符引用
		}

		//只读访问
		const char& operator[](size_t i)const
		{
			assert(i < _size);
			return _str[i];
		}

		//尾插单个字符
		void push_back(char ch)
		{
			if (_size >= _capacity)//检查容量是否足够
			{
				size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newcapacity);//调用reserve扩容
			}
			_str[_size] = ch;//尾插字符
			++_size;//更新长度
			_str[_size] = '\0';//维护'\0'结尾
		}
		//尾插字符串
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)//检查总长度是否超过容量
			{
				//扩容到“2倍原容量”和“当前长度+待插长度”中的较大值
				size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
				reserve(newcapacity);
			}

			memcpy(_str + _size, str, len + 1);//拷贝含‘\0’
			_size += len;//更新总长度
		}

		//+=字符
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		//+=字符串
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		//尾删一个字符
		void pop_back()
		{
			assert(_size > 0);//检查字符串是否空
			--_size;
			_str[_size] = '\0';
		}

		//在pos位置插入字符
		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newcapacity);
			}
			size_t end = _size + 1;
			while (end > pos)
			{
				//将pos以及pos之后的字符都向后挪动一位,为ch的插入留空间
				_str[end] = _str[end - 1];
				--end;
			}

			_str[pos] = ch;//插入字符
			++_size;//更新长度

			return *this;
		}

		//在pos位置插入字符串
		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			//和尾插字符串的开辟空间思路类似
			if (_size + len > _capacity)
			{
				size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
				reserve(newcapacity);
			}
			size_t end = _size + len;//挪动的终点
			while (end > pos + len - 1)
			{
				//将pos以及pos之后的字符向后挪动len位,为str的插入预留空间
				_str[end] = _str[end - len];
				--end;
			}

			//拷贝待插入字符串
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += len;//更新长度

			return *this;
		}

		//在pos位置开始删除len个字符
		string& erase(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			//要删的数据大于pos后面的字符个数,pos后边全删掉
			if (len == npos || len >= (_size - pos))
			{
				_size = pos;
				_str[_size] = '\0';//直接补‘\0’,无需挪动数据,效率O(1)
			}
			else
			{
				size_t i = pos + len;
				//部分删除,字符直接向前挪动覆盖原数据
				memmove(_str + pos, _str + i, _size + 1 - i);
				_size -= len;
			}
			
			return *this;
		}

		//查找字符
		size_t find(char ch, size_t pos = 0) const
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;//返回该字符位置
				}
			}
			return npos;//找不到返回npos
		}

		//查找字符串
		size_t find(const char* str, size_t pos = 0)const
		{
			//strstr是C标准库提供的字符串查找函数,效率高
			const char* p1 = strstr(_str + pos, str);
			if (p1 == nullptr)
			{
				return npos;//找不到
			}
			else
			{
				return p1 - _str;//找到了返回引索
			}
		}

		//提取子串
		string substr(size_t pos, size_t len = npos)const
		{
			if (len == npos || len >= _size - pos)
			{
				len = _size - pos;
			}
			string ret;
			ret.reserve(len);
			//将提取出的子串储存到新string对象,+=很方便
			for (size_t i = 0; i < len; i++)
			{
				ret += _str[pos + i];
			}
			return ret;
		}

		//清空字符串
		void clear()
		{
			_str[0] = '\0';//在开头放终止符
			_size = 0;//重置长度为0
		}


		// s1 < s2
		// "hello"  "hello"   -> false
		// "hellox" "hello"   -> false
		// "hello"  "hellox"  -> true
		bool operator<(const string& s) const
		{
			size_t i1 = 0, i2 = 0;
			//遍历到较短字符结束
			while (i1 < _size && i2 < s._size)
			{
				//高位小,整体小
				if (_str[i1] < s[i2])
				{
					return true;
				}
				//高位大,整体大
				else if (_str[i1] > s[i2])
				{
					return false;
				}
				else
				{
					++i1;
					++i2;
				}
			}
			//如果循环走完了,至少有一个字符已经遍历完了
			// s还有长度,说明s更大,当前字符串更小
			//"hello"  "hellox"  -> true
			return i2 < s._size;
		}
		bool operator==(const string& s) const
		{
			size_t i1 = 0, i2 = 0;
			//遍历到较短字符串结束
			while (i1 < _size && i2 < s._size)
			{
				//找到不相等字符,直接返回false
				if (_str[i1] != s[i2])
				{
					return false;
				}
				else
				{
					++i1;
					++i2;
				}
			}
			//必须两个字符串都遍历完才相等,长度内容均相等
			return i1 == _size && i2 == s._size;
		}
		//其他运算符(<=、>、>=、!=)均通过operator<和operator==组合而成
		// 避免重复代码
		bool operator<=(const string& s) const
		{
			return *this < s || *this == s;
		}
		bool operator>(const string& s) const
		{
			return!(*this <= s);
		}
		bool operator>=(const string& s) const
		{
			return!(*this < s);
		}
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}


	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
	//在类外定义npos
	const size_t string::npos = -1;

	//输出
	ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];//逐个输出字符
		}
		return out;//返回流对象,支持链式输出(cout<<s1<<s2)
	}
	//输入,跳过空格、换行
	istream& operator>>(istream& in, string& s)
	{
		s.clear();//清空原字符串
		char ch;
		//利用istream的默认行为 —— 自动跳过所有空白字符
		//直接读取第一个有效字符
		in >> ch;
		if (in)
		{
			s.push_back(ch);//将第一个非空字符插入ch
			//继续读取(包括空白字符),并且判断是否为空
			while (in.get(ch) && !isspace(ch))
			{
				s.push_back(ch);
			}
		}
		return in;//返回流对象,支持链式输入(如cin >> s1 >> s2)
		

	}

	//读取一行(包含空格,到delim为止)
	istream& getline(istream& is, string& str, char delim = '\n')
	{
		str.clear();
		char ch;
		// 逐个读取字符,直到遇到分隔符或流结束
		while (is.get(ch))// 读取包括空白字符在内的所有字符
		{
			if (ch == delim)//遇到分隔符,停止读取
			{
				break;
			}
			str.push_back(ch);
		}
		return is;
	}
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
//test.pp

#include"string.h"

//构造拷贝构造赋值
void test1()
{
	ST::string s1;
	ST::string s2("hello");
	ST::string s3(s2);
	ST::string s4 = "world";
	ST::string s5 = s4;

	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;
	cout << "s3:" << s3 << endl;
	cout << "s4:" << s4 << endl;
	cout << "s5:" << s5 << endl;
}

//迭代器遍历
void test2()
{
	ST::string s1("hello world");
	ST::string::iterator it = s1.begin();
	for (it = s1.begin(); it != s1.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

//尾部操作
void test3()
{
	ST::string s1;
	s1.push_back('a');
	s1.append("bcd");
	s1 += "hehe";
	s1.pop_back();
	cout << s1 <<" " << s1[0] << endl;
}

//插入与删除
void test4()
{
	ST::string s1("hello");
	s1.insert(2, 'X');
	s1.insert(5, "AB");
	s1.erase(0, 1);
	cout << s1 << endl;
}

//查找与子串
void test5()
{
	ST::string s1("abcabcabc");
	size_t pos1 = s1.find('b');
	size_t pos2 = s1.find('b', 3);//从索引3开始找
	size_t pos3 = s1.find("abc");
	size_t pos4 = s1.find("abc",4);
	cout << pos1 <<" " << pos2 <<" " << pos3 <<" " << pos4 << endl;

	ST::string s2 = s1.substr(3, 3);
	ST::string s3 = s1.substr(6);
	cout << s2 << endl;
	cout << s3 << endl;

}

//关系运算符
void test6()
{
	ST::string s1("haha");
	ST::string s2("hehe");
	ST::string s3("xixixi");
	cout << (s1 == s2) << endl;
	cout << (s1 != s2) << endl;
	cout << (s1 < s3) << endl;
}

void test7()
{
	ST::string s1("hello world");
	cout << s1.size() << endl;
	s1.clear();
	cout << s1.size() << endl;
	s1 += "hehe";
	cout <<s1<<" " << s1.size() << endl;
}

//输入输出
void test8()
{
	ST::string s1, s2;
	//输入hhh hhh,只输出hhh,跳过空格
	//cin >> s1;
	//cout << s1 << endl;

	////输入hhh hhh,输出hhh hhh,不跳过空格
	getline(cin, s2);
	cout << s2 << endl;
}
int main()
{
	//test1();
	//test2();
	//test3();
	//test4();
	//test5();
	//test6();
	//test7();
	test8();
	return 0;
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐