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

news2024/10/27 5:32:12
1.为什么手撕string类

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

2.string类的构造

//成员变量
private:
char* _str;
size_t _size;
size_t _capacity;

string::string - C++ Reference   文档

2.1 简单实现一个string类
#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[] 释放动态分配的内存,以避免内存泄漏。

补充:

#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 解决办法:

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

核心代码如下: 

//深拷贝构造函数
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 示例:
#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后的监视图如下:可以看到和上诉的情况一样,只完成浅拷贝/值拷贝,程序结束时,自动调用析构函数,同一块内存被析构多次,导致程序崩溃。 

解决方法:

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

// 赋值运算符重载
        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类迭代器模拟

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 类中实现类似的功能。

#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 示例代码:实现字符串插入
#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 示例代码:实现字符串删除
#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++有帮助,期待你的三连,你的支持就是我创作的动力!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2224447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线上环境的 JAVA 程序占用太多 CPU 资源,定位原因

线上环境的 JAVA 程序占用太多 CPU 资源&#xff0c;定位原因 top 命令执行显示一下结果 我们可以看到有一个 PID 是 4054 的应用程占用了超过一半的 CPU 资源&#xff0c;这是十分糟糕的事情&#xff0c;这个时候我们首先定位一下他是哪个线程在这里搞事情&#xff0c;这个时…

【JavaSE】认识String类,了解,进阶到熟练掌握

#1024程序员节 | 征文# 下面就让博主带领大家一起解决心中关于String类的疑问吧~~~ 1.字符串构造&#xff1a; 第一种和第二种&#xff08;有一定的区别&#xff0c;在常量池上&#xff09; public static void main(String[] args) { // 使用常量串构造 String s1 "h…

【机器学习】——numpy教程

文章目录 1.numpy简介2.初始化numpy3.ndarry的使用3.1numpy的属性3.2numpy的形状3.3ndarray的类型 4numpy生成数组的方法4.1生成0和1数组4.2从现有的数组生成4.3生成固定范围的数组4.4生成随机数组 5.数组的索引、切片6.数组的形状修改7.数组的类型修改8.数组的去重9.ndarray的…

【Visual Studio】下载安装 Visual Studio Community 并配置 C++ 桌面开发环境的图文教程

引言 Visual Studio 是一个面向 .NET 和 C 开发人员的综合性 Windows 版 IDE&#xff0c;可用于构建 Web、云、桌面、移动应用、服务和游戏。 安装步骤 访问 Visual Studio 的官方下载页面&#xff1a; https://visualstudio.microsoft.com/zh-hans/downloads/运行已下载的 V…

java疫苗发布和接种预约系统源码(springboot)

项目简介 疫苗发布和接种预约系统实现了以下功能&#xff1a; 疫苗发布和接种预约系统的主要使用者分为&#xff1a; 管理员对公告信息&#xff0c;医院信息&#xff0c;疫苗信息&#xff0c;医生信息&#xff0c;用户信息&#xff0c;论坛帖子信息以及预约接种信息等信息进行…

ThinkPad T480拆机屏幕改装:便携式显示器DIY指南

ThinkPad T480拆机屏幕改装&#xff1a;便携式显示器DIY指南 本文记录了将旧笔记本电脑 T480 拆机屏幕改装为便携式显示器的全过程。作者在决定升级设备后&#xff0c;选择通过 DIY 方式利用原有的屏幕资源。文章详细介绍了屏幕驱动板的安装、螺丝孔的剪裁、排线连接及固定的步…

系统性能优化——绑核

简要 绑核正如其名&#xff0c;将线程/进程绑定在一个或多个CPU核心。该技术可以使进程或线程在特定的处理器上运行&#xff0c;而不会被操作系统调度到其他处理器上。这里有两层含义。 如果线程被绑定在指定核心上&#xff0c;则只会在该核心上运行&#xff0c;即使其他核心…

Django自定义过滤器

一、介绍 Django过滤器是一种用于在Django模板中处理数据的技术。它们的主要作用是对模板中的变量进行加工、过滤或格式化&#xff0c;然后返回一个新的值供模板使用。这些过滤器可以在变量输出时&#xff0c;对输出的变量值做进一步的处理&#xff0c;以满足特定的显示需求。…

