【C++】string类内部机制的模拟实现

一、前言

  在我们对STL中的string类有了一个基本的认识后,今天我们手动的从0到1去模拟实现一个STL库中的string类中的一些常用接口。

二、string类的模拟实现

为了不和库里的string类产生冲突,我们可以建立一个名为其他的命名空间,此时因为作用域不同,就不会产生冲突。即namespace xxx.

下面是对string类的声明:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

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

namespace zihan//命名空间
{
	class string//定义类
	{
		
	public:
		//构造
		string(const char* str = " ")//全缺省
		{
			_size = strlen(str);
			_capacity = _size;
			//_capacity中不包含\0,为了存放字符 \0  需要长度加一
			_str = new char[_capacity + 1];
			strcpy(_str, str);//拷贝字符串

		}

		//显示调用拷贝构造
		//S2(S1)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;

		}

		 //赋值 S1=S2
        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;
		}

		size_t size()const
		{
			return _size;
		}

		size_t capacity()const
		{
			return _capacity;
		}

		//迭代器
		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;
		}

		//返回pos位置的字符
		const char& operator[](size_t pos)const
		{
			return _str[pos];
		}

		//清掉所有的数据但不动空间
		void clear()
		{
			_str[0] = '\0';
			_size = 0;

		}

		//预留空间
		void reserve(size_t n);

		//尾插字符
		void push_back(char ch);
		//尾插字符串
		void append(const char* str);
		//+=
		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 fing(const char* str, size_t pos = 0);
		//截取字符串
		string substr(size_t pos, size_t len);

		//比较
		bool operator<(const string& s)const;
		bool operator<=(const string& s)const;
		bool operator>(const string& s)const;
		bool operator>=(const string& s)const;
		bool operator==(const string& s)const;
		bool operator!=(const string& s)const;

		


	private:
		char* _str = nullptr;
		size_t _capacity = 0;
		size_t _size = 0;

		static const size_t npos;
	};

	//流插入 流提取
	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& out, string& s);
}

2.1成员函数

2.1.1构造函数

string(const char* str = " ")

string(const char* str = " ")//全缺省
		{
			_size = strlen(str);
			_capacity = _size;
			//_capacity中不包含\0,为了存放字符 \0  需要长度加一
			_str = new char[_capacity + 1];
			strcpy(_str, str);//拷贝字符串

		}

2.1.2拷贝构造函数

string(const string& s)

我们知道在拷贝构造中,若是一个类在没有显示定义拷贝构造,对于内置类型不做处理,对于自定义类型会去调用类中默认提供的拷贝构造函数,此时就会造成浅拷贝问题。

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

深拷贝:源对象和拷贝对象相互独立,其中一个对象的改动不会对另一个对象造成影响

若想进行深拷贝,必须显示定义拷贝构造

1.传统写法

//显示调用拷贝构造
		//S2(S1)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;

		}

传统写法的主要思想是开辟一块足够的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也拷贝过去即可。 

2.现代写法

void swap(string& s)
{
	std::swap(_str , s._str);//调用标准库中的swap函数将对象的成员变量进行交换成员变量
	std::swap(_size , s._size);
	std::swap(_capacity , s._capacity);
}
//现代写法
string(const string& s)
{
	string tmp(s._str);//带参构造
	swap(tmp);//通过交换去掠夺
}

现代写法的主要思想是先根据原字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象进行数据交换。 

2.1.3赋值运算符重载函数

string& operator=(const string& s)

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;
		}

2.现代写法

//S1=S2
//现代写法
string& operator=(const string& s)
{
	//不能自己赋值自己
	if (this != &s)
	{
		string tmp(s._str);
		swap(tmp);
	}

	return *this;
}

这里通过用源对象字符串构造一个tmp对象, tmp对象与s1交换之后,出了作用域也会销毁,可以达到将s2赋值给s1的效果。

2.1.4析构函数 

~string()

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

		}

2.2元素访问

2.2.1迭代器

  本质上是封装思想,可以类比指针,可以指向容器中的某个元素,并可以通过操作迭代器来访问和修改容器中的元素 。

//迭代器
		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;
		}

2.2.2 返回pos位置的字符

const char& operator[](size_t pos)const

//返回pos位置的字符
		const char& operator[](size_t pos)const
		{
			return _str[pos];
		}

2.3字符串修改

2.3.1push_back 尾插字符

//尾插字符
void string::push_back(char ch)
{
	//可能要扩容
	if (_capacity == _size)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size] = ch;
	++_size;
	//不要忘记\0
	_str[_size] = '\0';
}

2.3.2append尾插字符串

//尾插字符串
void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		//扩容  如果长度大于2倍  则该多大就开多大 否则就开2倍的空间
		reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);

	}
	strcpy(_str + _size, str);
	_size += len;
}

2.3.3operator+=(char ch)

