string的增删改查模拟实现(简单版)【C++】

发布于:2025-07-21 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

1. string的私有成员

2. 构造函数和析构函数

2.1 构造函数

2.1.1 无参构造函数 和 带参构造函数

2.1.2 全缺省构造函数

2.2 析构函数

3. string遍历

 3.1 size()、length() 和 operator[ ]

3.2 迭代器遍历

3.3 遍历测试

4. string的增删改查

4.1 insert

4.2 push_back 和 append

4.3 operator+=

4.4 erase

4.5 resize

4.6 swap

4.7 find

4.8 substr

5. 深浅拷贝问题

6. 比较关系运算符重载

6.1 operator==

6.2 operator<

6.3 其他关系运算符重载

7. 流插入、流提取和getline

7.1 流插入

7.2 流提取

7.3 getline

8. 源码


string的底层就是一个动态增长字符数组。首先在自己创建的mystring的命名空间中创建一个头文件string.h,再定义一个string类,同时创建一个test.cpp文件来调用测试。

1. string的私有成员

namespace mystring {

	class string 
	{

	public:
		//成员函数

	private:
		//string的底层就是动态增长的字符数组
		char* _str;	  //指向一个动态分配的字符数组
		size_t _size; //字符串的实际长度(不包括'\0')
		size_t _capacity;	//分配的内存容量
	};


	//测试函数
	void test_string1() 
	{

	}
}

其中size_t是无符号整型。

2. 构造函数和析构函数

2.1 构造函数

使用构造函数来进行初始化,同时使用初始化列表

	class string 
	{
	public:
		//成员函数
		//2.1 构造函数

		//无参构造函数
		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}
		//带参构造函数
		string(const char* str) 
			:_str(str)
			,_size(strlen(str))
			,_capacity(strlen(str))
		{}

	private:
		//string的底层就是动态增长的字符数组
		//1 私有成员
		char* _str;	  //指向一个动态分配的字符数组
		size_t _size; //字符串的实际长度(不包括'\0')
		size_t _capacity;	//分配的内存容量
	};

这时会出现一个问题 如图: 

  • const char* 不能改成 char* 因为这样可能会导致参数字符串被改变,得不到原来想要的参数。
  • 也不能把 私有成员 char* 改成 const char* ,因为我们想去修改字符串的时候,发现无法修改。

所以应该去开辟一个新空间,拷贝数据。正确方法:

		//带参构造函数
		string(const char* str) 
			:_str(new char[strlen(str)+1])//加1原因是为了存储'\0',与C语言风格字符串匹配
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str,str);	//拷贝数据
		}

这里发现调用了三次strlen,优化版本

错误示例:

		//错误示例:
		string(const char* str)
			:_size(strlen(str))
			,_str(new char[_size+1])
			, _capacity(strlen(str))
		{
			strcpy(_str, str);	//拷贝数据
		}

所以 在构造函数上的初始化列表上的初始化顺序 最好和私有成员定义的顺序一致。

正确写法:

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

2.1.1 无参构造函数 和 带参构造函数
		//无参构造函数
		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}
		string(const char* str)
			:_size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str,str);
		}

但是,当定义无参对象的时候仍有问题

string s1;

这里借助获取字符串的c_str()成员函数来检测

#include<iostream>

namespace mystring {

	class string 
	{
	public:

		//成员函数
		//2.1 构造函数

		//无参构造函数
		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}

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

		const char* c_str() 
		{
			return _str;
		}

	private:
		//string的底层就是动态增长的字符数组
		//1 私有成员
		char* _str;	  //指向一个动态分配的字符数组
		size_t _size; //字符串的实际长度(不包括'\0')
		size_t _capacity;	//分配的内存容量
	};

	//测试函数
	void test_string1() 
	{
		string s1;
		std::cout << s1.c_str() << std::endl;
	}
}

可以将无参构造函数带参构造函数合并为一个全缺省构造函数(即所有参数都有默认值的构造函数)

2.1.2 全缺省构造函数

在写全缺省构造函数的时候也要注意:

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

