【C++】string的模拟实现

发布于:2025-06-28 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

1. string的基本结构

2. 构造函数的实现

2.1 无参构造函数

2.2 带参构造函数

2.3 无参和带参构造函数合并

3. 析构函数的实现

4. [ ] 运算符重载

5. 观察运行范围for

6. 增删查改

6.1 插入字符

 6.2 插入字符串

6.3 在pos位置插入字符

6.4 在pos位置插入字符串 

6.5 删除从pos位置开始的len个字符 

6.6 查找子串 

7. 构造子串 

8. 拷贝构造函数 

9. 运算符重载

9.1 赋值运算符重载 

9.2 比较运算符重载 

9.3 <<和>>运算符重载 

10. 源码

string.h

string.cpp 


1. string的基本结构

三个成员变量:

  • 字符指针_str指向开辟的动态数组
  • _size标识有效数据个数
  • _capacity记录容量的大小
	class string
	{
	public:
		//成员函数
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

2. 构造函数的实现

2.1 无参构造函数

//无参error
string()
	:_str(nullptr)//这样写error!因为在打印空字符串时(string s1;  cout << s1.c_str())  _str是野指针,cout << s1.c_str()会导致程序崩溃!必须确保char*指针指向的内存以'\0'结尾。
	,_size(0)
	,_capacity(0)
{}

修改代码:

//无参right
string()		
    :_str(new char[1]{'\0'})//给_str开辟1字节空间,并填充\0就可以解决问题
    ,_size(0)
	,_capacity(0)
{}

2.2 带参构造函数

//带参 error!		
string(const char* str)
	:_size(strlen(str))          //2
	, _str(new char[_size + 1])  //1 初始化列表按照成员变量在类中声明顺序进行初始化,所以这样写是错误的!
	, _capacity(_size)           //3
{
	strcpy(_str, str);
}

修改代码:

形式1:

//带参 right!		
string(const char* str)
	:_str(new char[strlen(str) + 1])
	,_size(strlen(str))          
	, _capacity(_size)           
{
	strcpy(_str, str);
}

形式2: 

//带参 right!
string(const char* str)
{
	_size = strlen(str);
	_str = new char[_size + 1];
	_capacity = _size;
	strcpy(_str, str);
}

2.3 无参和带参构造函数合并

全缺省		
//string(const char* str = "")
//	:_str(new char[strlen(str) + 1])
//	,_size(strlen(str))          
//	, _capacity(_size)           
//{
//	strcpy(_str, str);
//}

3. 析构函数的实现

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

 实现一个接口,返回字符指针,结合cout打印字符串的内容。

const char* c_str() const
{
	return _str;
}

4. [ ] 运算符重载

char& operator[](size_t pos)
{
	assert(pos < _size);

	return _str[pos];
}
const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

5. 观察运行范围for

for (auto e : s2)
{
	cout << e << " ";
}
cout << endl;

会报错:

范围for的底层就是迭代器,报错的关键在于自定义类未满足范围for的迭代器接口要求。

模拟使用指针来实现迭代器:为自定义string类中定义begin()和end()

这样范围for和迭代器都可正常运行了。 

6. 增删查改

6.1 插入字符

	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		++_size;

		_str[_size] = '\0';
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}

 6.2 插入字符串

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			//大于2倍,需要多少开多少,小于2倍,按2倍扩容
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		strcpy(_str + _size, str);//目标地址:\0位置   strcpy会把\0也拷贝过去
		_size += len;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

6.3 在pos位置插入字符

//在pos位置插入字符 error
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	//挪动数据
	size_t end = _size;

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

这样写对于在0位置处插入字符操作程序会崩掉,因为当end=0,end--之后为-1,由于end为无符号整型,所以end会变成很大的正数,end是极大值,_str[end + 1]会超出字符串数组合法内存范围,触发内存访问越界,最终导致程序崩溃。

如果我们把end的类型改为int呢?程序依然会崩溃。因为在表达式end>=pos中,end是int(有符号),pos是size_t(无符号),C++会触发隐式类型转换,将int类型的end转换为size_t(无符号整型)。

解决办法:

方法1. 将pos显示转换为int: 

	//在pos位置插入字符 right
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		//挪动数据
		int end = _size;
		while (end >= (int)pos)
		{
			_str[end + 1] = _str[end];
			end--;
		}
		_str[pos] = ch;
		++_size;
	}

方法2. 改变end的位置

//在pos位置插入字符
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	//挪动数据
	size_t end = _size + 1;    //让end为\0的下一个位置

	while (end > pos)//假设pos=0,end最后一次=1,所以终止条件end>pos
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	++_size;
}

6.4 在pos位置插入字符串 

