当前位置: 首页 > article >正文

C++之模拟实现string

文章目录

  • 前言
  • 一、包含的相关头文件
  • 二、构造和析构
    • 1.构造函数
    • 2.拷贝构造
      • 1.传统写法
      • 2.现代写法
    • 3.赋值运算符重载
      • 1.传统写法
      • 2.现代写法
    • 4.析构函数
  • 三、iterator
  • 四、modify
    • 1.push_back(尾插一个字符)
    • 2.append(尾插一个字符串)
    • 3.运算符重载+=
      • 1.尾插字符
      • 2.尾插字符串
    • 4.clear
    • 5.insert
      • 1.插入一个字符
      • 2.插入一个字符串
    • 6.erase
  • 五、capacity
    • 1.size
    • 2.capacity
    • 3.empty
    • 4.resize
    • 5.reserve
  • 六、access
    • 1.普通对象的接口(可读可写)
    • 2.const对象的接口(只读)
  • 七、relational operators
    • 1.运算符<重载
    • 2.运算符>重载
    • 3.运算符<=重载
    • 4.运算符>=重载
    • 5.运算符==重载
    • 6.运算符!=重载
  • 八、String operations
    • 1.find
      • 1.找字符
      • 2.找子串
    • 2.c_str
  • 九、Non-member function overloads
    • 1.流插入
    • 2.流提取
  • 十、私有属性
  • 总结


前言

因为学习了string的相关知识,了解了string大部分接口的底层实现原理,所以我决定自己模拟实现一个mini版的string类,用来加深对string各方面知识的理解。
如果有错误或不足之处,还望各位读者小伙伴们指出。


一、包含的相关头文件

#include<iostream>
#include<assert.h>
#include<cstring>
using std::ostream;
using std::istream;
using std:: cout;
using std:: endl;

二、构造和析构

1.构造函数

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

2.拷贝构造

1.传统写法

该对象自己一点一点的进行深拷贝

		//拷贝构造
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			_size = s._size;
			_capacity = s._capacity;
			strcpy(_str, s._str);
		}

2.现代写法

找一个中间对象,让这个中间对象用参数的值进行直接构造,再将这个中间对象的内容与自己的内容进行交换。相较于传统写法,现代写法更加简洁。

		//拷贝构造
		string(const string& s)
			:_str(nullptr),//此处要注意将该对象的地址赋值为nullptr,否则析构中间对象时会因为发生野指针的访问而导致程序崩溃。
			_size(0),
			_capacity(0)
		{
			string temp(s);
			swap(temp);
		}

此处的swap用的是string自己实现的swap,为什么不用库里的swap呢?因为库里的swap是进行深拷贝,会降低效率。我们自己实现的swap只需要进行浅拷贝,即将它们对应的资源进行交换即可。

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

3.赋值运算符重载

1.传统写法

		//赋值运算符重载
		string& operator=(const string &s)
		{
			if (this != &s)
			{
				char* temp = new char[s._capacity + 1];//用一个中间值记录新开辟的空间,如果开辟空间成功,再将该空间的地址给_str。避免因为开辟空间失败,_str原本的内容也销毁的情况发生。
				strcpy(temp, s._str);
				delete[] _str;
				_str = temp;
				_capacity = s._capacity;
				_size = s._size;
			}
			return *this;
		}

2.现代写法

与拷贝构造的现代写法思想类似,可以使程序更加简洁。并且此处不需要专门定义一个中间对象,因为传值传参传过来的形参是实参拷贝构造出来的对象,它就是一个很好的中间对象,我们直接和它进行交换即可。

		//赋值运算符重载
		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

4.析构函数

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;//释放指针所指向的空间后要将该指针置为空(避免出现野指针的非法访问)
			_size = _capacity = 0;
		}

三、iterator

迭代器是一个使用起来像指针的东西,实际上string的迭代器就是char*类型的指针,因此我们先在最开始进行typedef

typedef char* iterator;

定义两个迭代器:分别指向字符串开头和结尾

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

四、modify

1.push_back(尾插一个字符)