strlen(nullptr) 这会导致程序崩溃,strlen的参数必须指向合法的C字符串(以 \0 结尾)

正确写法:

		string(const char* str = "")	//也可以"\0"
			:_size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str,str);
		}

2.2 析构函数

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

3. string遍历

 3.1 size()、length() 和 operator[ ]

在成员函数中进行实现

		//遍历
		size_t size() const
		{
			return _size;
		}
		size_t length() const
		{
			return _size;
		}
		//返回pos位置的字符 并且要可读可写
		char& operator[](size_t pos) //char& 返回引用 说明,
		{
			//断言检查有没有越界
			assert(pos < _size);	//开销不大,因为会变成内联函数
			return _str[pos];
			//_str[pos]访问的是堆上面的空间,出了作用域还在,就说明可以使用引用返回
			// 返回值类型是引用,返回的不是拷贝,返回的是别名,就说明可读可写
		}

传统的c语言的越界检查是一种给抽查(结束位置进行,看变量有没有被修改)

而C++函数封装的时候,可以加上越界判断,使用assert(#include<assert.h>)

3.2 迭代器遍历

迭代器是像指针一样,有可能是指针,也有可能不是指针,这里借助指针。

		//成员函数
        //指针版本的迭代器
		typedef char* iterator;
		typedef const char* const_iterator;
        //普通对象版本
		iterator begin() 
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		//const对象版本
		const_iterator begin() const    //第二个const是防止修改_str
		{
			return _str;
		}
		const_iterator end() const 
		{
			return _str + _size;
		}

3.3 遍历测试

	//测试访问
	void test_string1() 
	{
		//string s1;
		//cout << s1.c_str() << endl;

		//测试遍历
		string s1 = "hello world";
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i] << " ";
		}
		cout << "\n";
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << ++s1[i] << " ";
		}
		cout << "\n";

		//普通对象迭代器遍历+修改
		string::iterator it1 = s1.begin();
		while (it1 != s1.end()) 
		{
			cout << *it1 << " ";
			it1++;
		}
		cout << endl;

		it1 = s1.begin();
		while (it1 != s1.end())
		{
			cout << ++(*it1) << " ";
			it1++;
		}
		cout << endl;

		//const对象迭代器遍历
		const string s2("xxxx");
		string::const_iterator cit2 = s2.begin();
		while (cit2 != s2.end()) 
		{
			cout <<*cit2<< " ";
			++cit2;
		}
		cout << "\n";

		//范围for,底层是替换成迭代器,从编译器角度只增加一个替换规则
		for (auto e : s1)
		{
			cout << e << " ";
		}
		cout << '\n';

		//范围for根据对象类型 对应
		//在这里发现 当s是普通对象的时候会替换成普通迭代器,是const对象的时候就会替换成const迭代器
		const string s("xxxxx");
		for (auto e : s)
		{
			cout << e << endl;
		}

	}

测试结果;

4. string的增删改查

在实现下方函数时,因为增加数据要开空间,所以先实现reserve。

		//预分配好内存
		void reserve(size_t n) 
		{
			//当n > _capacity 需要单独开空间
			if (n > _capacity) 
			{
				//开新空间
				char* tmp = new char[n + 1];
				//拷贝数据
				strcpy(tmp,_str);
				//释放旧空间
				delete[] _str;

				//修改指针指向
				_str = tmp;
				//修改容量
				_capacity = n;
			}
		}

4.1 insert

这里模拟实现在pos位置插入字符串。

		//在pos位置插入单个字符
		void insert(size_t pos , char ch) 
		{
			//首先判断位置是否合法
			assert(pos <= _size);	//这里pos == _size 相当于尾插
			//判断是否需要扩容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 :2* _capacity);
			}

			//移动数据
			//先移动_size位置的'\0',之后在向前移动,直到移动完pos位置
			for (size_t i = 0; i != _size - pos + 1; i++)
			{
				_str[_size + 1 - i] = _str[_size - i];
			}
			//插入数据
			_str[pos] = ch;
			//调整大小
			++_size;
		}

		//在pos位置插入字符串
		void insert(size_t pos , const char* str) 
		{
			//先判断位置是否合法
			assert(pos <= _size);

			//判断是否需要进行扩容
			size_t len = strlen(str);
			if (len + _size >_capacity) 
			{
				reserve(len + _size);
			}

			//移动数据
			// 错误例子
			//size_t end = _size;
			//while (end >= pos) 
			//{
			//	_str[end+len] = _str[end];
			//	--end;
			//}

			//正确写法
			size_t end = _size + 1;
			while (end > pos) 
			{
				_str[end-1 + len] = _str[end-1];
				--end;
			}

			//拷贝数据
			strncpy(_str+pos,str,len);

			_size += len;

		}

