C++——string常用接口模拟实现

目录

一. string常用接口补充——to_string和stoi

二. 深浅拷贝

1. 浅拷贝

2. 深拷贝

 三. string模拟实现——传统方法

四. string模拟实现——现代方法

五. string模拟实现——其他常用接口

1. c_str()

2. 重载[ ]

3. size()

4. capacity()

5. reserve()

6. insert()插入单个字符

7. insert()插入字符串

8. push_back()

9. append()

10. 重载+=字符

11. 重载+=字符串

12. resize()

13. erase()

14. find()查找字符

15. find()查找字符串

15. clear()

16. 迭代器

17. ==、!=、>=,>,<=,<逻辑操作符的重载

18. 重载<<

19. 重载>>

六. 了解部分——写时拷贝


一. string常用接口补充——to_string和stoi

to_string将整形数字和浮点型数字转换为字符串
stoi将数字字符串转化为整形数字
    //数字转字符串
	string s1("hello world");
	int i = 1234567890;
	string s2 = to_string(i);
	cout <<  s2 << endl;
	
	//字符串转数字
	string s3("0987654321");
	int n = stoi(s3);
	cout << n << endl;

二. 深浅拷贝

当我们没有自己实现拷贝构造函数时,使用的是默认的拷贝构造函数。

namespace mystring
{
	class string
	{
	public:
		string(const char* str = "")
		{
			_str = new char[strlen(str + 1)];
			strcpy(_str, str);
		}

		//析构函数
		~string()
		{
			if (_str)
			{
				delete[]_str;
			}
		}

	private:
		char* _str;//指向字符串
	};
}

// 测试
void test()
{
    String s1("hello");
    String s2(s1);
}

解释:string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

1. 浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

浅拷贝会出现的问题即多次释放同一块空间。遇到这个问题时,我们需要采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享

2. 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。即我们需要自己提供拷贝构造函数。

 三. string模拟实现——传统方法

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

//string实现
namespace mystring
{
	class string
	{
	public:
		//构造函数
        //使用缺省值,成为默认构造函数
        //无参构造函数调用,可以使用缺省值,标准库是给的空字符串
        //使用依次初始化的方式不用频繁调用strlen
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造函数,不写该拷贝构造函数则发生拷贝时会是浅拷贝
		string(const string& s)
			:_str(new char[s._capacity + 1])
			, _size(s._size)
			, _capacity(s._capacity)
		{
			strcpy(_str, s._str);
		}

        //重载=
		string& operator=(const string& s)
		{
			//自己给自己赋值不配进来
			if (_str != s._str)
			{
				char* temp = new char[s._capacity + 1];
				strcpy(temp, s._str);
				delete[]_str;
				_str = temp;
				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}

		//析构函数
		~string()
		{
            //防止释放nullptr
			if (_str)
			{
				delete[]_str;
				_str = nullptr;
				_size = _capacity = 0;
			}
		}
	private:
		char* _str;//指向字符串
		size_t _size;//有效字符个数
		size_t _capacity;//容量的大小

        //声明npos
		const static size_t npos;
		//const static size_t npos = -1;不推荐,类里的只是声明,不应该是定义
	};

    //定义npos
	const size_t string::npos = -1;
}

注意:构造函数使用缺省值,成为默认构造函数,无参对象可调用,可以使用缺省值,标准库是给的空字符串,使用依次初始化的方式不用频繁调用strlen,_capacity + 1是预留一个'\0'

四. string模拟实现——现代方法

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

//string实现
namespace mystring
{
	class string
	{
	public:
		//构造函数
        //使用缺省值,成为默认构造函数
        //无参构造函数调用,可以使用缺省值,标准库是给的空字符串
        //使用依次初始化的方式不用频繁调用strlen
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//交换函数,交换两个对象
		void swap(string& s)
		{
			//std里的swap是浅拷贝
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		//现代方法的拷贝构造,将s初始化,防止当=里拷贝构造过来的的地址是随机值,然后释放随机值
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			//借用temp构造完了再给s
			string temp(s._str);
			swap(temp);
		}

        //现代方法重载=,s不使用传引用和const,使用传值
		string& operator=(string s)
		{
			//将赋值右边对象拷贝构造到s里,再将s里的和赋值左边的对象交换
            //由s将和赋值左边对象交换过来的释放(即释放左边的对象)
			swap(s);
			return *this;
		}

		//析构函数
		~string()
		{
			if (_str)
			{
				delete[]_str;
				_str = nullptr;
				_size = _capacity = 0;
			}
		}
	private:
		char* _str;//指向字符串
		size_t _size;//有效字符个数
		size_t _capacity;//容量的大小

        //声明npos
		const static size_t npos;
		//const static size_t npos = -1;不推荐,类里的只是声明,不应该是定义
	};

