【C++】模拟实现STL中的string类

目录

一、前言

二、模拟实现

2.1 成员变量和成员常量

2.2 正向迭代器

2.3 容量相关

2.3.1 一些简单函数

2.3.2 申请容量 —— reserve

2.3.3 改变字符串的有效长度 —— resize

2.4 元素访问

2.4.1 通过[](下标)访问字符串 —— operator[]

2.5 修改相关

2.5.1 尾插字符 —— push_back

2.5.2 尾插字符串 —— append

2.5.3 字符和字符串都能尾插 —— operator+=

2.5.4 在特定位置插入 —— insert

2.5.5 尾删字符 —— pop_back

2.5.6 在特定位置删除字符或字符串 —— erase

2.5.7 交换两个字符串 —— swap

2.6 字符串操作

2.6.1 将string转化为C类型字符串 —— c_str

2.6.2 在string中查找字符或字符串 —— find

2.6.3 在string中截取字符串 —— substr

2.7 非成员函数重载

2.7.1 关系操作符

2.7.2 流插入运算符 —— operator<<

2.7.3 流提取运算符 —— operator>>

2.7.4 输入一行数据 —— getline

2.8 构造函数和析构函数

2.9 赋值运算符重载 —— operator=

三、源码

3.1 string.h

3.2 string.cpp


一、前言

        在实现string类时,我采用了定义与声明分离(多文件)的方式,所以下文附上代码时我会将声明和定义一起贴上。并且为了不与std命名空间中的string冲突,所以我定义了一个自己的命名空间simulation,并把string类放在其中模拟实现。并且我没有展开std命名空间(using namespace std;)所以string模拟代码实现中,std中的函数等内容带上了作用域解析符(如 std::strlen())。很抱歉为您的阅读造成不便。

        如果文章有错误还请指出!

        注:string类的模拟中,只有成员变量被public修饰,其余要不为public属性,要不在类外。


二、模拟实现

2.1 成员变量和成员常量

namespace simulation {
	class string {
    public:
		/*
		* 成员常量
		*/

		//只有const修饰的静态成员变量才能直接赋初值
		//否则是要在类外面赋值的 因为类里面只是声明 外面才是定义
		const static size_t npos = -1;

	private:
		/*
		* 成员变量
		*/
		//为了契合C-style-string 所以会在字符串后面跟一个'\0' 
        //但是这个'\0'并不会计入_size和_capacity
		//而是偷偷的开一个额外空间来存储'\0'

		char*  _str; //指向存放字符串的空间的首地址
		size_t _size; //记录字符串的有效长度(不计入'\0' )
		size_t _capacity; //记录字符串的容量(最大有效长度,不计入'\0')
	};
}

        npos虽然表面上赋值为-1,但因为它的数据类型为size_t(无符号整形),所以它是一个非常大的整数。并且因为npos公开的静态成员,所以它是属于string类的而不是属于某一个对象的,这就让我么可以直接通过类来获取npos(string::npos),而不用麻烦的去创建一个对象了。


2.2 正向迭代器

/*
* 迭代器
*/
typedef char* iterator;
typedef const char* const_iterator;

//可读可写
iterator begin();
iterator end();
//可读不可写
const_iterator begin() const;
const_iterator end() const;


string::iterator string::begin()
{
	//begin指向字符串的第一个字符 而_str也指向字符串的第一个字符
	return _str;
}

string::const_iterator string::begin() const
{
	return _str;
}

string::iterator string::end()
{
	//end指向字符串最后一个字符的后面一个位置 最后一个位置的下标的size - 1 所以end的下标为size
	//因此end可以看作&_str[size] -> &*(_str + size) -> _str + size
	return _str + _size;
}

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

        迭代器(iterator)本质上是一个容器的内置类型,它可以是原生指针或者是被封装后的原生指针,而string类中的迭代器就是一个char*类型的原生指针


2.3 容量相关

2.3.1 一些简单函数

//返回字符串的有效长度
size_t size() const;
size_t length() const;

//返回字符串的容量
size_t capacity() const;

//返回字符串的最大容量
ize_t max_size() const;

//清空字符串有效字符
void clear();

//判断字符串是否为空
bool empty() const;


size_t string::size() const
{
	return _size;
}

size_t string::length() const
{
	return _size;
}

size_t string::capacity() const
{
	return _capacity;
}

size_t string::max_size() const
{
	return string::npos;
}

void string::clear()
{
	//只需要把第一个字符换成'\0' 然后把_size标为0 就能达到清空一个字符串的效果
    //因为完成以上操作后 正常使用string类的api就不会访问到那些没有真正清空的内容
	_str[0] = '\0';
	_size = 0;
	//清空字符串只是清空字符串的内容和有效长度 容量并不会清空 所以不需要对容量做处理
}

bool string::empty() const
{
	//当字符串为空时 _size == 0 否则不为0
	return _size == 0;
}

2.3.2 申请容量 —— reserve

//申请扩容 只会改字符串容量而不会改内容和长度
void reserve(size_t n = 0);