上述在移动插入单个字符的实现移动数据实现上 通过移动的数据个数来控制,不会出现头插的bug,而在 插入在字符串移动数据使用end指针来控制的时候,需要格外关注头插问题。

4.2 push_back 和 append

push_back 和 append都是在字符串的末尾进行追加。

		//追加单个字符
        void push_back(char ch)
		{
			//尾插,首先要判断是否需要扩容
			if (_size == _capacity) 
			{
				reserve(_capacity == 0 ? 4 : 2*_capacity);	//这样写是为了避免_capacity == 0的情况
			}

			//追加数据
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
        //追加字符串
		void append(const char* str) 
		{
			size_t len = strlen(str);
			if (len + _size > _capacity) 
			{
				reserve(len+_size);
			}

			//拷贝数据
			strcpy(_str+_size,str);
			_size += len;
		}

 优化:当然也可以借助insert函数来实现


		void push_back(char ch)
		{
			insert(_size,ch);
		}

		void append(const char* str)
		{
			insert(_size,str);
		}

4.3 operator+=

对于增加字符串,operator+=重载是经常用的。

		string& operator+=(char ch)
		{
			//这里可以直接调用push_back
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* str) 
		{
			//直接调用append
			append(str);
			return *this;
		}

4.4 erase

这里简单实现一下erase常用的函数,在pos位置删除len个字符

这里的nops是一个共有的静态const成员变量,属于整个类,属于每个对象 ,类内声明,类外初始化。

注意在实现erase函数的时候要注意参数

如果 len = npos 或者 pos+ len >= _size的时候,直接在pos位置加 \0,否则直接删除部分串即可。

但是 pos + len 会有溢出风险,len = npos的时候,就会出现的。

所以最好改写成 _size - pos <= len 。

		//在pos位置删除len个字符
		void erase(size_t pos , size_t len = npos) 
		{
			//判读删除位置是否合法
			assert(pos < _size);
			//pos位置后面全部删除
			if (len == npos || _size - pos <= len ) 
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else 
			{
				//部分删除
				strcpy(_str+pos,_str+pos+len);
				_size -= len;
			}
		}

4.5 resize

调整字符长度,需要注意的就是调整小空间和调大的情况

  • 调小:直接在字符串 n 的位置给 \0 表示该串结束
  • 调大:先扩容,再进行填充字符ch,注意最后n位置要加\0表示结束
		void resize(size_t n,char ch = '\0')
		{
			//调小的情况
			if (n < _size)
			{
				_str[n] = '\0';
			}
			else 
			{
				//调大的情况
                //先扩容,再在原串的末尾填充ch
				reserve(n);
				for (size_t i = _size; i < n;i++) 
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
			}
			_size = n;	//注意修改

		}

4.6 swap

swap具体有三种,算法库里面的swap,手动模拟实现的swap,string里面重载的swap。

标准库里面的swap:

		//库里面的swap对象交换的底层是
		/*
		template<class T> void swap(T&a,T&b) 
		{
			T c(a);a = b; b = c;
		}
		这里的代价就是:三次拷贝+一次析构
		*/

这使用整个模板有一定的代价,三次拷贝+一次析构。

手动实现的swap:

为了避免上述的代价,最简单的方式就是把对象里面的成员进行交换。

		
        void swap(string & s) 
		{
			//命名空间展开
			//using namespace std; 先去局部找,在类中和函数找,没有找到就去全局找
			//一般不会直接去命名空间找(除非 命名空间展开)
			//编译器只会向上找 , 先局部-> 全局,如果命名空间展开就是命名空间找
			std::swap(_str,s._str);
			std::swap(_size,s._size);
			std::swap(_capacity,s._capacity);
		}

有什么方式避免误用swap?

我们发现string里面 重载了一个 void swap(string& x,string&y){x.swap(y)}
有现成就用现成,没有就去调用模板的。

4.7 find

实现 查找在pos位置出现的字符或者字符串

  • 查找字符:先判断pos位置是否合法,然后从pos位置开始查找,找到返回下标,找不到返回npos。
  • 查找字符串:先判断pos位置是否合法,然后从pos位置开始查找( 这里使用strstr()函数进行查找。strstr( ) 找不到就会返回空,找到了就会返回指向原字符串中第一次出现的子串的指针找到。)找到返回下标(这里返回的下标通过 两个指针来实现),找不到返回npos。
		//find ch
		size_t find(char ch,size_t pos = 0) const
		{
			//判断位置是否合法
			assert(pos < _size);

			//查找字符,从pos位置开始查找
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch) 
				{
					return i;
				}
			}
			return npos;
		}

		//find substr
		size_t find(const char* substr, size_t pos = 0) const
		{
			//判断位置是否合法
			assert(pos < _size);
			//查找字串
			const char* p = strstr(_str+pos,substr);

			if (p) 
			{
				//不为空就返回下标
				//指针-指针返回的就是两指针之间的距离,也就是下标
				return  p - _str;
			}
			else 
			{
				return npos;
			}
		}

