首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++篇】手撕string类:从初级到高级入门

【C++篇】手撕string类:从初级到高级入门

作者头像
熬夜学编程的小王
发布2024-11-20 20:32:58
发布2024-11-20 20:32:58
3590
举报
文章被收录于专栏:编程小王编程小王
1.为什么手撕string类

在面试或者一些学习场景中,手撕 string 类不仅仅是对字符串操作的考察,更多的是考察程序员对 C++ 内存管理的理解。例如,深拷贝与浅拷贝的实现,如何正确重载赋值运算符,如何避免内存泄漏,这些都是需要掌握的核心技能,本文章最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。

2.string类的构造
代码语言:javascript
复制
//成员变量
private:
char* _str;
size_t _size;
size_t _capacity;

string::string - C++ Reference 文档

2.1 简单实现一个string类
代码语言:javascript
复制
#include<cstring>
namespace A
{
	class string
	{
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

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

#include"string.h"
using namespace std;

int main()
{
	A::string s1("hello string!");
	return 0;
}
2.1.1 解释:
  • 构造函数:为字符串动态分配内存,并将传入的字符串内容复制到新分配的空间中。
  • 析构函数:使用 delete[] 释放动态分配的内存,以避免内存泄漏。

补充:

代码语言:javascript
复制
#include<cstring>
namespace A
{
	class string
	{
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

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


#include"string.h"
using namespace std;

int main()
{
	A::string s1("hello string!");
	A::string s2(s1);
	return 0;
}

解释:

未显示拷贝构造函数,默认生成拷贝构造函数会完成浅拷贝/值拷贝,导致一块内存空间被多个对象使用,析构函数会对这个空间析构多次,程序崩溃!

如下图:s1,s2同时指向同一块空间。

2.1.2 解决办法:

为了避免浅拷贝带来的问题,我们需要在拷贝构造函数中实现深拷贝。深拷贝确保每个对象都有自己独立的内存空间,不会与其他对象共享内存。

核心代码如下:

代码语言:javascript
复制
//深拷贝构造函数
string(const string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[s._size];
	strcpy(_str, s._str);
}

#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	A::string s2(s1);
	return 0;
}

内存图如下:

可以看到s1,s2对象指向不同的空间,完成深拷贝,程序正常运行。

3.赋值运算符重载与深拷贝
3.1 为什么需要重载赋值运算符?

在C++中,当我们将一个对象赋值给另一个对象时,默认情况下,编译器会为我们生成一个浅拷贝的赋值运算符。这意味着赋值后的对象和原对象会共享同一个内存空间,这会导致和浅拷贝相同的潜在问题,特别是在一个对象被销毁时,另一个对象继续使用该内存区域会引发错误。

3.2 实现赋值运算符重载

在赋值运算符重载中,我们需要考虑以下几点:

  1. 自我赋值:对象是否会被赋值给自己,避免不必要的内存释放和分配。
  2. 释放原有资源:在赋值前,我们需要释放被赋值对象原有的内存资源,避免内存泄漏。
  3. 深拷贝:为目标对象分配新的内存,并复制内容。
3.2.1 示例:
代码语言:javascript
复制
#include<cstring>
namespace A
{
	class string
	{
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