C# 串口通信教程

串口通信&#xff08;Serial Communication&#xff09;是一种用于设备之间数据传输的常见方法&#xff0c;通常用于与外部硬件设备&#xff08;如传感器、机器人、微控制器&#xff09;进行通信。在 C# 中&#xff0c;System.IO.Ports 命名空间提供了与串口设备交互的功能&…

Golang | Leetcode Golang题解之第508题出现次数最多的子树元素和

题目&#xff1a; 题解&#xff1a; func findFrequentTreeSum(root *TreeNode) (ans []int) {cnt : map[int]int{}maxCnt : 0var dfs func(*TreeNode) intdfs func(node *TreeNode) int {if node nil {return 0}sum : node.Val dfs(node.Left) dfs(node.Right)cnt[sum]if…

数字后端零基础入门系列 | Innovus零基础LAB学习Day6

今天没有具体的数字IC后端lab实验。今天的重点是熟悉掌握静态时序分析STA中的几类timing path以及setup和hold检查机制&#xff08;包含setup和hold计算公式&#xff09;。 芯片流片失败的那些故事 数字后端零基础入门系列 | Innovus零基础LAB学习Day5 等大家把今天内容学习…

设计模式(二)工厂模式详解

设计模式&#xff08;二&#xff09;工厂模式详解 简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子&#xff1a;Spring 中的 BeanFactory 使用简单工厂模式&#xff0c;产生 Bean 对象。 工厂模式简介 定义&#xff1a;工厂模式是一种创建…

Spring Boot框架下中小企业设备管理系统开发

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理中小企业设备管理系统的相关信息成为必然。…

Kafka-代码示例

一、构建开发环境 File > New > Project 选择一个最简单的模板 项目和坐标命名 配置maven路径 添加maven依赖 <dependencies><!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients --><dependency><groupId>org.apache.kaf…

最长子序列模型二(二分优化版)

文章目录 提高课题解一、拦截导弹二、导弹防御系统三、最长公共上升子序列四、二分函数速写 基础课题解五、最长上升子序列 II 提高课题解 一、拦截导弹 题目链接 第一问非常简单&#xff0c;直接用之前最长上身子序列模板就行 第二问就有难度了&#xff0c;我们要用最少的递…

基于SSM“毛毛宠物店”宠物信息交流平台的设计与实现

开发说明 开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myec…

DEV-C++如何调试

1、先编译&#xff0c;再点击“调试”按钮 2、使用调试按钮&#xff0c;可以输入输出数据 第21次发博客 以后会慢慢更新

【计网】从零开始认识IP协议 --- 认识网络层,认识IP报头结构

从零开始认识IP协议 1 网络层协议1.1 初步认识IP协议1.2 初步理解IP地址 2 IP协议报头3 初步理解网段划分 1 网络层协议 1.1 初步认识IP协议 我们已经熟悉了传输层中的UDP和TCP协议&#xff0c;接下来我们来接触网络层的协议&#xff1a; 网络层在计算机网络中的意义主要体现…

EXCELL中如何两条线画入一张图中,标记坐标轴标题?

1&#xff0c;打开excel&#xff0c;左击选中两列&#xff0c; 2&#xff0c;菜单栏>“插入”>”二维折线图”选中一个 3&#xff0c;选中出现的两条线中的一条右击>最下一行&#xff0c;“设置数据系列格式” 4&#xff0c;右测“系列选项中”>点击“次坐标轴” 5…

Java 开发——(上篇)从零开始搭建后端基础项目 Spring Boot 3 + MybatisPlus

一、概述 记录时间 [2024-10-23] 本文是一个基于 Spring Boot 3 MybatisPlus 的项目实战开发&#xff0c;主要涵盖以下几个方面&#xff1a; 从零开始的项目创建IDEA 中开发环境的热部署Maven、Swagger3、MybatisPlus 等的配置路由映射知识静态资源访问文件上传功能实现拦截器…