void string::reserve(size_t n)
{
    //只有申请的容量大于字符串的容量才会扩容
    //即不会出现容量缩小的情况
	if (n > _capacity)
	{
        //开辟一块大小为n + 1的新空间存放原来空间的数据并预留空间
        //额外开的一个空间是为了存放'\0' 因为这个'\0'并不归属于字符串 不计入_size 和 _capacity
        //所以需要我们自己偷偷开空间自行管理
		char* temp = new char[n + 1];
		std::strcpy(temp, _str);

        //一定要先释放原来的空间 不然原来的空间在_str的指向被替换后就找不到了
        //这样会造成内存泄漏
        delete[] _str;

        //修改_str指向和展示给用户的容量大小
		_str = temp;
		_capacity = n;
	}
}

        因为我们的要求是模拟实现std::string,所以也要模仿它api的作用效果 —— 只会扩容而不会缩容

        以下为std::stringreserve()的文档描述:


2.3.3 改变字符串的有效长度 —— resize

        std::stringresize()的文档描述为: 

        从文档中我们可以发现,resize()既可以缩容也可以扩容,其中扩容的情况有会影响到_capacity,所以resize()的作用效果有三种情况:

  • 情况一:n < _size 只保留n个字符 其他全部丢弃;
  • 情况二:n > _size && n =< _capacity 用字符c来延伸n - _size个字符 不需要扩容;
  • 情况三:n > _size && n > _capacity 同情况二 但需要扩容。
//改变字符串的长度 一般不会改变字符串的容量
//通过缺省参数来达到文档中不传入字符c就用'\0'填充的效果
void resize(size_t n, char c = '\0');

void string::resize(size_t n, char c)
{
	//resize分为3种情况
	//一、n < _size 只保留n个字符 其他全部丢弃
	//二、n > _size && n =< _capacity 用字符c来延伸n - _size个字符 不需要扩容
	//三、n > _size && n > _capacity 同二但需要扩容

	if (n < _size)
	{
		//情况一
		//第n个字符的下标为n - 1 我们只需将下标为n的位置改为'\0'就能达到删除效果
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		//第二、第三种情况一起处理
		//reserve只有当n > _capacity时才会进行扩容 所以当n = _capacity时并不会起任何作用
		reserve(n);

		std::memset(_str + _size, c, n - _size);
		_size = n;
		_str[_size] = '\0';
	}
}

2.4 元素访问

2.4.1 通过[](下标)访问字符串 —— operator[]

        因为string类类似于顺序表是使用数组实现的,所以可以用下标来访问字符串来得到其中的数据。

//可读可写
char& operator[](size_t pos);
//可读不可写
const char& operator[](size_t pos) const;

char& string::operator[](size_t pos)
{
	assert(pos >= 0 && pos <= _size - 1);

	return _str[pos];
}

const char& string::operator[](size_t pos) const
{
	assert(pos >= 0 && pos <= _size - 1);

	return _str[pos];
}

2.5 修改相关

2.5.1 尾插字符 —— push_back

void push_back(char c);

void string::push_back(char c)
{
	if (_size == _capacity)
	{
		//当字符串为空串时_capacity == 0 为了应对对空串插入的情况要进行判断 
        //当_capacity为0时就对_capacity赋初值 否则新的容量就为原容量的2倍
		reserve(_capacity == 0 ? 5 : _capacity * 2);

		//reserve在申请扩容时会对_capacity进行修改 所以后续不再需要修改_capacity
	}

	_str[_size++] = c;

	//要在字符串末尾补'\0'
	//因为在申请空间时每次都会多申请一个空间 且这个空间没有记录在_capacity中
	//所以不存在 在_size++后因为_size == _capacity而没有位置存放'\0'的情况
	_str[_size] = '\0';
}

2.5.2 尾插字符串 —— append

        红框中为模拟实现的两个。

//string
string& append(const string& str);
//C-string
string& append(const char* s);

string& string::append(const string& str)
{
	assert(str._str);

	if (str._size + _size > _capacity)
	{
		reserve(str._size + _size);
	}

	//str自带'\0' 不需要我们自行补充
    //strcat会找到_str的'\0' 然后从这个位置开始把str._str拷贝过去 
    //拷贝完str._str的'\0'结束
	std::strcat(_str, str._str);
	_size += str._size;

	return *this;
}

//与string版的实现大同小异
string& string::append(const char* s)
{
	assert(s);
	size_t len = std::strlen(s);

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

    //s同样自带'\0'
	std::strcat(_str, s);
	_size += len;

	return *this;
}

2.5.3 字符和字符串都能尾插 —— operator+=

        +=在重载后能够完成大部分push_backappend的任务,并且简单好用,所以+=十分常用。并且+=可以通过复用push_backappend来实现。

string& operator+=(const string& str);
string& operator+=(const char* s);
string& operator+=(char c);

string& string::operator+=(const string& str)
{
	assert(str._str);

	append(str);

	return *this;
}

