C++(14)——string的模拟实现

       前几篇文章中介绍了关于string以及其相关函数的使用,为了更清楚的了解这些函数的作用,本篇文章通过模拟实现的方式来加深对于函数作用原理的理解。

目录

1. String的整体框架:

1.1 成员变量:

1.2 构造函数:

1.3 析构函数:

1.4 外部获取:

测试: 

2. 功能函数:

2.1 获取_size:

2.2 迭代器:

2.3 []访问及修改: 

测试:

2.4 打印函数:

3 对于对象的修改函数:

3.1 扩容函数:

3.2 插入函数:单个字符:

3.3 插入函数:字符串:

3.4 运算符重载+=:

测试:

3.5 在指定位置插入单个字符:

3.6 在指定位置插入字符串:

测试:

3.7  从指定位置开始向后删除任意长度字符串:

测试:

3.8 交换函数:

3.9 查询函数:查询单个字符:

3.10 查询函数:查询字符串:


1. String的整体框架:

1.1 成员变量:

       通过前面对于string类的学习以及使用,不难得出,如果要模拟实现string,则需要设置三个成员变量,即:char *_str,_capacity,_size因此,对于类的整体框架,可以有下面的代码表示:

namespace violent
{
	class string
	{
	public:


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

在有了整体的框架后,就需要在类中加入需要的成员函数

1.2 构造函数:

       在文章C++类与对象基础(7)-CSDNz中提到了初始化列表的概念,但是从上出给出的三个成员变量不难看出,三个变量全是内置类型,因此,对于内置类型的初始化,直接在函数内部进行,由于string类主要针对于,字符串,因此,构造函数的参数也应该符合类型。

       在文章C++(9)——内存管理-CSDN博客提到了在C++中,管理内存的可以使用C语言的两个函数,即mallloc,free.也可以使用C++中的两个关键字,即:new,delete。文章此处选择利用关键字来完成对于内存的管理。

对于如何获取参数中字符串的内容,可以利用strcpy来完成。

      对于三个关键字的取值,_capacity_size可以利用字符串函数strlen计算得出,对于开辟空间大小,需要注意,字符串的末尾会附带一个0,strlen函数计算的出的值并不会计算0因此需要手动+1.具体代码如下:

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

但是,如果需要初始化的对象为空,上述函数并不能完成初始化。对于空的对象,可以看做对象中存储了一个0可以通过缺省值的方式来完成针对于空对象的构造函数,即:

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

1.3 析构函数:

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

1.4 外部获取:

单独建立一个函数,用于返回指针_str即可:

const char* c_str()
		{
			return _str;
		}

测试: 

      利用下方的代码,对上述函数的功能进行测试:

void test1string()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}

测试结果如下:

2. 功能函数:

2.1 获取_size:

在遍历string类型的对象时,经常会把对象中内容的长度,即_size作为循环的结束,为了方便获取这个值,文章提供函数size来实现。

size_t size()
		{
			return _size;
		}

2.2 迭代器:

之前的文章介绍迭代器的使用时,提到可以把迭代器看作指针(二者原理并不相同),因此,文章模拟实现迭代器也使用指针的方式,即:

iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

2.3 []访问及修改: 

       在之前的文章,在打印string类型的对象时,会使用[]来完成对于对象的打印,并且,[]还可以完成对于对象中内容的修改。在介绍引用时,提到,引用返回相对于指针返回有两个有点,一是速度更快,二是可以修改返回值。在进行函数实现时,可以加上对于参数给出的位置是否合法的检查。因此,对于本小节的函数,实现如下:

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

测试:

通过下面的代码,完成对于上述函数功能的测试:

void test1string()
	{
       string s2("hello world");

		cout << "测试[]的访问功能;";
		for (size_t i = 0; i < s2.size(); i++)
		{
			cout << s2[i] << ' ';
			i++;
		}
		cout << endl;
		cout << "测试[]对于返回值的修改功能:";
		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i]++;
			cout << s2[i] << ' ';
		}

		cout << endl;
		cout << "测试迭代器访问:";
		string::iterator it1 = s2.begin();
		while (it1 != s2.end())
		{
			cout << *it1 << ' ';
			it1++;
		}
	}

运行结果如下:

2.4 打印函数:

上述打印方式都需要额外编写,为了方便使用,直接将打印封装在一个函数,此处命名为print_str:

void print_str(const string& s)

需要注意,此时参数的类型与之前功能函数的类型并不匹配,因此需要根据前面的功能函数尽心修改,即:

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

		size_t size() const
		{
			return _size;
		}

对于迭代器和[]访问,需要额外提供两个函数,即:

typedef const char* const_iterator;

const_iterator begin() const
		{
			return _str;
		}

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

在对函数功能函数进行修改后,可以得出打印函数:
 

void print_str(const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			cout << s[i] << ' ';
		}

		string::const_iterator it2 = s.begin();

		while (it2 != s.end())
		{
			cout << *it2 << ' ';
			it2++;
		}


	}

测试效果如下:

3 对于对象的修改函数:

