从 0 到 1 实现 C++ string 类:深入理解动态字符串的底层机制--《Hello C++ Wrold!》(11)--(C/C++)

发布于:2025-06-24 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

在 C++ 编程中,标准库提供的string类是处理文本数据的核心工具,但它的内部实现细节却如同一个 “黑箱”。理解string类的底层原理,不仅是面试中的高频考点,更是掌握 C++ 内存管理与泛型编程思想的关键一步。本章节将从零开始模拟实现一个简化版的string类,通过手动实现动态扩容、迭代器、拷贝构造等核心功能,带你揭开string类的神秘面纱。

我们的实现将聚焦于以下核心内容:
动态内存管理:通过reserve和resize实现按需扩容,避免频繁内存分配

迭代器设计:模拟实现begin()和end()接口,支持范围 for 循环等 STL 算法

拷贝控制:深入探讨浅拷贝与深拷贝的区别,通过现代 C++ 手法实现高效赋值

核心操作实现:包括插入、删除、查找等关键接口,理解其中的边界处理与性能优化

在实现过程中,我们将特别关注以下易错点:
内存泄漏与野指针问题(如析构函数的正确实现)

扩容时的数据迁移与\0结尾符的处理

插入 / 删除操作中的大量数据移动开销

此外,我们还将延伸讨论 “写时拷贝(Copy-On-Write)” 技术,了解如何通过引用计数优化内存使用效率。通过这个完整的实现案例,你不仅能掌握string类的底层原理,更能深刻理解 C++ 中资源管理、深拷贝 / 浅拷贝等重要概念。

无论你是希望深入理解 STL 容器的开发者,还是正在备战技术面试的求职者,亲手实现一个string类都是一次不可多得的学习机会。让我们一起动手,从底层构建属于自己的字符串类,感受 C++ 内存管理的精妙之处!

string类的模拟实现(可能和库里面的有偏差哈)

namespace renshen
{
	class string
	{
	public:
		typedef char* iterator;//模拟实现迭代器
		typedef const char* const_iterator;//只能读的迭代器
//这俩搞的是内部类,在main函数里用要:renshen::string::iterator这样才是一个类型
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

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

		string(const char* str = "")//构造函数
//""是C风格字符串,自带\0的
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
	      strcpy(_str, str);
		}

		string(const string& s)//拷贝构造函数
		{
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s._size + 1);
//注意,这里要是memcpy,不能是strcpy;因为字符串中间可能有\0这些
			_size = s._size;
			_capacity = s._capacity;
		}

/*string(const string& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
这里必须要初始化,
因为delete[]一个未初始化的_str,导致未定义行为

{
	string tmp(s._str);->如果对象是h\0a的话就遭了,因为这个时候会用上面那个构造函数
	swap(tmp);
}*//这种的用现代写法不好


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

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

		size_t size() const
		{
			return _size;
		}

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

		const char& operator[](size_t pos) const 
//const renshen::string s1;这种要用这个,而且char&前面那个const不能去掉的
		{
			assert(pos < _size);
			return _str[pos];
		}
//库里面也是这样写两种的

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

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);//一般这么搞的
                //判断_capacity这一下很巧妙
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

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

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

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

		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);
			if (_size +n > _capacity)
			{
				reserve(_size + n);
			}

			size_t end = _size;
			while (end >= pos && end != npos)
  //注意这里的end!=npos避免了pos为0的话出现的异常
//异常情况的诱因:end是size_t类型的
//注意:改成int也不行,因为有符号和无符号比较,有符号会整型提升成无符号进行比较
//int转成double叫做算数转换哈
			{
				_str[end + n] = _str[end];
				--end;
			}

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

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += len;
		}

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			if (len == npos || pos + len >= _size)//注意这里顺序的巧妙之处
			{
				_str[pos] = '\0';
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}
//由此可见:insert和erease尽量少用,代价太大了

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

		size_t find(const char* str , size_t pos = 0)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);//注意strstr的用法
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

  string substr(size_t pos = 0, size_t len = npos)//pos是开始查的位置(此需查)
		{
			assert(pos < _size);
			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
//new出来的空间虽然出了函数还在,可是tmp是string类型的,有析构函数,出了函数就被析构释放了
//之后里面的东西会深拷贝出去哈
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];//注意理解这里,和s1+="a"再+="b"是一个道理
                //这样搞的话,\0永远还是在最后
			}

			return tmp;
		}

  void resize(size_t n, char ch = '\0')
 {
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		reserve(n);

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

		_size = n;
		_str[_size] = '\0';
	}


bool operator<(const string& s) const
{
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
 // 不要搞成_size+1这样,这样就把\0也比进去了,虽然答案还是一样,但是意义就相悖了
      //(字典序那样比较大小是不把\0结束标志搞进去的) 
	return ret == 0 ? _size < s._size : ret < 0;
}

bool operator==(const string& s) const
{
	return _size == s._size 
		&& memcmp(_str, s._str, _size) == 0;//这种先后顺序会好些
}
//只要实现两个,其他的都能用这俩个实现了

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

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
//不能写成std::swap(tmp,*this);不然会无穷递归
}

string& operator=(string tmp)
{
	swap(tmp);//这里调用函数,swap里面依旧是可以直接用那个的成员变量

	return *this;
}//这里返回*this最主要想满足链式赋值

}

	private:
		size_t _size;
		size_t _capacity;//像这种肯定是非负数的模拟实现一般要搞成size_t
		char* _str;

	public:
		const static size_t npos;
	};

	const size_t string::npos = -1;//这个也能放在命名空间里
std::ostream& operator<<(std::ostream& out, const string& s)
//没在命名空间里搞的话,要写成renshen::string
{
      for (auto ch : s)
	{
		out << ch;
	}

	return out;
}
std::istream& operator>>(std::istream& in, string& s)
{
	s.clear();
	char ch = in.get();
	// 处理前缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}

	char buff[128];//这样到一定容量才放的话会比s直接+ch的频繁扩容要好
	int i = 0;

	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();//注意这个get是cin的成员函数
	}

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

	return in;
}
};


在这里插入图片描述

注意点:1.记得检查pos位置是否合法

        2.string里面的s1.[s1.size()]位置存的是`\0`

        3.范围for只要模拟实现出begin和end这两个迭代器就可以用了

         (这两个迭代器的名字都不能变,范围for的识别很死板的)

     4.自己有些遗忘strcpy和memcpy的用法了:
char * strcpy ( char * destination, const char * source );
引申:string类型不能被隐式转换成C风格的
memcpy的话是可以指定到哪截止   strcpy的话是遇到\0就终止(但他会默认拷贝上\0)
引申:回忆下strcmp和memcmp
 5.扩容时要注意_capacity是不是等于0

6.自己重载出来的<<和直接cout<<s1.c_str()出来的还是有不同的,比如:在string类型的字符串下中间有空格时

7.流插入和流提取不让拷贝

在这里插入图片描述

写时拷贝

用途:用来解决浅拷贝相较于深拷贝的问题(由于指向同一空间导致的)

1.会析构多次导致错误 2.一个对象修改了会影响多个对象的内容

引入的一个引用计数去解决这两个问题

解决方法:第一个问题:引用计数是1时进行析构才让析构

           第二个问题:在修改时,如果引用计数不是1,就先进行深拷贝,再修改

作用:可以避免一些用浅拷贝就可以满足的场景却用了深拷贝


网站公告

今日签到

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