string& string::operator+=(const char* s)
{
	assert(s);

	append(s);

	return *this;
}

string& string::operator+=(char c)
{
	push_back(c);

	return *this;
}

2.5.4 在特定位置插入 —— insert

string& insert(size_t pos, const string& str);
string& insert(size_t pos, const char* s);
string& insert(size_t pos, size_t n, char c);

string& string::insert(size_t pos, const string& str)
{
    //当pos == _size就相当于尾插
	assert(pos >= 0 && pos <= _size);
	assert(str._str);

    //如果插入之后字符串的有效长度大于容量就需要扩容
	if (_size + str._size > _capacity)
	{
        //至少扩大到有效长度
		reserve(_size + str._size);
	}

	size_t end = _size;
    //将插入位置后的数据后移 包括末尾的'\0'
    //一定要从'\0'处开始后移 不然可能会对原来的数据进行覆盖
	while (end >= pos && end != npos)
	{
		_str[end + str._size] = _str[end];
		--end;
	}

    //这里拷贝了str._size个字符 也就是_str的全部字符但不包括末尾的'\0'
	std::memcpy(_str + pos, str._str, str._size);
	_size += str._size;

	return *this;
}

string& string::insert(size_t pos, const char* s)
{
	assert(pos >= 0 && pos <= _size);
	assert(s);
	
	size_t len = std::strlen(s);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	size_t end = _size;
	while (end >= pos && end != string::npos)
	{
		_str[end + len] = _str[end];
		--end;
	}

	std::memcpy(_str + pos, s, len);
	_size += len;

	return *this;
}

string& string::insert(size_t pos, size_t n, char c)
{
	//pos == _size时相当于尾插
	assert(pos >= 0 && pos <= _size);
	
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}

	size_t end = _size;
	while (end >= pos && end != string::npos)
	{
		_str[end + n] = _str[end];
		--end;
	}

	//因为前面后移时把'\0'也后移了 所以不需要补充'\0'
	std::memset(_str + pos, c, n);
	_size += n;

	return *this;
}
size_t end = _size;
while (end >= pos && end != npos)
{
	_str[end + str._size] = _str[end];
	--end;
}

         你可能会对这里循环条件的end != npos感到困惑,其实这个条件是为了处理头插的情况。我们就假设一次头插且没有条件end != npos

头插(pos = 0)

(假设_size = 3)

end(end = _size)

进入循环前

end

出循环后

第一次进入循环32
第二次进入循环21
第三次进入循环10
第四次进入循环0-1(?)
第五次进入循环(会进入循环吗?)-1(?)

        我们从表中可以看见在进行头插时,最后end会变成-1,按常理来说此时end已经不符合循环条件end >= pos了,应该会退出循环然后完美地结束循环才对啊,这有什么问题吗?

那问题可大着了,别忘了end的数据类型可是size_t(无符号整形)end是不可能为-1的!!!end最后会变成一个非常大的整数,但恰好这个值就是就是string::npos的值,所以我们也让end != npos来当另一个循环条件,如果end == npos了也就说明头插已经结束了,此时就可以退出循环了。


2.5.5 尾删字符 —— pop_back

void push_back(char c);

void string::pop_back()
{
    //删除数据的前提是有数据可以删除
	if (_size > 0)
	{
		_str[--_size] = '\0';
	}
}

2.5.6 在特定位置删除字符或字符串 —— erase

string& erase(size_t pos, size_t len = string::npos);

string& string::erase(size_t pos, size_t len)
{
	//删除有两种情况 一种是把pos后面只删了一部分 另一种是把pos后面的内容全删了
	assert(pos >= 0 && pos <= _size - 1);

	if (len == string::npos || pos + len >= _size)
	{
		//第一种情况
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		//第二种情况
		std::strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}

	return *this;
}

2.5.7 交换两个字符串 —— swap

        string类字符串有三种属性,要达到交换字符串的效果就需要把三种属性都进行交换。

void swap(string& str);

void string::swap(string& str)
{
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}

2.6 字符串操作

2.6.1 将string转化为C类型字符串 —— c_str

        string类中的_str是一个char*类型的指针,它指向的是存放字符的连续空间,并且在这个空间的后面一定有一个'\0'。所以当string类中只有_str时,这难道不就是C类型的字符串吗?因此要将string转化为C类型字符串时,我们只需要单独将_str拿出来就好了。

const char* c_str() const;

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

2.6.2 在string中查找字符或字符串 —— find