string只提供了push_back的接口没有提供头插的接口,因为头插需要移动数据,效率很低,所以尽量不要在string中使用头插。

		void push_back(char c)
		{
			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;//要考虑到如果初始字符串的容量为0的情况
				reserve(newcapacity);
				_capacity = newcapacity;
			}
			_str[_size++] = c;
			_str[_size] = '\0';
		}

2.append(尾插一个字符串)

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}
			strcpy(end(), str);
			_size += len;
		}

3.运算符重载+=

1.尾插字符

尾插一个字符(复用尾插)

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

2.尾插字符串

尾插一个字符串(复用append)

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

4.clear

清除string中所有元素

		void clear()
		{
			_size = 0;
			_str[0] = '\0';
		}

5.insert

1.插入一个字符

在字符串某位置插入一个字符:

		// 在pos位置上插入字符c/字符串str
		string& insert(size_t pos, char c)
		{
			assert(pos <= _size);
			if (_size + 1 > _capacity)
			{
				reserve(_size + 1);
			}
			for (size_t i = _size + 1; i > pos; --i)//要注意避免i减为-1变成一个极大的无符号数,导致死循环
			{
				_str[i] = _str[i - 1];
			}
			_str[pos] = c;
			++_size;
			return *this;
		}

2.插入一个字符串

在字符串某位置插入一个字符串:

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t i = 0;
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			for (i = _size + len; i > pos + len - 1; --i)
			{
				_str[i] = _str[i - len];
			}
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

6.erase

删除从字符串某位置开始,某长度的子串(如果不说明删除子串的长度,则默认从开始位置到最后的所有内容都删除)

		// 删除pos位置上的元素
		string& erase(size_t pos, size_t len = npos)
		{
			if (len == npos || pos + len > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

五、capacity

1.size

类外成员不能直接访问类的私有属性/数据,因此需要提供一个接口进行访问数据,但是要对数据进行保护,因此用了const传参和传值返回。
访问string中的元素个数。

		size_t size()const
		{
			return _size;
		}

2.capacity

访问string的容量。

		size_t capacity()const
		{
			return _capacity;
		}

3.empty

判断字符串是否为空。

		bool empty()const
		{
			if (_size == 0)
			{
				return true;
			}
			return false;
		}

4.resize

修改字符串的元素个数,可以尾插指定的字符,未指定则默认插入’\0’。

		void resize(size_t n, char c = '\0')//更改元素个数
		{
			if (n <= _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);//先扩容,再加元素
				for (size_t i = _size; i < n; ++i)
				{
					_str[i] = c;
				}
				_str[n] = '\0';
				_size = n;
			}
		}

5.reserve

修改字符串的容量。

		void reserve(size_t n)//更改容量大小
		{
			if (n > _capacity)
			{
				char* temp = new char[n + 1];//先开空间再拷贝值(避免因为空间开辟失败的异常使_str原地址也丢失了)
				strcpy(temp, _str);
				delete[] _str;
				_str = temp;
				_capacity = n;
			}
		}

六、access

访问字符串中的字符,运算符[]重载。

1.普通对象的接口(可读可写)

		char& operator[](size_t index)//普通对象的接口(可读可写)
		{
			assert(index < _size);
			return _str[index];
		}

2.const对象的接口(只读)

		const char& operator[](size_t index)const//const对象的接口(只读)
		{
			assert(index < _size);
			return _str[index];
		}

七、relational operators

1.运算符<重载

		bool operator<(const string& s)
		{
			size_t len = _size < s._size ? _size : s._size;
			for (size_t i = 0; i < len; ++i)
			{
				if ((*this)[i] >= s[i])
				{
					return false;
				}
				return true;
			}
		}

2.运算符>重载

		bool operator>(const string& s)
		{
			size_t len = _size < s._size ? _size : s._size;
			for (size_t i = 0; i < len; ++i)
			{
				if ((*this)[i] <= s[i])
				{
					return false;
				}
				return true;
			}
		}

3.运算符<=重载

复用>。

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

4.运算符>=重载

复用<。

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

5.运算符==重载

复用<和>。

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

6.运算符!=重载

复用==。

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

八、String operations

1.find

1.找字符

找一个字符第一次在字符串中出现的下标

		// 返回c在string中第一次出现的位置(下标)
		size_t find(char c, size_t pos = 0) const
		{
			assert(pos < _size);
			for (size_t i = 0; i < _size; ++i)
			{
				if ((*this)[i] == c)
				{
					return i;
				}
			}
			return npos;
		}

2.找子串

找一个子串第一次出现在字符串中的下标
复用C语言对字符串的操作——strstr函数(在一个字符串中找子串)

		// 返回子串s在string中第一次出现的位置(下标)
		size_t find(const char* s, size_t pos = 0) const
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, s);
			if (ptr == nullptr)
			{
				return npos;
			}
			else
			{
				return ptr - _str;
			}
		}