    //定义npos
	const size_t string::npos = -1;
}

注意:现代方法引进的思想是创建一个中间对象由中间对象去复制需要拷贝的内容,最后再交换给需要的对象。

现代方法的拷贝构造需要注意要将s对象初始化,防止当=里拷贝构造过来的的地址是随机值,然后释放随机值引起报错。

现代方法重载=,s对象不使用传引用传参和const修饰,使用传值传参,目的是将赋值右边的对象拷贝构造到临时对象s里(传值传参需要调用拷贝构造函数),再将s里的内容和赋值左边的对象里的内容交换,由s将和赋值左边对象交换过来的内容释放(即释放左边的对象里的内容)。

交换函数,交换两个对象,使用的是std里的swap交换函数而不是全局的交换函数,因为std里的swap函数是浅拷贝,只是交换内容,减少性能消耗。

五. string模拟实现——其他常用接口

1. c_str()

//返回C语言字符串,让const对象和非const对象都可以调用该函数
const char* c_str() const
{
    return _str;
}

mystring::string s1("hello world");
cout << s1.c_str() << endl;

这里使用const修饰的是*this,即让传过来的对象不能被更改,返回const是为了不让对象里的字符串被修改

2. 重载[ ]

//重载[]
char& operator[](size_t pos)
{
	//防止越界,小于_size
	assert(pos < _size);

	return _str[pos];
}

//重载[]的const版本,需要注意返回值也需要被const修饰
const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

解释:需要重载一个const版本为了让const对象调用,并且返回值是const类型的,防止返回以后的字符还是能被修改

3. size()

//返回字符串长度,const和普通对象都能调用
size_t size() const
{
	return _size;
}

解释:加上const,为了让const对象也能调用

4. capacity()

//返回字符串空间容量,const和普通对象都能调用
size_t capacity() const
{
	return _capacity;
}

解释:加上const,为了让const对象也能调用

5. reserve()

//扩容函数
void reserve(size_t n)
{
	//当n大于容量的时候扩容
	if (n > _capacity)
	{
		char* temp = new char[n + 1];
		//这种复制如果一直不断插入'\0'会出现问题,除非第一次扩的空间足够大
		//事实上,一般也不会不断插入'\0'
		strcpy(temp, _str);
		delete[]_str;
		_str = temp;

		_capacity = n;
	}
}

注意:不能直接先释放_str,如果是自己扩容会出现问题

6. insert()插入单个字符

//insert插入字符
string& insert(size_t pos, char ch)
{
	//越界处理
	assert(pos <= _size);

	if (_size + 1 > _capacity)
    {
		reserve(_size + 1);
	}

	//这样写由于无符号整数的原因,会死循环
	/*size_t end = _size;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		--end;
	}*/

	//注意由于_size是\0,所以这里其实\0也会一起往后移动,不需要自己添加
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;

	return *this;
}

注意:要注意被注释的写法,会死循环

7. insert()插入字符串

//insert插入字符串
string& insert(size_t pos, const char* str)
{
	//判断合法性
	assert(pos <= _size);

	size_t length = strlen(str);
	size_t end = _size + length;

	//判断扩容
	if (end > _capacity)
	{
		reserve(end);
	}

	//当end在pos+length位置时还需要移
	while (end > pos + length - 1)
	{
		_str[end] = _str[end - length];
		--end;
	}

	//从pos位置开始复制
	strncpy(_str + pos, str, length);
	_size += length;

	return *this;
}

注意:循环位置的判断,我们要写的是循环继续的条件,当end大于pos+length-1时,说明还有字符未拷贝到后面,当end到了pos+length-1时,正好在拷贝完的下一个位置,即中断循环,还需要注意拷贝开始的位置

8. push_back()

//实现push_back,插入单个字符函数
void push_back(char ch)
{
	扩容
	//if (_size == _capacity)
	//{
	//	//reserve(_capacity * 2);//当对象是无参构造时_capacity=0
	//	reserve(_capacity == 0 ? 4 : _capacity * 2);
	//}

	//_str[_size] = ch;
	//++_size;
	最后需加上'\0'
	//_str[_size] = '\0';

	//复用insert
	insert(_size, ch);
}

解释:这里可以复用insert插入单个字符,或者是自己写,自己写不复用需要注意不能盲目乘以2,不然_capacity是0时就会出现传给reserve是0

9. append()

//实现append,尾部插入字符串
void append(const char* str)
{
	str长度
	//int length = strlen(str) + _size;
	length大于容量需要扩容
	//if (length > _capacity)
	//{
	//	reserve(length);
	//}

	//strcpy(_str + _size, str);
	//_size = length;

	//复用insert
	insert(_size, str);
}

解释:同样是可以复用insert,如果自己写需要注意拷贝开始的位置

10. 重载+=字符

//+=字符重载
string& operator+=(char ch)
{
    push_back(ch);

	return *this;
}

解释:复用一下push_back即可

11. 重载+=字符串

//+=字符串重载
string& operator+=(const char* str)
{
	append(str);

	return *this;
}

解释:复用一下append即可

12. resize()

//扩空间+初始化
//删除部分数据,保留前n个
void resize(size_t n, char ch = '\0')
{
	//当n小于有效数据个数时,减少有效数据个数
	if (n < _size)
	{
		_str[n] = ch;
		_size = n;
	}
	//当n大于容量时需要扩容并用ch初始化
	else
	{
		reserve(n);
		for (size_t i = _size; i < n; ++i)
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\0';
	}
}

注意:resize扩空间是需要初始化多余的空间的,resize只会缩小有效字符个数_size,不会缩小容量,也就是说_capacity是不会被改变的

13. erase()

//删除pos位置的字符或者是字符串
string& erase(size_t pos, size_t len = npos)
{
	//判断越界情况
	assert(pos < _size);

	//当长度大于字符串长度即全部删除
	if (len == npos || len + pos >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	//长度小于字符串
	else
	{
		size_t begin = pos + len;
		while (begin <= _size)
		{
			_str[begin - len] = _str[begin];
			++begin;
		}
		_size = pos;
	}

	return *this;
}

解释:分类讨论删除的情况,确定要删除的位置

14. find()查找字符

//查找字符,pos为开始找的位置,如果没有提供,则默认从0位置处开始寻找
size_t find(char ch, size_t pos = 0)
{
	for (; pos < _size; ++pos)
	{
		if (_str[pos] == ch)
			return pos;
	}

	//找不到需要返回npos
	return npos;
}

注意:需要注意的是找不到需要返回npos,否则返回下标,npos在上面已经声明和定义

15. find()查找字符串

//查找字符串,pos为开始找的位置,如果没有提供,则默认从0位置处开始寻找,找到则返回第一个字符的下标
size_t find(const char* str, size_t pos = 0)
{
	char* p = strstr(_str + pos, str);
	if (p == nullptr)
	{
		return npos;
	}

	return p - _str;
}

解释:直接套用strstr库函数查找,找不到strstr返回nullptr,需要我们另外判断并返回pos,利用指针减指针返回两个指针之间的距离即下标

15. clear()

//清理对象
void clear()
{
	_str[0] = '\0';
	_size = 0;
}

解释:直接使用\0来清理

16. 迭代器

//迭代器,提供const防止数据被篡改
typedef char* iterator;
typedef const char* const_iterator;

const_iterator begin() const
{
	return _str;
}

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

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

解释:string的迭代器就是字符指针typedef后来的,提供const版本是为了const对象也可以使用迭代器,需要注意的是begin和end不能更改,因为使用范围for遍历时是替换成迭代器的,但是范围for里的begin和end是固定这样的,不能更改为其他函数名,甚至大写也不行,否则会报错

17. ==、!=、>=,>,<=,<逻辑操作符的重载

//操作符重载,使用c_str()函数不需要访问私有成员,可以不使用友元
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 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 !(s1 < s2);
}

解释:这些重载在类外,由于使用了c_str()函数,所以不需要访问私有成员,也就不用使用友元

18. 重载<<

//重载<<
ostream& operator<<(ostream& out, const string& s)
{
	//不使用下面这种方法主要是因为某些特殊情况下会出问题,如需要打印\0
	/*out << s.c_str();
	return out;*/

	for (auto ch : s)
	{
		out << ch;
	}

	return out;
}

解释:重载在类外,虽然实现了c_str(),但是在vs2019使用c_str()会出现不可见字符(\0)无空位的情况,2013中可见,所以采用范围for的方式打印

19. 重载>>

//重载>>
istream& operator>>(istream& in, string& s)
{
	//当s有初始值时清理s
	s.clear();

	//使用istream的get函数一个一个的获取字符
	char ch = in.get();

	//先存储到字符数组里
	char buff[128] = {'\0'};

	//当字符数组里被存满且未满足条件时,就放到s里并清空数组
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		//预留\0,字符串结尾标志
		if (i == 127)
		{
			//满了放入s里
			s += buff;
			//清空并用\0初始化
			memset(buff, '\0', 127);
			//清零重来
			i = 0;
		}

		ch = in.get();
	}

	//将输入的字符放入s里
	s += buff;

	return in;
}

解释:重载在类外,注意需要先清理一下对象,使用先存入数组的方式是为了减少某些情况下不断扩容的带来的性能消耗

六. 了解部分——写时拷贝

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

由于使用了引用计数,这就就解决了析构的问题,但是一个对象被修改了是会影响其他对象的。

其实,核心理念就是:延迟拷贝,如果没有写,那就不会拷贝,就赚到了

想了解更多看这里——写时拷贝

还有写时拷贝读取时的缺陷——写时拷贝在读取时的缺陷

更多的string看这里——面试中string的一种正确写法

吐槽向——STL中的string怎么啦?

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hiland.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值