size_t find(char c, size_t pos = 0) const;
size_t find(const string& str, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;

size_t string::find(char c, size_t pos) const
{
	assert(pos >= 0 && pos <= _size - 1);

    //从pos位置开始遍历
	for (size_t i = pos; i <= _size - 1; ++i)
	{
		if (_str[i] == c)
		{
            //如果找到了就返回下标
			return i;
		}
	}

    //没找到就返回npos
	return string::npos;
}

size_t string::find(const string& str, size_t pos) const
{
	assert(str._str);
	assert(pos >= 0 && pos <= _size - 1);

    //strstr如果找到了就会返回母串中相应字符串第一个字符的地址 没找到就会返回空指针
	const char* p = std::strstr(_str + pos, str._str);

    //找到了就返回母串中相应字符串的第一个字符 没找到就返回npos
	return p == nullptr ? string::npos : p - _str;
}

size_t string::find(const char* s, size_t pos) const
{
	assert(s);
	assert(pos >= 0 && pos <= _size - 1);

	const char* p = std::strstr(_str + pos, s);

	return p == nullptr ? string::npos : p - _str;
}


2.6.3 在string中截取字符串 —— substr

string substr(size_t pos = 0, size_t len = string::npos) const;

string string::substr(size_t pos, size_t len) const
{
	assert(pos >= 0 && pos <= _size - 1);

	//ans为局部变量 在出函数时会自行调用析构函数
	string ans;

	//当len == string::npos || pos + len >= _size == true时为第一种情况:
    //将pos后面的位置全部复制
	//反之为第二种情况:将pos后面的位置部分复制
	(len == string::npos || pos + len >= _size) ? 
    ans.reserve(_size - pos) : ans.reserve(len);

	//两种情况不同的地方只有开的空间大小不同 截取字串都可以通过+=来完成 所以一起完成
    //遍历被截取部分的每一个字符 将这个字符尾插到新字符串中
	for (size_t i = pos; i <= _size; ++i)
	{
		ans += _str[i];
	}

	return ans;
}

2.7 非成员函数重载

2.7.1 关系操作符

        注:这个我不小心写成了成员函数,非常抱歉!

        关系操作符看起来很多,但其实我们只需要完成两个,剩下的全部复用完成的两个就行

bool operator==(const string& str) const;
bool operator!=(const string& str) const;
bool operator>(const string& str) const;
bool operator>=(const string& str) const;
bool operator<=(const string& str) const;
bool operator<(const string& str) const;

bool string::operator==(const string& str) const
{
	return _size == str._size
		&& std::strcmp(_str, str._str) == 0;
}

bool string::operator>(const string& str) const
{
	return std::strcmp(_str, str._str) > 0;
}

bool string::operator>=(const string& str) const
{
	return _str > str._str || _str == str._str;
}

bool string::operator!=(const string& str) const
{
	return !(_str == str._str);
}

bool string::operator<(const string& str) const
{
	return !(_str >= str._str);
}

bool string::operator<=(const string& str) const
{
	return !(_str > str._str);
}

2.7.2 流插入运算符 —— operator<<

//该操作符(函数)的声明在命名空间外
std::ostream& operator<<(std::ostream& os, const simulation::string& str);

//虽然在string类外实现 但却不需要作为simulation::string类的友元函数
//因为该函数并没有直接访问string类的私有成员 而是通过string类公开的api实现的访问
std::ostream& operator<<(std::ostream& os, const string& str)
{
	assert(str.c_str());

	//有迭代器就支持范围for循环
	for (auto& ch : str)
	{
		os << ch;
	}

	return os;
}

2.7.3 流提取运算符 —— operator>>

std::istream& operator>>(std::istream& in, simulation::string& str);

std::istream& operator>>(std::istream& in, simulation::string& str)
{
	//如果对已经有内容的字符串进行输入的话 会先清空内容再重新赋值
	str.clear();

    //get()会一个一个字符地从输入缓冲区中读取数据(包括' '和'\n')
	char ch = in.get();

	//处理有效字符前的空白字符
	//如果输入的只有空白字符 当把这些空白字符读取完毕的时候 
    //get()会返回EOF 此时就不会进入循环了
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}

	//创建一个缓冲区 避免每插入一个字符就扩一次容造成性能损失
	char buffer[128] = { 0 };
	size_t i = 0;
	//cin读取到' '(空格) 或 '\n'就会停止读取
	while (ch != ' ' && ch != '\n')
	{
		buffer[i++] = ch;

		//为'\0'预留一个空间
		if (i == 127)
		{
			buffer[i] = '\0';
			str += buffer;
			i = 0;
		}

		ch = in.get();
	}

	//当i != 0时 意味着缓冲区内还有数据 要把这些数据也输入进字符串
	if (i)
	{
		buffer[i] = '\0';
		str += buffer;
	}

	return in;
}

2.7.4 输入一行数据 —— getline

        getline的实现几乎和operator>>一模一样,只是少了读取到' '就停止的限制。

std::istream& getline(std::istream& in, simulation::string& str);

std::istream& getline(std::istream& in, simulation::string& str)
{
	//getline可以读取一行

	str.clear();

	char buffer[128] = { 0 };
	size_t i = 0;

	char ch = in.get();
	while (ch != '\n')
	{
		buffer[i++] = ch;

		//为'\0'预留一个空间
		if (i == 127)
		{
			buffer[i] = '\0';
			str += buffer;
			i = 0;
		}

		ch = in.get();
	}
	if (i)
	{
		buffer[i] = '\0';
		str += buffer;
	}

	return in;
}