//在pos位置插入字符串
void string::insert(size_t pos, const char* s)
{
	assert(pos <= _size);
	size_t len = strlen(s);

	if (_size + len > _capacity)
	{
		//大于2倍,需要多少开多少,小于2倍,按2倍扩容
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	//挪动数据
	size_t end = _size + len;

	//while (end >= pos + len)//当pos=0,len=0时,--end到0,--end,end=-1,end变成极大值>0,程序崩溃!所以条件改成end > pos+len-1
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		--end;
	}	
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = s[i];
	}
	_size += len;

}

6.5 删除从pos位置开始的len个字符 

	//删除pos开始的len个字符
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[pos] = _str[i];
				pos++;
			}
			_size -= len;
		}
	}
	
}

6.6 查找子串 

	//查找子串
	size_t string::find(const char* s, size_t pos = 0)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str, s);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;//指针-指针得到元素个数,指针差直接对应字符串中的字符索引
		}
	}

7. 构造子串 

	//构造子串
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		//len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

		string sub;  //构造局部对象
		sub.reserve(len);

		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		
		return sub;//传值返回  拷贝构造临时对象
	}



//使用
void test_string3()
{
	string s("test.cpp.zip");
	size_t pos = s.find('.');
	cout << pos << endl;

	string suffix = s.substr(2);
	cout << suffix.c_str() << endl;

}

这段代码在vs2019崩溃在vs2022正常,核心在于返回值优化的差异上:

函数传值返回sub,会触发拷贝构造临时对象,再将临时对象拷贝给调用处的suffix,编译器的优化可能将临时对象省略,直接让sub拷贝构造suffix,但在拷贝过程中,两者的_str会指向同一块内存,当sub出作用域销毁时,释放内存导致suffix的_str成为野指针,访问时崩溃。

vs2022优化一步到位,把三次构造(局部对象sub、临时对象、suffix)合并成一次,直接在suffix的内存位置构造子串,完全避免了sub的拷贝,规避了浅拷贝问题。

8. 拷贝构造函数 

若未显示定义,编译器会生成默认的拷贝构造函数,进行浅拷贝,所以string需要显示的提供拷贝构造:

确保string类的_str在拷贝时是深拷贝,而非浅拷贝(共享指针),需要提供拷贝构造函数

//拷贝构造函数
string(const string& s)
{
	_str = new char[s._capacity + 1]; //新开辟内存复制内容
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

9. 运算符重载

9.1 赋值运算符重载 

同时,赋值也会存在浅拷贝问题,我们也要进行赋值运算符重载: 

//赋值运算符重载(深拷贝问题)
string& operator=(const string& s)
{
	if (this != &s)//防止自己给自己赋值
	{
		delete[] _str;

		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
		return *this;
	}
}

9.2 比较运算符重载 

	//只需要实现<和==运算符重载即可,其它运算符可以复用已重载的运算符
	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;//复用<和==
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);//复用<=
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);//复用<
	}
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);//复用==
	}

使用: 

	void test_string()
	{
		string s1("abcdef");
		string s2("abcdef");
		cout << (s1 < s2) << endl;//0
		cout << (s1 > s2) << endl;//0
		cout << (s1 == s2) << endl;//1

		cout << ("hello world" < s2) << endl;//0 单参数构造函数支持隐式类型转换,通过构造函数将"hello world"转换成string临时对象
		cout << (s1 == "hello world") << endl;//0

		cout << ("hello world" =="hello world") << endl;//1
		//运算符重载的参数必须至少有一个类类型。
		//当没有合适的重载运算符时,编译器会优先使用原生运算符,这里实际比较的是指针的地址,结果为1。需用strcmp直接比较。
	}

9.3 <<和>>运算符重载 

标准输入流的>>运算符设计为提取“单词”,cin的行为特性默认以空白字符(空格、换行、制表符等)作为分隔符,遇到空白就停止读取。这种设计符合大多数场景中“按单词解析”的需求。例如:

string s1,s2;
cin>>s1>>s2;
//输入"hello world"时,s1="hello",s2="world"(空格被跳过)

标准库的>>运算符提取字符串时,核心是将空格作为“单词分隔符”。

