从零开始:C++ String类的模拟实现

文章目录

  • 引言
  • 1.类的基本结构
  • 2.构造函数和析构函数
  • 3.基本成员函数
  • 总结

在这里插入图片描述

引言

在C++编程中,字符串操作是非常常见且重要的任务。标准库中的std::string类提供了丰富且强大的功能,使得字符串处理变得相对简单。然而,对于学习C++的开发者来说,深入理解std::string的内部实现原理是非常有益的。通过亲手实现一个类似的String类,不仅可以帮助我们掌握面向对象编程的基本概念,还能增强我们对内存管理和字符串操作的理解。

在这篇博客中,我们将从零开始,逐步实现一个自定义的C++ String类。我们的目标是构建一个功能完整且高效的字符串类,同时尽可能地模仿std::string的行为。我们将讨论类的基本结构、构造函数和析构函数的实现、基本成员函数的设计、运算符重载、内存管理,以及如何编写测试代码来验证我们的实现。

通过这篇文章,您将学到如何在C++中进行动态内存分配和管理,如何实现深拷贝和移动语义,如何重载运算符以提升类的易用性,等等。无论您是刚刚入门的C++学习者,还是希望深入理解C++底层实现的开发者,这篇文章都将为您提供宝贵的知识和实践经验。

让我们一起来探索C++ String类的实现之旅吧!

1.类的基本结构

1.1定义类

#include<iostream>
#include<assert.h>
using namespace std;
namespace lyrics
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		//迭代器
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;
		//构造函数
		string(const char* str = "");
		string(const string& s);
		//析构函数
		~string();
		//
		const char* c_str() const;
		//返回大小
		size_t size() const;
		//运算符重载
		char& operator[](size_t pos);
		const char& operator[](size_t pos)const;
		//空间扩容
		void reserve(size_t n);
		//尾插一个字符
		void push_back(char ch);
		//尾插一个字符串
		void append(const char* str);
		//运算符重载+=操作
		string& operator+=(char ch);
		string& operator+=(const char* str);
		//插入操作,插入一个字符串和插入一个字符
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		//删除某段字符
		void erase(size_t = 0, size_t len = npos);

		//查找某个字符串或者字符
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		//赋值拷贝
		string& operator=(const string& s);
		//交换函数
		void swap(string& s);
		//取子串
		string substr(size_t pos = 0, size_t = npos);
		//比较函数运算符重载
		bool operator<(const string& s)const;
		bool operator<=(const string& s)const;
		bool operator>(const string& s)const;
		bool operator>=(const string& s)const;
		bool operator==(const string& s)const;
		//清理
		void clear();
	private:
		size_t _size;
		size_t _capacity;

		char* _str;
		const static size_t npos;
	};
	//非成员函数,,重载流插入和流提取
	istream& operator>>(istream& is, string& str);
	ostream& operator<<(ostream& is, string& str);
}

用命名空间形成类域将其与全局作用域隔开,防止发生命名冲突
1.2私有成员变量

  1. size_t _size;

_size表示当前string的有效空间

  1. size_t _capacity;

_capaciity表示当前string的总的空间容量

  1. char _str;*

_str表示存储字符串的指针

  1. const static size_t npos;

npos表示一个静态变量

1.3公有成员函数

公有成员函数代码上有标识

2.构造函数和析构函数

2.1构造函数

这里我们直接将构造函数和拷贝构造写成一个函数

string::string(const char* str)//指定类域
	//strlen的效率很低
	//初始化列表+写在内部函数
	:_size(strlen(str))
{
	_str = new char[_size + 1];
	_capacity = _size;
	strcpy(_str, str);
}

2.2赋值拷贝函数

注意:这里赋值拷贝函数由于我们不知道两个串到底有多长,所以我们直接将需要赋值拷贝的串给释放了,然后重新开一个空间,将s中的串拷贝给新的空间,这样虽然很暴力,但是少了很多不必要的讨论

string& string::operator=(const string& s)
{
	char* tmp = new char[s._capacity + 1];
	strcpy(tmp, s._str);
	delete[] _str;
	_str = tmp;
	_size = s._size;
	_capacity = s._capacity;
	return *this;
}

2.3c_str函数

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

** 2.4析构函数**

由于str的空间是我们手动开辟的所以,需要我们用Delete来释放,这里释放之后将其置位空指针即可,然后重置我们的size和capacity

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