4.8 substr

获取部分字串,根据标准库来模拟实现。也就是获取从pos位置后且长度为len的字串。

先判断pos位置是否合法。

当 len 为npos的时候或者 pos+len >=_size的时候,获取从pos位置开始所有字符。

否则获取 pos开始长度为len的字符串。

		//获取部分字串
		string substr(size_t pos = 0,size_t len = npos) const
		{
			string sub;
			assert(pos < _size);
			if (len == npos || _size - len <= pos) 
			{
				获取pos位置后的全部
				for (size_t i = pos; i < _size; i++)
				{
					sub += _str[i];
				}
			}
			else 
			{
				//获取从pos位置开始的len个
				for (size_t i = pos; i < pos + len;i++) 
				{
					sub += _str[i];
				}
			}
			return sub;
		}

优化的地方:可以去复用append函数 

5. 深浅拷贝问题

拷贝构造函数

在没有手动实现拷贝构造函数的情况下,进行拷贝构造,发现程序出现问题,从调试可以发现问题:

  • 在没有写拷贝构造的情况下,编译器会自动默认生成的拷贝构造是浅拷贝。
  • 浅拷贝会指向相同的空间
  • 而且会析构同一空间两次。
  • 当s1修改的时候,发现s也被修改了。

所以编译器生成的拷贝构造函数不合适,所以需要手动生成一个拷贝构造函数。