模拟实现流提取运算符重载(遇到空格或换行就停止) 

	istream& operator>>(istream& in, string& s)
	{
		s.clear();//清空要存储输入内容的string对象s
		const int N = 256;
		char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符
		int i = 0;

		char ch;	
		ch = in.get();//从输入流中提取单个字符
		while (ch != ' ' && ch != '\n')//遇到空格/换行停止读取
		{
			buff[i++] = ch;//存入缓冲区

			if (i == N - 1)//只剩最后一个位置来存储结束标志\0时
			{
				buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理
				s += buff;

				i = 0;//重置缓冲区下标
			}			
			
			ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断
		}
		if (i > 0) //当循环遇到空格或换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

 使用:

void test_string5()
{
	string s2;
	cin >> s2;
	cout << s2;
}

运行结果:

上面代码完成了从输入流读取内容(遇到空格\换行就停止读取),并将内容存储起来。手动实现字符串读取逻辑, 如果需要包含空格的输入,标准库提供了std::getline,是string专门的函数,实现了空格的输入,例如:

string s;
getline(cin,s);
//输入"hello world"时,s="hello world"(包含空格)

模拟实现一下getline(读取到换行停止):

   	istream& operator>>(istream& in, string& s)
	{
		s.clear();//清空要存储输入内容的string对象s
		const int N = 256;
		char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符
		int i = 0;

		char ch;
		ch = in.get();//从输入流中提取单个字符
		while (ch != '\n')//直到遇到换行符停止读取
		{
			buff[i++] = ch;//存入缓冲区

			if (i == N - 1)//只剩最后一个位置来存储结束标志\0时
			{
				buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理
				s += buff;

				i = 0;//重置缓冲区下标
			}
			
			ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断
		}

		if (i > 0)//当循环遇到换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

 使用:

void test_string5()
{
	string s2;
	cin >> s2;
	cout << s2;
}

运行结果:

10. 源码

string.h

#define _CRT_SECURE_NO_WARNINGS 1

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


namespace pig
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}


		//无参
		string()			
			:_str(new char[1]{'\0'})
			,_size(0)
			,_capacity(0)
		{}
		
		//带参 right!
		string(const char* str)
		{
			_size = strlen(str);
			_str = new char[_size + 1];
			_capacity = _size;
			strcpy(_str, str);
		}

		//拷贝构造函数(解决浅拷贝问题)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		//赋值运算符重载(解决浅拷贝问题)
		string& operator=(const string& s)
		{
			if (this != &s)//防止自己给自己赋值
			{
				delete[] _str;

				_str = new char[s._capacity + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
				return *this;
			}
		}
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0; 
		}

		const char* c_str() const
		{
			return _str;
		}

		void clear()
		{
			_str[0] = '\0';//清掉所有数据,不释放空间
			_size = 0;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		void reserve(size_t n);
		void push_back(char ch);
		void append(const char*);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);
		
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* s, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);

	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		static const size_t npos = -1; //static允许类内初始化的特殊情况
	};

	//比较运算符重载
	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);	

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in,  string& s);
}

string.cpp 

#include"string.h"

namespace pig
{
	//const size_t string::npos = -1;

	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		++_size;

		_str[_size] = '\0';
	}

	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			//大于2倍,需要多少开多少,小于2倍,按2倍扩容
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		strcpy(_str + _size, str);//目标地址:\0位置   strcpy会把\0也拷贝过去
		_size += len;
	}

	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

	//在pos位置插入字符
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		//挪动数据
		size_t end = _size + 1;    //让end为\0的下一个位置

		while (end > pos)//假设pos=0,end最后一次=1,所以终止条件end>pos
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[pos] = ch;
		++_size;
	}


	//在pos位置插入字符串
	void string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);
		size_t len = strlen(s);

		if (_size + len > _capacity)
		{
			//大于2倍,需要多少开多少,小于2倍,按2倍扩容
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		//挪动数据
		size_t end = _size + len;

		//while (end >= pos + len)//当pos=0,len=0时,--end到0,--end,end=-1,end变成极大值>0,程序崩溃!所以条件改成end > pos+len-1
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}	
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}
		_size += len;

	}

	//删除pos位置开始的len个字符
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[pos] = _str[i];
				pos++;
			}
			_size -= len;
		}
	}
	
	//查找字符
	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}			
		}
		return npos;
	}

	//查找子串
	size_t string::find(const char* s, size_t pos)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str, s);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;//指针-指针得到两指针之间的元素个数
		}
	}

	//构造子串
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		//len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

		string sub;  //构造局部对象
		sub.reserve(len);

		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		
		return sub;
	}

	//只需要实现<和==运算符重载即可,其它运算符可以复用已重载的运算符
	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;//复用<和==
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);//复用<=
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);//复用<
	}
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);//复用==
	}

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	//模拟实现流提取运算符重载
	istream& operator>>(istream& in, string& s)
	{
		s.clear();//清空要存储输入内容的string对象s
		const int N = 256;
		char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符
		int i = 0;

		char ch;	
		ch = in.get();//从输入流中提取单个字符
		while (ch != ' ' && ch != '\n')//遇到空格/换行停止读取
		{
			buff[i++] = ch;//存入缓冲区

			if (i == N - 1)//只剩最后一个位置来存储结束标志\0时
			{
				buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理
				s += buff;

				i = 0;//重置缓冲区下标
			}			
			
			ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断
		}
		if (i > 0) //当循环遇到空格或换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}


网站公告

今日签到

点亮在社区的每一天
去签到