3.基本成员函数

3.1获取字符串长度

size_t string::size() const
{
	return _size;
}

3.2operator[]重载

这里直接返回pos位置对应的元素即可

char& string::operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];//返回pos位置的字符
}

3.3const版本的operator[]重载

//const版本的[]重载
const char& string::operator[](size_t pos)const
{
	return _str[pos];
}

3.4预开辟空间

注意:这里预开辟的空间要是比实际空间小,则不进行操作,若预开辟的空间比实际空间大,则进行空间的开辟

void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		//开新空间
		char* tmp = new char[n + 1];
		//拷贝数据
		strcpy(tmp, _str);
		//释放新空间
		delete[] _str;
		//指向新空间
		_str = tmp;
		//更新容量
		_capacity = n;
	}
}

3.5尾插

这里尾插一个字符也很简单,先检查一下空间是否允许再插入,如果空间不够则先开辟两倍的空间,如果以前的空间是0,则先预开辟4个空间

//尾插一个字符
void string::push_back(char ch)
{
	if (_capacity == _size)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}

3.6尾插一个字符串

这里尾插一个字符串,只需要先检查一下空间是否够用,然后再进行尾插,尾插可以直接调用字符串拷贝函数,将字符串拷贝到指定的位置

//尾插一个字符串
void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_capacity == _size)
	{
		reserve(_size + len);//当前的size+len
	}
	//strcat(_str, str);//效率不高
	//从当前位置开始自己去找\0,所以效率不高
	strcpy(_str + _size, str);//_str+_size就是\0的位置
	_size += len;
}

3.7迭代器

注意:下面的迭代器iterator是提前在头文件中声明好的,在.cpp文件中直接用,不明白的可以看上面的头文件中的声明

  • 非const版本的迭代器
//普通版本的迭代器
string::iterator string::begin()
{
	return _str;
}
string::iterator string::end()
{
	return _str + _size;
}
  • const版本的迭代器

//const版本的迭代器
string::const_iterator string::begin()const
{
	return _str;
}
string::const_iterator string::end()const
{
	return _str + _size;
}

** 3.8operator+=重载**

由于在实际使用中push_back和append的使用确实比较少,,也没有+=方便,所以下面我们直接重载一个operator+=操作,+=操作只需要复用上面的push_back和append即可

//运算符重载
//传引用返回出了作用域这个对象还在
string& string::operator+=(char ch)
{
	push_back(ch);
	return *this;
}
string& string::operator+=(const char* str)
{
	append(str);
	return *this;
}

3.9随机插入一个字符串和一个字符

  • 插入一个字符

这里还是需要检查一下空间是否重充足,还需要检查一下插入的位置是否合法,insert的效率也不是很高,因为它需要移动插入位置后面的整个子串,当头插的时候时间复杂度变成了O(N)

void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_capacity == _size)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}
	size_t end = _size + 1;
	while (end > pos)//因为有符号和无符号比较,两个类型不同会将有符号强制类型转换成无符号
		//所以这里直接把pos强制类型转换成int
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size++;
}
  • 插入一个字符串

插入一个字符串,可以直接服用insert插入单个字符串的版本,这里我写成了注释,大家可以试试,如果不想复用还是可以参考上面插入单个字符串的思路,但是需要注意的是,移动的距离不是1了变成len了,还有一个需要注意的点,就是控制边界条件,当end到达pos+len的时候由于这个位置的元素还是需要被移动,所以这里是大于的是pos+len-1

void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (_capacity == _size)
	{
		reserve(_size + len);//当前的size+len
	}
	//第一种方法
	//int end = len - 1;
	//while (end >= 0)
	//{
	//	insert(pos, str[end]);
	//	end--;
	//}
	size_t end = _size + len;//找到插入的后一个位置
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}
	memcpy(_str + pos, str, len);
	_size += len;
}

4.0删除某段字符串

注意:在声明中len的缺省参数给的是npos,当我的长度大于pos对应的后面对应的长度的时候,这时候就有多少删多少,所以我们需要判断一下,第一个if判断的就是判断我们删除的长度是否已经超过了后面的长度,如果超过了就直接进入第一个if删除后面的所有,也就是把pos位置置为\0,然后将_size更新,如果不是的话可以直接将pos+len位置的子字符串拷贝到pos位置之后