2.8 构造函数和析构函数

//默认构造
string();
//C类型字符串构造
string(const char* s);
//深度拷贝构造
string(const string& str);

string::string()
	: _str(new char[1])
	, _size(0)
	, _capacity(0)
{
	_str[0] = '\0';
}

string::string(const char* s)
	//strlen只会计算C类型字符串的有效长度(不计入'\0')
	//额外开一个空间给'\0'
	: _str(new char[std::strlen(s) + 1])

	//'\0'的空间并不计入_size和_capacity中 所以就按照s的长度来初始化
	, _size(strlen(s))
	, _capacity(_size)
{
	//s是C类型字符串 自带'\0'
	//strcpy会先自行拷贝一个'\0'然后再结束拷贝
	std::strcpy(_str, s);
}

//深拷贝拷贝构造的传统写法
//string::string(const string& str)
//	//注意这里是str._capacity + 1 而不是str._size + 1 
//  //因为size是字符串的当前长度 而capacity是字符串的最大长度
//	//我们要开够字符串的极限长度 并且预留一个'\0'的空间
//	: _str(new char[str._capacity + 1])
//	, _size(str._size)
//	, _capacity(str._capacity)
//{
//	assert(str._str);
//
//	std::strcpy(_str, str._str);
//}

//深拷贝拷贝构造的现代写法
string::string(const string& str)
	: _str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string temp(str._str);
	swap(temp);
}

        拷贝构造有着传统现代两种写法,传统写法就是像用C类型字符串来构造一样,什么都由自己实现,从0开始完成对一个字符串内容属性的拷贝。而现代写法就是一种狸猫换太子的方法,因为用C类型字符串来构造也是一种深拷贝,我们就直接复用C类型字符串的构造方法,借助它来完成深拷贝然后与我们构建的一个什么都没有的字符串交换,这样我们可以毫不费力地得到一个深拷贝拷贝构造了,并且因为temp是局部变量,它出了作用域后就会自动调用析构函数释放空间。这样我们构建的那个什么都没有的字符串temp也会帮我们释放掉,一举两得。

~string();