		//深拷贝构造函数
		string(const string& s)
		{
			_size = s._size;
			_capacity = s._capacity;
			_str = new char[s._size];
			strcpy(_str, s._str);
		}
		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

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


#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	A::string s2("hello C++");
	s1 = s2;
	return 0;
}

执行到语句 : s1 = s2后的监视图如下:可以看到和上诉的情况一样,只完成浅拷贝/值拷贝,程序结束时,自动调用析构函数,同一块内存被析构多次,导致程序崩溃。

解决方法:

显示深拷贝的赋值运算符重载

代码语言:javascript
复制
// 赋值运算符重载
        string& operator=(const string& s){
            if (this != &s) {  // 避免自我赋值
                delete[] _str;  // 释放原有内存
                _size = s._size;
                _capacity = s._capacity;
                _str = new char[_capacity + 1];  // 分配新内存
                strcpy(_str, s._str);  // 复制内容
            }
            return *this;
        }


#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	A::string s2("hello C++");
	s1 = s2;
	return 0;
}

完成深拷贝后程序,s1,s2 指向不同的空间,如下图:

3.2.2 解读代码 自我赋值检查:自我赋值是指对象在赋值时被赋值给自己,例如 s1 = s1。在这种情况下,如果我们没有进行检查,就会先删除对象的内存,然后再试图复制同一个对象的内容,这样会导致程序崩溃。因此,重载赋值运算符时,自我赋值检查是非常必要的。

释放原有内存:在分配新内存之前,我们必须先释放旧的内存,以防止内存泄漏。

深拷贝:通过分配新的内存,确保目标对象不会与源对象共享内存,避免浅拷贝带来的问题。

4.迭代器与字符串操作
4.1 迭代器的实现

迭代器是一种用于遍历容器(如数组、string 等)的工具,它允许我们在不直接访问容器内部数据结构的情况下遍历容器。通过迭代器,可以使用范围 for 循环等简便的方式遍历 string 对象中的字符。

4.1.1 string类迭代器模拟

代码语言:javascript
复制
namespace A
{
class string{
//迭代器
itreator begin()
{
	return _str;
}

itreator end()
{
	return _str + _size;
}

//const迭代器
const_itreator begin()const
{
	return _str;
}

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


#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	for (auto ch : s1)
		cout << ch;
	cout << endl;
	A::string const s2("hello C++!");
	for (auto ch : s2)
		cout << ch;
	cout << endl;
	return 0;
}
5.字符串的常见操作

在 C++ 标准库 string 类中,提供了很多方便的字符串操作接口,如查找字符或子字符串、插入字符、删除字符等。我们也需要在自定义的 string 类中实现这些操作。接下来,我们将逐步模拟实现这些功能,并进行测试。

5.1 查找操作

C++ 中 string 类的 find() 函数用于查找字符串或字符在当前字符串中的位置。如果找到了字符或子字符串,find() 会返回其位置;如果找不到,则返回 string::npos

我们将在自定义的 string 类中实现类似的功能。

代码语言:javascript
复制
#include<string.h>
#include<assert.h>
namespace A
{
	class string
	{
		//typedef char* itreator;
		using itreator = char*;
		using const_itreator = const char*;
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

		//迭代器
		itreator begin()
		{
			return _str;
		}

		itreator end()
		{
			return _str + _size;
		}

		//const迭代器
		const_itreator begin()const
		{
			return _str;
		}

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


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

		// 赋值运算符重载
		string& operator=(const string& s) {
			if (this != &s) {  // 避免自我赋值
				delete[] _str;  // 释放原有内存
				_size = s._size;
				_capacity = s._capacity;
				_str = new char[_capacity + 1];  // 分配新内存
				strcpy(_str, s._str);  // 复制内容
			}
			return *this;
		}
		    size_t find(const string& str, size_t pos = 0) const;

			// 查找子字符串在字符串中的第一次出现位置
			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;
				}
			}
			// 查找字符在字符串中的第一次出现位置
			size_t find(char c, size_t pos = 0) const
			{
				assert(pos<_size);
				for (int i = pos; i < _size; i++)
				{
					if (_str[i] == c)
						return i;
				}
				return npos;
			}

		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		static const size_t npos = -1;  // 定义 npos 为 -1,表示未找到
	};
}

#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	for (auto ch : s1)
		cout << ch;
	cout << endl;
	A::string const s2("hello C++!");
	for (auto ch : s2)
		cout << ch;
	cout << endl;

	size_t ret=s1.find('g', 0);
	cout << ret << endl;

	 ret = s1.find("str", 0);
	cout << ret << endl;
	return 0;
}

输出结果:

hello string! hello C++! 11 6

5.1.1 为什么 static const size_t npos = -1 可以在类内初始化?

size_t 是一种整型类型,尽管其大小和符号位取决于平台,但它仍然是整型常量的一种。因此,npos 的初始化类似于前面提到的整型静态成员变量。由于 -1 可以表示为 size_t 的最大值,这个值在编译时就可以确定,因此它符合类内初始化的条件。

class String { public: static const size_t npos = -1; // 可以在类内初始化 };

总结:因为 npos 是整型常量,并且编译器可以在编译时确定其值,(只要是在编译时可以确定为常量就可以在类内初始化,无任何限制)符合在类内部初始化的条件。

5.2 插入操作

C++ 中的 string 类允许我们在字符串的任意位置插入字符或子字符串。接下来,我们将在自定义的 string 类中实现类似的插入功能。

5.2.1 示例代码:实现字符串插入
代码语言:javascript
复制
#include<string.h>
#include<assert.h>
namespace A
{
	class string
	{
		//typedef char* itreator;
		using itreator = char*;
		using const_itreator = const char*;
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

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