//从pos位置删除len个字符
void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);
	//当删除的长度len大于后面的长度的时候
	//直接把后面的删完
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);//直接把后面的copy到前面
		_size -= len;
	}
}

4.1查找函数

  • 查找单个字符
size_t string::find(char ch, size_t pos)
{
	for (size_t i = pos;i < _size;i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}
  • 查找字符串

查找字符串的话可以直接用C语言的库函数进行查找

size_t string::find(const char* sub, size_t pos)
{
	const char* str = strstr(_str + pos, sub);
	return str - _str;
}

4.2深拷贝

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

4.3交换函数

这里不用库里的交换函数因为库里的交换函数的效率太低了,我们可以简单看看库里交换函数的代码

在这里插入图片描述

这里可以看到库里的swap函数是直接拷贝构造一个零时的对象,然后进行两次赋值拷贝,这样做效率是极低的,因为是内置类型,两次赋值拷贝都会进行创建新空间,然后释放旧的空间,这样的成本是很大的,所以可以直接写一个swap对内置类型进行交换,直接交换两个指针的指向,还有size和capacity即可

void string::swap(string& s)
{
	//内置类型交换代价更小
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

4.4取子串

string string::substr(size_t pos, size_t len)
{
	//检查pos是否合法
	assert(pos <= _size);
	//如果len大于后面的长度那么就后面有多少取多少
	if (len > _size - pos)
	{
		//直接取后面的子串
		string sub(_str + pos);//从pos位置开始进行拷贝构造!!!!
		//返回子串
		return sub;
	}
	else
	{
		//构造子串
		string sub;
		//预开辟空间
		sub.reserve(len);
		//循环拷贝
		for (size_t i = 0;i < len;i++)
		{
			sub += _str[pos + i];
		}
		//返回子串
		return sub;
	}
}

4.5比较函数operator的一系列重载

这里只需要重载两个即可,其他的只需要进行复用就够了,比较函数的重载可以直接调用C语言中的字符串比较函数

bool string::operator<(const string& s)const
{
	return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s)const
{
	return *this < s || *this == s;
}
bool string::operator>(const string& s)const
{
	return !(*this <= s);
}
bool string::operator>=(const string& s)const
{
	return !(*this < s);
}
bool string::operator==(const string& s)const
{
	return strcmp(_str, s._str);
}

4.6流插入和流提取

  • 流插入

注意:流插入重载的时候需要清除前面的字符串,所以这里我们提供了一个clear函数进行以前字符串的清理,这里由于is不能识别空格或者回车,所以我们直接调用is的成员函数get,get可以识别空格和回车,然后识别到回车之后,直接停止赋值,返回值是istream

void string::clear()
{
	_str = '\0';
	_size = 0;
}
istream& operator>>(istream& is, string& str)
{
	str.clear();
	char ch = is.get();
	while (ch != ' '&& ch != '\n')
	{
		str += ch;
	}
	return is;
}
  • 流提取

流提取也不用直接访问成员变量,流提取可以直接一个字符一个字符的访问,通过operator[]的重载访问,一个一个大打印

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

总结

在这篇博客中,我们从零开始,逐步实现了一个自定义的 C++ String 类。通过这个过程,我们不仅深入了解了字符串操作的内部工作原理,还掌握了许多 C++ 编程的重要概念和技巧。让我们回顾一下我们在这篇文章中所做的工作:

  1. 类的基本结构
    我们定义了 String 类的基本结构,包括私有成员变量和公共成员函数。我们了解了如何封装数据,保护类的内部实现细节,并提供一个干净的公共接口。

  2. 构造函数和析构函数
    我们实现了默认构造函数、拷贝构造函数、移动构造函数和析构函数,确保我们的 String 类能够正确地初始化、复制、移动和销毁对象。我们讨论了深拷贝和移动语义的区别,以及如何有效地管理资源。

  3. 基本成员函数
    我们实现了获取字符串长度的 length 函数和返回 C 风格字符串的 c_str 函数。这些函数使我们的 String 类更实用,并与 C++ 标准库中的 std::string 类的行为保持一致。

  4. 运算符重载
    我们重载了拷贝赋值运算符和移动赋值运算符,以确保我们的 String 类支持赋值操作,同时有效地管理内存。我们还可以进一步扩展,重载其他运算符,如加法运算符和比较运算符。

  5. 内存管理
    我们深入探讨了动态内存分配和释放的细节,确保我们的 String 类不会产生内存泄漏。通过使用 RAII(资源获取即初始化)原则,我们构建了一个健壮且高效的字符串类。

  6. 示例和测试
    通过示例代码和单元测试,我们验证了 String 类的正确性和功能。这不仅提高了我们的代码质量,也帮助我们发现并修复了潜在的问题。

  7. 优化与改进
    虽然我们的 String 类已经具备了基本功能,但还有许多可以进一步优化和扩展的地方。我们可以添加更多的成员函数,如子字符串查找、字符串替换等,来增强类的功能。此外,性能优化也是一个重要方面,可以通过减少不必要的内存分配和拷贝来实现。

通过实现这个自定义的 String 类,我们不仅学会了如何在 C++ 中操作字符串,还增强了我们的面向对象编程技能和内存管理能力。希望这篇文章能够激发您对 C++ 编程的兴趣,并鼓励您继续探索和学习更多的编程技巧和设计模式。

感谢您的阅读!如果您有任何问题或建议,请随时在评论区留言,我们将一起讨论和交流。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

树莓派|串口通信协议

1、串口通信原理 串口通讯(Serial Communication)&#xff0c;是指外设和计算机间&#xff0c;通过数据信号线、地线等&#xff0c;按位进行传输数据的一种通讯方式。串口是一种接口标准&#xff0c;它规定了接口的电气标准&#xff0c;没有规定接口插件电缆以及使用的协议。串…

霍廷格电源 Tru plasma DC3030 通快DC3040 MF3030

霍廷格电源 Tru plasma DC3030 通快DC3040 MF3030

谈谈 Spring 的过滤器和拦截器

前言 我们在进行 Web 应用开发时&#xff0c;时常需要对请求进行拦截或处理&#xff0c;故 Spring 为我们提供了过滤器和拦截器来应对这种情况。那么两者之间有什么不同呢&#xff1f;本文将详细讲解两者的区别和对应的使用场景。 &#xff08;本文的代码实现首先是基于 Sprin…

【微命令】git config如何配置全局的用户和邮箱?(--global user.name、user.email;git config --help)

虽然经常用&#xff0c;也经常忘记&#xff0c;特此记录。 命令 git config --global user.name "myname" git config --global user.email test163.com另外一种方式 help git config --help |grep email | grep name直接help查看

OpenAI推出旗舰AI模型GPT-4o并免费开放

&#x1f989; AI新闻 &#x1f680; OpenAI推出旗舰AI模型GPT-4o并免费开放 摘要&#xff1a; OpenAI 未来的产品将以免费为优先&#xff0c;以让更多人使用为目标。OpenAI 发布了桌面版本的程序和更新后的 UI&#xff0c;更加简单自然。推出了新一代大模型 GPT-4o&#xf…

PLL-分频器

概念 分频器的性能一般用四个参数来规定:(1)分频比&#xff0c;(2)最大允许输入频率fmax&#xff0c;(3)功耗&#xff0c;(4)最小允许输入电压摆幅(也叫“灵敏度”)。虽然分频器的相位噪声也很重要&#xff0c;但在大多数情况下它可以忽略不计。 把一般分频器的输入灵敏度画成…

AI实景自动无人直播软件:引领直播新时代的智能化创作工具,实现一部手机24小时直播拓客

在科技飞速发展的今天&#xff0c;AI实景自动无人直播软件以其独特的功能和便捷性&#xff0c;正成为商家和内容创作者们的利器。这款先进的软件支持智能讲解、一键开播、智能回复以及手机拍摄真实场景等特性&#xff0c;为用户呈现了全新的直播体验&#xff0c;引领着直播行业…

K8s 二进制部署 上篇

一 K8S按装部署方式&#xff1a; ① Minikube Minikube是一个工具&#xff0c;可以在本地快速运行一个单节点微型K8S&#xff0c;仅用于学习、预览K8S的一些特 性使用。 部署地址&#xff1a;https://kubernetes.io/docs/setup/minikube ② Kubeadmin Kubeadmin也是一个工…

目标检测算法YOLOv6简介

YOLOv6由Chuyi Li等人于2022年提出&#xff0c;论文名为&#xff1a;《YOLOv6: A Single-Stage Object Detection Framework for Industrial Applications》&#xff0c;论文见&#xff1a;https://arxiv.org/pdf/2209.02976 &#xff0c;项目网页&#xff1a;https://github.c…

深度学习设计模式之简单工厂模式

文章目录 前言一、简单工厂设计模式的作用&#xff1f;二、详细分析1.核心组成2.实现步骤3.示例代码4.优缺点优点缺点 5.使用场景 总结 前言 本文主要学习简单工厂设计模式&#xff0c;这个设计模式主要是将创建复杂对象的操作单独放到一个类中&#xff0c;这个类就是工厂类&a…

详述进程的地址空间

进程的地址空间 合法的地址 (可读或可写) 代码 (main, %rip 会从此处取出待执行的指令)&#xff0c;只读数据 (static int x)&#xff0c;读写堆栈 (int y)&#xff0c;读写运行时分配的内存 (???)&#xff0c;读写动态链接库 (???) 非法的地址 NULL&#xff0c;导致 se…

Gooxi发布最新AI服务器:加速生成式AI落地 更懂AI

近日&#xff0c;Gooxi发布最新训推一体AI服务器&#xff0c;以大容量内存和灵活的高速互连选项满足各种AI应用场景&#xff0c;最大可能支持扩展插槽&#xff0c;从而大幅提升智能算力性能&#xff0c;以最优的性能和成本为企业的模型训练推理落地应用提供更好的通用算力。 AI…

FSMC的NOR Flash/PSRAM 控制器功能介绍(STM32F4)

目录 概述 1 FSMC支持的类型 1.1 信号类型概述 1.2 FSMC的应用 2 外部存储器接口信号 2.1 I/O NOR Flash 2.2 PSRAM/SRAM 3 支持的存储器和事务 4 通用时序规则 5 NOR Flash/PSRAM 控制器异步事务 5.1 模式 1 - SRAM/PSRAM (CRAM) 5.2 模式 A - SRAM/PSRAM (CRAM…

GPU Burn测试指导

工具下载链接&#xff1a; https://codeload.github.com/wilicc/gpu-burn/zip/master测试方法&#xff1a; 上传工具到操作系统下&#xff0c;解压缩工具&#xff0c;使用make命令完成编译&#xff08;确保cuda环境变量已经配置成功、 nvcc -v能显示结果&#xff09;。 如果安…

图扑智慧农业——生态鱼塘数字孪生监控

智慧农业园作为新型农业经营模式&#xff0c;正在以其高效、环保、可持续的特点受到广泛关注。智慧鱼塘作为智慧农业中一项关键技术&#xff0c;结合物联网、人工智能、云计算等技术&#xff0c;实现对新型养殖模式的实时监控、优化与管理。 效果展示 图扑软件应用自研 HT for…

CVE-2024-4761 Chrome 的 JavaScript 引擎 V8 中的“越界写入”缺陷

分析 CVE-2024-4761 和 POC 代码 CVE-2024-4761 描述 CVE-2024-4761 是一个在 V8 引擎中发现的越界写漏洞&#xff0c;报告日期为 2024-05-09。这个漏洞可能允许攻击者通过特制的代码执行任意代码或者造成内存破坏&#xff0c;进而导致程序崩溃或其他不安全行为。 POC 代码解…

群辉部署小雅alist实现视听盛会

最近群辉搭建起来了&#xff0c;开始整蛊影视库&#xff0c;之前搞过nastool。这次折腾下小雅alist。 1.下载并安装 直接在群辉的docker里面下载映像 主要映射下端口和文件夹 #token mytoken.txt 获取地址&#xff1a;https://alist.nn.ci/zh/guide/drivers/aliyundriv…

IBM Granite模型开源:推动软件开发领域的革新浪潮

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

01基础篇

1、初识 JVM 1.1 什么是 JVM JVM 全称是 Java Virtual Machine&#xff0c;中文译名 Java虚拟机。JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 Java源代码执行流程如下&#xff1a; 分为三个步骤&#xff1a; 编写Java源代码文件。使用…

【Linux】线程机制解析:理解、优势与Linux系统应用

文章目录 前言&#xff1a;1. 线程概念1.1. 什么是线程1.2. 线程得优点&#xff1a;1.3. 线程的缺点线程异常线程的用途 2. 线程的理解&#xff08;Linux 系统为例&#xff09;2.1. 为什么要设计Linux“线程"&#xff1f;2.2. 什么是进程&#xff1f;2.3. 关于调度的问题2…