string::~string()
{
	//在释放一连串的空间时需要用 delete[] 而不能用 delete
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

2.9 赋值运算符重载 —— operator=

string& operator=(const string& str);

//传统写法
//string& operator=(const string& str)
//{
//	//如果等号两边为同一个字符串 就不进行赋值操作
//	if (this != &str)
//	{
//		delete[] _str;
//		_str = new char[str._capacity + 1];
//		std::strcpy(_str, str._str);
//
//		_size = str._size;
//		_capacity = str._capacity;
//	}
//
//	return *this;
//}

//现代写法
string& string::operator=(const string& str)
{
	if (this != &str)
	{
        //直接复用拷贝构造 然后再进行交换
		string temp(str);
		
		swap(temp);//this->swap(temp);
	}

	return *this;
}

//现代写法变形
//因为变形的参数并不是引用 所以在传参时就会调用一次拷贝构造
//相当于把现代写法函数体内的拷贝构造移到了传参过程中
//string& operator=(string temp)
//{
//	swap(temp);
//	return *this;
//}

三、源码

3.1 string.h

#include <iostream>
#include <algorithm>
#include <string>
#include <cassert>

namespace simulation {
	class string {
	public:
		/*
		* 构造函数
		*/

		//默认构造
		string();
		//C类型字符串构造
		string(const char* s);
		//深度拷贝构造
		string(const string& str);

		/*
		* 析构函数
		*/

		~string();

		/*
		* 迭代器
		*/

		typedef char* iterator;
		typedef const char* const_iterator;

		//可读可写
		iterator begin();
		iterator end();
		//可读不可写
		const_iterator begin() const;
		const_iterator end() const;

		/*
		* 容量操作
		*/

		size_t size() const;
		size_t length() const;
		size_t capacity() const;
		size_t max_size() const;

		//清空字符串有效字符
		void clear();

		//判断字符串是否为空
		bool empty() const;

		//申请扩容 只会改字符串容量而不会改内容和长度
		void reserve(size_t n = 0);

		//改变字符串的长度 一般不会改变字符串的容量
		void resize(size_t n, char c = '\0');



		/*
		* 元素访问
		*/

		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

		/*
		* 修改操作
		*/

		void push_back(char c);
		string& append(const string& str);
		string& append(const char* s);
		string& operator+=(const string& str);
		string& operator+=(const char* s);
		string& operator+=(char c);
		void swap(string& str);
		void pop_back();
		string& insert(size_t pos, const string& str);
		string& insert(size_t pos, const char* s);
		string& insert(size_t pos, size_t n, char c);
		string& erase(size_t pos, size_t len = string::npos);
		string& operator=(const string& str);


		/*
		* 字符串操作
		*/

		const char* c_str() const;
		size_t find(char c, size_t pos = 0) const;
		size_t find(const string& str, size_t pos = 0) const;
		size_t find(const char* s, size_t pos = 0) const;
		string substr(size_t pos = 0, size_t len = string::npos) const;

		/*
		* 非成员函数重载
		* 这里我写错了 我写成了成员函数
		*/
		bool operator==(const string& str) const;
		bool operator!=(const string& str) const;
		bool operator>(const string& str) const;
		bool operator>=(const string& str) const;
		bool operator<=(const string& str) const;
		bool operator<(const string& str) const;


		/*
		* 成员常量
		*/

		//只有const修饰的静态成员变量才能直接赋初值
		//否则是要在类外面赋值的 因为类里面只是声明 外面才是定义
		const static size_t npos = -1;

	private:
		//为了契合C-style-string 所以会在字符串后面跟一个'\0' 但是这个'\0'并不会计入size和capacity
		//而是偷偷的开一个额外空间来存储'\0'
		char*  _str;
		size_t _size;
		size_t _capacity;
	};
}

std::ostream& operator<<(std::ostream& os, const simulation::string& str);
std::istream& operator>>(std::istream& in, simulation::string& str);
std::istream& getline(std::istream& in, simulation::string& str);

3.2 string.cpp

#include "string.h"
using namespace simulation;

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

size_t string::size() const
{
	return _size;
}

size_t string::length() const
{
	return _size;
}

size_t string::capacity() const
{
	return _capacity;
}

bool string::empty() const
{
	//当字符串为空时 _size == 0 否则不为0
	return _size == 0;
}

string::iterator string::begin()
{
	//begin指向字符串的第一个字符 而_str也指向字符串的第一个字符
	return _str;
}

string::const_iterator string::begin() const
{
	return _str;
}

string::iterator string::end()
{
	//end指向字符串最后一个字符的后面一个位置 最后一个位置的下标的size - 1 所以end的下标为size
	//所以end可以看作&_str[size] -> &*(_str + size) -> _str + size
	return _str + _size;
}

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

string::string()
	: _str(new char[1])
	, _size(0)
	, _capacity(0)
{
	_str[0] = '\0';
}

string::string(const char* s)
	//strlen只会计算C类型字符串的有效长度(不计入'\0')
	//额外开一个空间给'\0'
	: _str(new char[std::strlen(s) + 1])

	//'\0'的空间并不计入_size和_capacity中 所以就按照s的长度来初始化
	, _size(strlen(s))
	, _capacity(_size)
{
	//s是C类型字符串 自带'\0'
	//strcpy会先自行拷贝一个'\0'然后再结束拷贝
	std::strcpy(_str, s);
}

//深拷贝的传统写法
//string::string(const string& str)
//	//注意这里是str._capacity + 1 而不是str._size + 1 因为size是字符串的当前长度 而capacity是字符串的最大长度
//	//我们要开够字符串的极限长度 并且预留一个'\0'的空间
//	: _str(new char[str._capacity + 1])
//	, _size(str._size)
//	, _capacity(str._capacity)
//{
//	assert(str._str);
//
//	std::strcpy(_str, str._str);
//}

//深拷贝的现代写法
string::string(const string& str)
	: _str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string temp(str._str);
	swap(temp);
}