(注:对于本部分内的函数实现原理与数据结构中链表的实现的文章一起学数据结构(3)——万字解析:链表的概念及单链表的实现-CSDN博客相同,因此不对原理进行过多介绍)

3.1 扩容函数:

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

3.2 插入函数:单个字符:

void push_back(char ch)
		{
			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

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

3.3 插入函数:字符串:

void append(const char* s)
		{
			size_t len = strlen(s);

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

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

3.4 运算符重载+=:

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

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

测试:

测试代码如下:

void test2string()
	{
		string s2("hello world");
		cout << s2.c_str() << endl;

		s2 += 'x';
		cout << s2.c_str() << endl;

		s2 += "yyyyyy";
		cout << s2.c_str() << endl;
	}

运行结果如下:

3.5 在指定位置插入单个字符:

在文章一起学数据结构(3)——万字解析:链表的概念及单链表的实现-CSDN博客中,详细介绍了如何实现此函数。在文章本部分,不进行详细的介绍,只给出图片来演示此过程:

如上图可视:加入需要将字符apos 插入,首先需要将pos位置及其后面的字符整体向后移动一位。即:

代码如下:

void insert(size_t pos, char ch)
		{
			assert(pos <= _size);
				if(_size == _capacity)
				{
					size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
					reserve(newcapacity);
				}

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

3.6 在指定位置插入字符串:

原理与插入单个字符大致相同,代码如下:
 

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

			int end = _size;
			while (end >= pos + len)
			{
				_str[end + 1] = _str[end];
				end--;
			}
			strncpy(_str + pos, s,len);
            _size += len;

		}

测试:

利用下方的代码对上述函数进行测试:

void test2string()
	{
		string s2("hello world");
		cout << s2.c_str() << endl;

		s2 += 'x';
		cout << s2.c_str() << endl;

		s2 += "yyyyyy";
		cout << s2.c_str() << endl;

		cout << endl << endl;
		s2.insert(0, 'a');
		cout << s2.c_str() << endl;

		s2.insert(3, "wwwww");
		cout << s2.c_str() << endl;
	}

运行结果如下:

3.7  从指定位置开始向后删除任意长度字符串:

对于删除字符,需要分下面几种情况:

1.需要删除的字符长度大于等于npos

2.指定位置pos加上需要删除的字符长度大于_size

对于上述两种情况,从pos位置向后全部删除。

对于其他情况,则根据需要删除的长度进行删除

对于第一种情况的删除,并不需要真正的将字符一个一个进行删除,只需要将pos位置的字符改为0并且对_size进行修改即可。

对于第二种情况的删除,加入指定为位置为pos,需要删除的字符串长度为len。只需要将利用strcpypos+len位置之后的内容,从pos位置拷贝即可:

void erase(size_t pos, size_t len = npos)
		{
			    assert(pos <= _size);
				if (len == npos || pos + len >= _size)
				{
					_str[pos] = '';
					_size = pos;
				}
				else
				{
					strcpy(_str + pos, _str + pos + len);
					_size -= len;
				}
		}

测试:

用下面给出的代码对上述函数进行测试:

void test3string()
	{
		string s3("hello world");

		s3.erase(5, 3);
		cout << s3.c_str() << endl;

		string s4("hello world");
		s4.erase(5, 100);
		cout << s4.c_str() << endl;	
	}

运行结果如下:

3.8 交换函数:

cplusplus网站中可以找到三种不同的交换函数swap,假如有两组不同类型的变量,即:

int a,b;
string s1,s2;

对于第一组变量,通过前面对于模板的了解得知其会去自动调用模板函数,对于第二组变量,在没有第一、二种swap的情况下会去调用模板函数,对于第一种和第二种,二者的区别是调用的形式不同,第一种的调用方法为:

swap(s1,s2);

对于第二种的调用方法为:

s1.swap(s2);

在有第一、二种函数的情况下,不会调用第三种,而是优先调用第一、二种。

对于第三种方式而言,由于代码T, c(a)对于string类型的对象需要进行一次拷贝构造,因此效率较慢。对于第二种交换方式,是通过this指针和str来完成的,即交换两个对象的地址,并不交换其中的内容,即:

因此,为了速度,采用第二种交换方式,即:

void swap(string& s1)

 但是,在函数内部,并不能直接再次调用swap,即不能直接调用下面的代码:

void swap(string& s1)
		{
			swap(_str, s1._str);
			swap(_size, s1._size);
		    swap(_capacity, s1._capacity);
		}

按照上述代码的调用会引起歧义,因此,需要调用不同地方的swap,即:

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

3.9 查询函数:查询单个字符:

代码如下:

size_t find(char ch, size_t pos = 0)
		 {
			 assert(pos <= _size);

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

			 }
			 return npos;
		 }

3.10 查询函数:查询字符串:

对于字符串的查询可以通过函数strstr,对于函数strstr的原理,可以从C语言——字符串和内存函数_c语言 指针 字符串 内存-CSDN博客进行查看。代码如下:

size_t find(const char* s, size_t pos = 0)
		{
			assert(pos <= _size);

			const char* str = strstr(_str + pos, s);
			if (str == nullptr)
			{
				return npos;
			}
			else
			{
				return str - _str;
			}
		}