				_capacity = n;
			}
		}

		// 在指定位置插入字符串
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				size_t newcapacity = 2 * _capacity;
				//扩2倍不够,需要多少扩多少
				if (newcapacity < _size + len)
					newcapacity = _size + len;
				reserve(newcapacity);
			}
			size_t end = _size + len;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				--end;
			}
			for (size_t i = 0; i < len; i++)
			{
				_str[pos++] = str[i];
			}
			_size += len;
		}
			// 在指定位置插入字符
			string& insert(size_t pos, char c)
			{
				assert(pos <= _size);

				if (_size == _capacity) {
					reserve(_capacity * 2);  // 如果容量不够,扩展容量
				}

				for (size_t i = _size; i > pos; i--)
				{
					_str[i] = _str[i - 1];
				}
				_str[pos] = c;
				_size++;
				_str[_size] = '\0';

				return *this;
			}
		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		static const size_t npos = -1;  // 定义 npos 为 -1,表示未找到
	};
}

#include"string.h"
#include<iostream>
using namespace std;

int main()
{
	A::string s1("hello string!");
	s1.insert(11, "hello C++");
	return 0;
}
5.3 删除操作

string 类允许我们删除指定位置的字符或子字符串。接下来,我们实现字符串的删除功能。

5.3.1 示例代码:实现字符串删除
代码语言:javascript
复制
#include<string.h>
#include<assert.h>
namespace A
{
	class string
	{
		//typedef char* itreator;
		using itreator = char*;
		using const_itreator = const char*;
	public:
		//默认构造函数
		string(const char* str = " ")
			:_size(strlen(str))//字符有效个数
		{
			_capacity = _size;//容量大小
			_str = new char[_size + 1];//申请内存空间
			strcpy(_str, str);//拷贝数据
		}

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

				_capacity = n;
			}
		}

		// 在指定位置插入字符串
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				size_t newcapacity = 2 * _capacity;
				//扩2倍不够,需要多少扩多少
				if (newcapacity < _size + len)
					newcapacity = _size + len;
				reserve(newcapacity);
			}
			size_t end = _size + len;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				--end;
			}
			for (size_t i = 0; i < len; i++)
			{
				_str[pos++] = str[i];
			}
			_size += len;
		}
			// 在指定位置插入字符
			string& insert(size_t pos, char c)
			{
				assert(pos <= _size);

				if (_size == _capacity) {
					reserve(_capacity * 2);  // 如果容量不够,扩展容量
				}

				for (size_t i = _size; i > pos; i--)
				{
					_str[i] = _str[i - 1];
				}
				_str[pos] = c;
				_size++;
				_str[_size] = '\0';

				return *this;
			}

			//删除字符或字符串
			void erase(size_t pos, size_t len)
			{
				assert(pos < _size);
				if (len >= _size - len)
				{
					_str[pos] = '\0';
				}
				else
				{
					size_t end = pos + len;
					while (end <= _size)
					{
						_str[end - len] = _str[end];
						++end;
					}
					_size -= len;
				}
			}
		//析构函数
		~string()
		{
			delete[]_str;//释放空间
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		static const size_t npos = -1;  // 定义 npos 为 -1,表示未找到
	};
}

相信通过这篇文章你对string类的有了初步的了解。如果此篇文章对你学习C++有帮助,期待你的三连,你的支持就是我创作的动力!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.为什么手撕string类
  • 2.string类的构造
    • 2.1 简单实现一个string类
    • 2.1.1 解释:
    • 2.1.2 解决办法:
  • 3.赋值运算符重载与深拷贝
  • 3.1 为什么需要重载赋值运算符?
  • 3.2 实现赋值运算符重载
    • 3.2.1 示例:
  • 4.迭代器与字符串操作
    • 4.1 迭代器的实现
  • 5.字符串的常见操作
    • 5.1 查找操作
      • 5.1.1 为什么 static const size_t npos = -1 可以在类内初始化?
    • 5.2 插入操作
      • 5.2.1 示例代码:实现字符串插入
    • 5.3 删除操作
      • 5.3.1 示例代码:实现字符串删除
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档