string::~string()
{
	//在释放一连串的空间时需要用 delete[] 而不能用 delete
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

void string::clear()
{
	//只需要把第一个字符换成'\0' 然后把_size标为0 就能达到清空一个字符串的效果
	//清空字符串只是清空字符串的内容和有效长度 容量并不会清空
	_str[0] = '\0';
	_size = 0;
}

char& string::operator[](size_t pos)
{
	assert(pos >= 0 && pos <= _size - 1);

	return _str[pos];
}

const char& string::operator[](size_t pos) const
{
	assert(pos >= 0 && pos <= _size - 1);

	return _str[pos];
}

void string::reserve(size_t n)
{
	//只有申请的容量大于字符串的容量才会扩容
	//即不会出现容量缩小的情况
	if (n > _capacity)
	{
		//开辟一块大小为n + 1的新空间存放原来空间的数据并预留空间
		//额外开的一个空间是为了存放'\0' 因为这个'\0'并不归属于字符串 不计入_size 和 _capacity
		//所以需要我们自己偷偷开空间自行管理
		char* temp = new char[n + 1];
		std::strcpy(temp, _str);

		//一定要先释放原来的空间 不然原来的空间在_str的指向被替换后就找不到了
		//这样会造成内存泄漏
		delete[] _str;

		//修改_str指向和展示给用户的容量大小
		_str = temp;
		_capacity = n;
	}
}

void string::resize(size_t n, char c)
{
	//resize分为3种情况
	//一、n < _size 只保留n个字符 其他全部丢弃
	//二、n > _size && n =< _capacity 用字符c来延伸n - _size个字符 不需要扩容
	//三、n > _size && n > _capacity 同二但需要扩容

	if (n < _size)
	{
		//情况一
		//第n个字符的下标为n - 1 我们只需将下标为n的位置改为'\0'就能达到删除效果
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		//第二、第三种情况一起处理
		//reserve只有当n > _capacity时才会进行扩容 所以当n = _capacity时并不会起任何作用
		reserve(n);
		std::memset(_str + _size, c, n - _size);
		_size = n;
		_str[_size] = '\0';
	}
}

void string::push_back(char c)
{
	if (_size == _capacity)
	{
		//当字符串为空串时 _capacity == 0 为了应对对空串插入的情况要进行判断 当_capacity为0时就对_capacity赋初值
		//reserve在申请扩容时会对_capacity进行修改 所以后续不再需要修改_capacity
		reserve(_capacity == 0 ? 5 : _capacity * 2);
	}
	_str[_size++] = c;

	//要在字符串末尾补'\0'
	//因为在申请空间时每次都会多申请一个空间 且这个空间没有记录在_capacity中
	//所以不存在 在_size++后因为_size == _capacity而没有位置存放'\0'的情况
	_str[_size] = '\0';
}

string& string::append(const string& str)
{
	assert(str._str);

	if (str._size + _size > _capacity)
	{
		reserve(str._size + _size);
	}

	//str自带'\0' 不需要我们自行补充
	//strcat会找到_str的'\0' 然后从这个位置开始把str._str拷贝过去 
	//拷贝完str._str的'\0'结束
	std::strcat(_str, str._str);
	_size += str._size;

	return *this;
}

string& string::append(const char* s)
{
	assert(s);
	size_t len = std::strlen(s);

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

	std::strcat(_str, s);
	_size += len;

	return *this;
}

string& string::operator+=(const string& str)
{
	assert(str._str);

	append(str);

	return *this;
}

string& string::operator+=(const char* s)
{
	assert(s);

	append(s);

	return *this;
}

string& string::operator+=(char c)
{
	push_back(c);

	return *this;
}

void string::swap(string& str)
{
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}

size_t string::max_size() const
{
	return string::npos;
}

void string::pop_back()
{
	if (_size > 0)
	{
		_str[--_size] = '\0';
	}
}

string& string::insert(size_t pos, const string& str)
{
	//当pos == _size就相当于尾插
	assert(pos >= 0 && pos <= _size);
	assert(str._str);

	//如果插入之后字符串的有效长度大于容量就需要扩容
	if (_size + str._size > _capacity)
	{
		//至少扩大到有效长度
		reserve(_size + str._size);
	}

	size_t end = _size;
	//将插入位置后的数据后移 包括末尾的'\0'
	while (end >= pos && end != npos)
	{
		_str[end + str._size] = _str[end];
		--end;
	}

	//这里拷贝了str._size个字符 也就是_str的全部字符但不包括末尾的'\0'
	std::memcpy(_str + pos, str._str, str._size);
	_size += str._size;

	return *this;
}

string& string::insert(size_t pos, const char* s)
{
	assert(pos >= 0 && pos <= _size);
	assert(s);
	
	size_t len = std::strlen(s);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	size_t end = _size;
	while (end >= pos && end != string::npos)
	{
		_str[end + len] = _str[end];
		--end;
	}

	std::memcpy(_str + pos, s, len);
	_size += len;

	return *this;
}

string& string::insert(size_t pos, size_t n, char c)
{
	//pos == _size时相当于尾插
	assert(pos >= 0 && pos <= _size);
	
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}

	size_t end = _size;
	while (end >= pos && end != string::npos)
	{
		_str[end + n] = _str[end];
		--end;
	}

	//因为前面后移时把'\0'也后移了 所以不需要补充'\0'
	std::memset(_str + pos, c, n);
	_size += n;

	return *this;
}

string& string::erase(size_t pos, size_t len)
{
	//删除有两种情况 一种是把pos后面只删了一部分 另一种是把pos后面的内容全删了
	assert(pos >= 0 && pos <= _size - 1);

	if (len == string::npos || pos + len >= _size)
	{
		//第一种情况
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		//第二种情况
		std::strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}

	return *this;
}

size_t string::find(char c, size_t pos) const
{
	assert(pos >= 0 && pos <= _size - 1);

	for (size_t i = pos; i <= _size - 1; ++i)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}

	return string::npos;
}

size_t string::find(const string& str, size_t pos) const
{
	assert(str._str);
	assert(pos >= 0 && pos <= _size - 1);

	const char* p = std::strstr(_str + pos, str._str);

	return p == nullptr ? string::npos : p - _str;
}

size_t string::find(const char* s, size_t pos) const
{
	assert(s);
	assert(pos >= 0 && pos <= _size - 1);

	const char* p = std::strstr(_str + pos, s);

	return p == nullptr ? string::npos : p - _str;
}

std::ostream& operator<<(std::ostream& os, const string& str)
{
	assert(str.c_str());

	//有迭代器就支持范围for循环
	for (auto& ch : str)
	{
		os << ch;
	}

	return os;
}