复用push_back即可

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

2.3.4operator+=(const char* str)

复用append即可

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

2.3.5在pos位置插入字符insert

void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	//万一要扩容
	if (_capacity == _size)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	size_t end = _size;
	//进行挪动 end 挪向end+1
	while (end >=  (int)pos)
	{
		_str[end + 1] = _str[end];
		--end;

	}
	_str[pos] = ch;
	++_size;

}

注意这里挪动数据时的while循环的判断条件end>=pos,因为end和pos都是size_t无符号类型,所以当_size=0时,end无论怎么— —都不会小于_size,也就会陷入死循环 。

这里有解决方法:1.把无符号类型强转为有符号类型,进行整型提升

                             2.令end=_size+1,循环条件变成>

2.3.6在pos位置插入字符串insert

void string::insert(size_t pos, const char* str)
{
	assert(pos <=_size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		//扩容  如果长度大于2倍  则该多大就开多大 否则就开2倍的空间
		reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);

	}
	size_t end = pos + len;
	while (end >= pos+len)
	{
		_str[end] = _str[end - len];
		end--;
	}
	//再依次把字符串插进去
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += len;


}

2.3.7删除pos之后的n个字符

void string::erase(size_t pos, size_t len )

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

这里分两种情况:

1.如果len>_size-pos,说明pos之后的全删除,只需把pos位置置为'\0'

2.否则需要将不删除的字符一个个往前挪动 

2.4比较运算符

2.4.1大于>

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

2.4.2小于<

bool string::operator<(const string& s)const
{
	return strcmp(_str, s._str) < 0;
}

2.4.3等于==

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

2.4.4大于等于>=

这里可以复用>和=,也可以对<取反

bool string::operator>=(const string& s)const
{
	return !(*this < s);
}

2.4.5小于等于<=

同理,这里可以复用<和=,也可以对>取反

bool string::operator<=(const string& s)const
{
	return !(*this > s);
}

2.4.6不等于!=

bool string::operator!=(const string& s)const
{
	return !(*this == s);
}

2.5查找和截取

2.5.1find,寻找一个字符

size_t find(char ch, size_t pos = 0);

size_t string::find(char ch, size_t pos)
{
	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
		return npos;

	}

}

2.5.2find,寻找一个字符串

size_t fing(const char* str, size_t pos = 0);

这里直接使用C语言中的库函数strstr函数

size_t string::fing(const char* str, size_t pos)
{
	const char* ptr = strstr(_str + pos, str);//str在_str+pos中的位置
	if (ptr)
	{
		return ptr - _str;//找到了,返回下标,所以指针相减
	}
	return npos;//没找到

}

2.5.3substr,截取字符串 

string string::substr(size_t pos, size_t len)

//截取字符串
string string::substr(size_t pos, size_t len)
{
	if (len > _size - pos)//大于有效字符个数
	{   //更新len
		len = _size - pos;
 		}

	//最后需要返回字符串
	string sub;
	sub.reserve(len);
	for (size_t i = 0; i < len; i++)
	{
		//把要截取的字符尾插到sub
		sub += _str[pos + i];
	}
	return sub;
}

在更新好有效字符个数后,从pos位置取n个字符追加到临时的string对象中去, 最后将其返回即可,这里我们返回了一个出作用域就销毁的临时变量,只能使用传值返回,不能使用传引用返回。

 2.6空间操作

2.6.1size

获取当前字符串的有效长度,不包含'\0'

size_t size()const
{
	return _size;
}

2.6.2capacity

获取字符串的当前的容量

size_t capacity()const
{
	return _capacity;
}

2.6.3clear清空数据

清掉所有的数据但不动空间

//清掉所有的数据但不动空间
void clear()
{
	_str[0] = '\0';
	_size = 0;

}

2.6.4reserve预留空间,扩容

void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//开出一块新的空间,永远要多开一个空间 存\0
		
		strcpy(tmp, _str);//将原本的数据拷贝过来
		delete[]_str;//释放旧空间
		_str = tmp;//指向新空间
		_capacity = n;
	}
}

2.7流插入,流提取

2.7.1operator<< 流插入

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch: s)//范围for遍历
	{
		out << ch;
	}
	return out; 
}

注意这里不用加友元,因为并没有访问私有成员变量 

2.7.2operator>> 流提取

istream& operator>>(istream& in, string& s)
{

	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();
		const int n = 256;
		char buff[n];
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i = n - 1)//如果字符存满了
			{
				buff[n - 1] = '\0';
				s += buff;
				i = 0;//继续循环 直到读完
			}

			if (i > 0)//如果没有读满数组 还有剩余空间
			{
				buff[i] = '\0';
				s += buff;

			}
			return in;
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;

	}
}

样例测试:

以上就是string类基本接口的模拟实现,有不足的地方还请大家指正,共同交流! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值