采用深拷贝的方式

		//拷贝构造函数
		//s1(s)
		string(const string& s) 
		{
			//深拷贝
			//另开一个空间,再把数据拷贝过来
			_str = new char[s._capacity + 1];
			strcpy(_str,s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

再次调试验证一下:

拷贝的对象不再指向相同空间了。

赋值运算符重载

当我们按照上图的方式赋值的时候,发现也会出现浅拷贝的问题。

所以需要手动实现赋值运算重载。

传统写法版本

		//赋值运算符重载
		//如果不手动实现就会出现浅拷贝的问题
        //传统写法
		string& operator=(const string & s) 
		{
			//开新空间
			char* tmp = new char[s._capacity + 1];
			//拷贝数据
			strcpy(tmp,s._str);
			//释放旧空间
			delete[] _str;

			//指向tmp
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;
			return *this;
		}

现代写法

		//拷贝构造函数的现代写法
        string(const string& s) 
		{
			string tmp(s._str);
			swap(tmp);

		}

6. 比较关系运算符重载

6.1 operator==

一般会直接在类中来实现该成员函数

		bool operator==(const string & s)
		{
			int ret = strcmp(_str,s._str);//这里借助strcmp函数来实现
			return ret == 0;
		}

但是在比较的时候会出现问题:成员函数的左端必须是对象才可以。

 解决方法:可以重载为 全局的关系运算符。

	bool operator==(const string& s1,const string& s2)
	{
		int ret = strcmp(s1.c_str(), s2.c_str());
		return ret == 0;
	}

6.2 operator<

这里借助strcmp函数来实现,strcmp(str1,str2) 当str1 < str2 的时候,会返回一个小于0的值。

	bool operator<(const string& s1,const string& s2) 
	{
		int ret = strcmp(s1.c_str(),s2.c_str());
		return ret < 0;
	}

6.3 其他关系运算符重载

可以复用上述的关系运算符,来实现其他关系运算符。

	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,string& s2) 
	{
		return !(s1 == s2);
	}

7. 流插入、流提取和getline

7.1 流插入

当直接给对象输入字符串的时候和直接用对象输出字符串的时候,发现程序报错。需要我们手动来模拟实现。这里使用标准输出流类来实现。

	//流插入必须是全局的
	ostream& operator<<(ostream& out , const string& s) 
	{
		//使用范围for拿到字符,在使用out来输出
		for (auto ch : s) 
		{
			out << ch;
		}

		return out;	//这里返回希望连续cout<< s << s1...
	}

 发现这里没有使用友元,因为这里只是打印字符,不需要去访问私有成员,打印字符可以使用operator[ ]、范围for等来访问 。

所以,运算符流插入的重载 不一定 必须为是全局函数且为类的友元函数。

7.2 流提取

这里对一个对象输入一个新的字符串。

需要注意的是先提前清空缓冲区,或者提前清空原来的串,也就是新的字符串覆盖原来的串,这样才可以保证打印出来的是新的串,这里通过复用模拟实现的clear来实现。

		void clear()
		{
			//内容清空,空间不变
			_size = 0;
			_str[_size] = '\0';
		}

标准库中的cin提取不到空格和换行,但是这里string中的流提取需要读到。这里使用istream类中的重载函数get。

	//流提取
	istream& operator>>(istream& in , string& s) 
	{
		s.clear();
		//把提取的字符放到s里面去
		char ch;

		ch = in.get();
		while (ch != ' ' && ch != '\n') 
		{
			s += ch;
			ch = in.get();
		}
		return in;

	}

问题:

当输入比较长的字符串的时候,上述代码会频繁的进行扩容,字符换越长,扩容次数就越多,就会导致一系列的消耗。

如果使用reserve,虽然可以减少扩容,但是不确定要开多少的内存

上述如果给了128,但是提取字符串较少的时候,内存空间就开大了,有些浪费。

优化:

		s.clear()
        //为避免频发库容 和 reserve的不合适
		char ch;
		ch = in.get();
		char buff[128];
		size_t i = 0;
		
		while (ch != ' ' && ch != '\n') 
		{
			buff[i++] = ch;
			if (i == 127) 
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;

所以这里没有把提取的数据直接用string += 而是先放到一个buff数组里面,如果字符串比较短,例如3个字符遇到空格或者换行就结束,string对象直接+=buff 就可以。

 如果提取的字符串大 _capacity就大,值得注意的是,字符拆较大的时候,借助buff一段一段加。

 

7.3 getline

getline是获取一行的字符串,包括中间的空格。这里只需要把流提取的 ch !=  ‘ ’ 去掉就可以。

	istream& getline(istream& in,string& s) 
	{
		s.clear();
		char ch;
		ch = in.get();
		while (ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

getline同样也可以优化

	istream& getline(istream& in,string& s) 
	{

		//优化
		//避免频繁扩容 和 reserve的不合适
        s.clear();
		char ch;
		char buff[128];
		ch = in.get();
		size_t i = 0;
		while (ch != '\n') 
		{
			buff[i++] = ch;
			if (i == 127)
			{
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

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

		return in;

	}

8. 源码

详细源码 请猛戳 此处


网站公告

今日签到

点亮在社区的每一天
去签到