//string string::substr(size_t pos = 0, size_t len = string::npos) const
//{
//	assert(pos >= 0 && pos <= _size - 1);
//	if (len == string::npos || pos + len >= _size)
//	{
//		//将pos后面的位置全部复制
//		char* temp = new char[_size - pos + 1];//留一个位置放'\0'
//		for (size_t i = 0, j = pos; j <= _size; ++i, ++j)
//		{
//			temp[i] = _str[j];
//		}
//
//		string ans(temp);
//		delete[] temp;
//		return ans;
//	}
//
//	//将pos后面的位置部分复制
//	char* temp = new char[len + 1];
//	for (size_t i = 0, j = pos; j < pos + len; ++i, ++j)
//	{
//		temp[i] = _str[j];
//	}
//
//	temp[len] = '\0';
//
//	string ans(temp);
//	delete[] temp;
//
//	return ans;
//}

string string::substr(size_t pos, size_t len) const
{
	assert(pos >= 0 && pos <= _size - 1);

	//ans为局部变量 在出函数时会自行调用析构函数
	string ans;

	//if (len == string::npos || pos + len >= _size)
	//{
	//	//将pos后面的位置全部复制
	//	ans.reserve(_size - pos);
	//	//for (size_t i = 0, j = pos; j <= _size; ++i, ++j)
	//	//{
	//	//	ans[i] = _str[j];
	//	//}
	//}
	//else
	//{
	//	//将pos后面的位置部分复制
	//	ans.reserve(len);
	//	//for (size_t i = 0, j = pos; j < pos + len; ++i, ++j)
	//	//{
	//	//	ans[i] = _str[j];
	//	//}
	//}

	//当len == string::npos || pos + len >= _size == true时为第一种情况:将pos后面的位置全部复制
	//反之为第二种情况:将pos后面的位置部分复制
	(len == string::npos || pos + len >= _size) ? ans.reserve(_size - pos) : ans.reserve(len);

	//两种情况不同的地方只有开的空间大小不同 截取字串都可以通过+=来完成 所以一起完成
	for (size_t i = pos; i <= _size; ++i)
	{
		ans += _str[i];
	}

	return ans;
}

bool string::operator==(const string& str) const
{
	return _size == str._size
		&& std::strcmp(_str, str._str) == 0;
}

bool string::operator>(const string& str) const
{
	return std::strcmp(_str, str._str) > 0;
}

bool string::operator>=(const string& str) const
{
	return _str > str._str || _str == str._str;
}

bool string::operator!=(const string& str) const
{
	return !(_str == str._str);
}

bool string::operator<(const string& str) const
{
	return !(_str >= str._str);
}

bool string::operator<=(const string& str) const
{
	return !(_str > str._str);
}

std::istream& operator>>(std::istream& in, simulation::string& str)
{
	//如果对已经有内容的字符串进行输入的话 会先清空内容再重新赋值
	str.clear();

	char ch = in.get();

	//处理有效字符前的空白字符
	//如果输入的只有空白字符 当把这些空白字符读取完毕的时候 get会返回EOF 此时就不会进入循环了
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}

	//创建一个缓冲区 避免每插入一个字符就扩一次容造成性能损失
	char buffer[128] = { 0 };
	size_t i = 0;
	//cin读取到' '(空格) '\n'就会停止读取
	while (ch != ' ' && ch != '\n')
	{
		buffer[i++] = ch;

		//为'\0'预留一个空间
		if (i == 127)
		{
			buffer[i] = '\0';
			str += buffer;
			i = 0;
		}

		ch = in.get();
	}

	//当i != 0时 意味着缓冲区内还有数据 要把这些数据也输入进字符串
	if (i)
	{
		buffer[i] = '\0';
		str += buffer;
	}

	return in;
}

std::istream& getline(std::istream& in, simulation::string& str)
{
	//getline可以读取一行

	str.clear();

	char buffer[128] = { 0 };
	size_t i = 0;

	char ch = in.get();
	while (ch != '\n')
	{
		buffer[i++] = ch;

		//为'\0'预留一个空间
		if (i == 127)
		{
			buffer[i] = '\0';
			str += buffer;
			i = 0;
		}

		ch = in.get();
	}
	if (i)
	{
		buffer[i] = '\0';
		str += buffer;
	}

	return in;
}

//传统写法
//string& operator=(const string& str)
//{
//	//如果等号两边为同一个字符串 就不进行赋值操作
//	if (this != &str)
//	{
//		delete[] _str;
//		_str = new char[str._capacity + 1];
//		std::strcpy(_str, str._str);
//
//		_size = str._size;
//		_capacity = str._capacity;
//	}
//
//	return *this;
//}

//现代写法
string& string::operator=(const string& str)
{
	if (this != &str)
	{
		string temp(str);
		
		swap(temp);//this->swap(temp);
	}

	return *this;
}

//现代写法变形
//因为变形的参数并不是引用 所以在传参时就会调用一次拷贝构造
//相当于把现代写法函数体内的拷贝构造移到了传参过程中
//string& operator=(string temp)
//{
//	swap(temp);
//	return *this;
//}
  • 37
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值