也可以一个字符一个字符的遍历寻找:

		// 返回子串s在string中第一次出现的位置(下标)
		size_t find(const char* s, size_t pos = 0) const
		{
			assert(pos < _size);
			size_t i = 0;
			while (i < _size)
			{
				int flag = 0;
				if ((*this)[i] == s[0])
				{
					size_t k = i;
					for (size_t j = 0; j < strlen(s); ++j, ++k)
					{
						if ((*this)[k] != s[j])
						{
							flag = 1;
						}
					}
					if (flag == 0) return i;
				}
				i++;
			}
			return npos;
		}

2.c_str

返回字符串的地址

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

九、Non-member function overloads

1.流插入

	ostream& operator<<(ostream& _cout, const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			_cout << s[i];
		}
		return _cout;
	}

2.流提取

	istream& operator>>(istream& _cin, string& s)
	{
		s.clear();//将原来s中的值清除
		char buff[128] = { '\0' };
		size_t i = 0;
		char ch = _cin.get();//这里不能使用getc或者scanf函数的原因是他们都是按照空格' '或者换行'\n'进行分割内容的,所以无法取到空格和换行,因此只能用get()
		while (ch != ' ' || ch != '\n')
		{
			if (i == 127)//缓存部分满了
			{
				s += buff;//将内容存入s,同时将缓存部分情况
				i = 0;
			}
			buff[i++] = ch;
			ch = _cin.get();
		}
		return _cin;
	}

十、私有属性

	private:
		char* _str;
		size_t _capacity;
		size_t _size;
		const static size_t npos = -1;//只有这个特例可以这样定义,其他static数据成员都要在类内声明,在类外定义。

总结

以上就是今天要讲的内容,本文介绍了作者自己实现的string类的相关类成员函数,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!


http://www.kler.cn/a/3256.html

相关文章:

  • kafka消费者详细介绍(超级详细)
  • 01机器学习入门
  • 汽车OEMs一般出于什么目的来自定义Autosar CP一些内容
  • LongLoRA:高效扩展大语言模型上下文长度的微调方法
  • Spring Boot(6)解决ruoyi框架连续快速发送post请求时,弹出“数据正在处理,请勿重复提交”提醒的问题
  • 后盾人JS--闭包明明白白
  • “两会”网络安全相关建议提案回顾
  • 集合之HashMap 1.7总结
  • windows渗透(sam、system文件导出)
  • css学习14(多媒体查询)
  • Reids中的有序集合Zset
  • 个人时间管理网站—Git项目管理
  • 【C++进阶】C++11(中)左值引用和右值引用
  • 多线程控制讲解与代码实现
  • resnet34 对皮肤病分类(从txt文件读取label)
  • Android 12.0 Settings主页面去掉FocusRecyclerView相关功能
  • [数据结构]二叉树OJ(leetcode)
  • chatGPT测试:angular 8以上,word转pdf的组件生成
  • ajax本地跨域请求以及解决方法
  • 59、C语言程序设计谭浩强第七章
  • 你真的掌握到“优先级队列“的精髓了吗?
  • linux入门---操作体统的概念
  • leetcode.1574 删除最短的子数组使剩余数组有序 - 阿里笔试 双指针 二分
  • 清晰概括:进程与线程间的区别的联系
  • 两种方法教你在postman设置请求里带动态token
  • 入职第一天就被迫离职,找工作多月已读不回,面试拿不到offer我该怎么办?