【C++】理解string类的核心理念(实现一个自己的string类)

目录

一、引言

二、自我实现

1.成员变量的读写

2.构造与析构

3.迭代器

4.插入字符或字符串

尾插

中间插入

5.删除字符或子字符串

6.查找字符或子串

7.获取子串

三、完整代码

四、补充


一、引言

        实现自己的 string 类是学习 C++ 语言和面向对象编程的一个好方法。通过编写一个简单的字符串类,可以深入理解类的概念、内存管理、构造函数、析构函数等核心理念。理解了string类的底层逻辑之后会发现,一些在上层看似复杂的操作在底层其实很简单。下面就让我们来实现一个自己的string类吧!

二、自我实现

1.成员变量的读写

        string是一个字符串类,所以我们在定义成员变量的时候需要一个char类型的指针,指向存放字符串的空间,为了方便实现对字符串的操作以及内存的管理,还需要定义两个整形变量,一个表示字符串长度,一个表示当前空间大小。

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

这里定义成私有成员变量所以还需要使变量可读:

        const char* c_str() const //const关键字进行函数重载,表示const对象也可以调用,不加则不行{return _str;}size_t size() const{return _size;}

这里的 const是一个关键字,作用是对函数进行重载,使其具有普通成员函数以及常量成员函数的双重身份,如果没有常量成员函数,那么常量对象就无法调用不带 const 修饰的成员函数,这可能导致在使用常量对象时的一些限制和不便。

我们需要重载[],完成对指定位置的字符的读或写操作:

		char& operator[](size_t pos) //引用返回:返回值出了作用域任然存在,可读可写{assert(pos < _size);return _str[pos];}const char& operator[](size_t pos) const //const对象调用这个,只读{assert(pos < _size);return _str[pos];}

这里提供两个版本,一个是普通成员函数,一个是常量成员函数,之所以要分开写是因为他们的返回值类型不同,一个是可读可写,一个是只读不可写。

2.构造与析构

  • 默认构造函数
		string(const char* str = "") //全缺省,常量字符串末尾默认'\0':_size(strlen(str)),_capacity(_size),_str(new char[_capacity + 1]){strcpy(_str, str);}

其作用是创建一个 string 类对象,该对象的内部包含一个动态分配的字符数组 _str,存储了传入的 C 字符串的内容,并且记录了字符串的长度 _size 和容量 _capacity。我们调用无参构造函数时 _str内部默认存在有字符 '\0'。'\0 '标记了字符串的末尾。

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

这是一个字符串类的拷贝构造函数的实现。拷贝构造函数用于创建一个新的对象,并以另一个同类型对象的内容为模板进行初始化。具体来说,对于字符串类而言,这段代码的作用是创建一个新的字符串对象,并将其内容初始化为另一个字符串对象 s 的内容的副本。

这里的构造函数都是以深拷贝的方式实现,新对象拥有一个新的内存块,该内存块包含源对象或源字符串的副本。

  • 析构函数
		~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}

析构函数无需多言,需要注意的是 delete后面一定要加 [],表示释放的是一个字符串的空间。

3.迭代器

string类提供了迭代器(iterator)来遍历字符串的元素,迭代器是一种抽象的、通用的数据访问方式,它可以被用于遍历不同类型的数据结构。在string中,迭代器通常是一个指向字符的指针或类似指针的对象,这里我们模拟实现的是指针类型的迭代器:

//迭代器typedef char* iterator;typedef const char* const_iterator;

同样也有普通指针和常量指针两个版本

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

begin() 和 end()分别返回指向字符串首元素以及尾元素的后一位,由于返回值的不同,普通成员函数与常量成员函数要分开写。

其实实现了迭代器也就实现了基于范围的for循环,不信可以看看以下代码:

#include<iostream>
using namespace std;#include"string.h"void text_iterator()
{bit::string a("Hello world!");bit::string::iterator it = a.begin();while (it != a.end()){cout << *it;it++;}cout << endl;for (auto ch : a){cout << ch;}cout << endl;
}int main()
{text_iterator();return 0;
}

此时输出结果是:

看到了吗,两个循环的结果是一样的,我们没有做任何操作,就实现了基于范围的for循环诶,其实,实现了迭代器之后,第二个循环体与前一个循环体对编译器来看是一样的,这是给编译器设计好的,不需要我们进行多余的操作。

4.插入字符或字符串

在进行插入操作时,我们要先判断字符串的空间大小,如果插入的字符/字符串的长度大于所剩余的空间,就需要进行扩容,在string中,reserve成员函数实现上述功能:

//扩容void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;//可以直接指针复制,令两者指向同一块空间_capacity = n;}}

如果需要扩容,我们的做法是开辟一块新的空间,存放原字符串的副本,并且对原字符串进行空间释放,再进行指针复制,令_str指向新开辟的那块空间。

尾插

接下来就可以进行尾部插入字符或字符串的操作了:

		//插入字符void push_back(char ch){if (_size == _capacity){//2倍扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';}

插入字符时,首先判断空间大小,空间不足则进行2倍扩容,要注意的是原字符串为空的情况,此时就不是2倍扩容了,而是给定一个初始大小的空间。插入一个字符不仅要对插入位置进行赋值,还要将它的下一位置赋值为'\0'。

//插入字符串void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){//至少扩容到_size+lenreserve(_size + len);}strcpy(_str + _size, str);_size += len;}

插入字符串的操作和插入字符类似。进行扩容操作之后用strcpy函数将要插入的字符串赋值到原字符串的末尾处。

我们在使用string类的时候经常会用到其重载后的+=操作,其作用是直接在str后面插入字符或字符串,很方便,其实实现起来也很简单,就是用到上述的插入函数:

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

中间插入

string类中,insert函数用于在字符串的指定位置插入字符或字符串。

		void insert(size_t pos, size_t n, char ch){assert(pos <= _size);if (_size + n < _capacity){//至少扩容到_size+nreserve(_size + n);}//挪动数据size_t end = _size;while (end >= pos && end != -1) //若pos为0呢?end!=-1{_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){//至少扩容到_size+nreserve(_size + len);}//挪动数据size_t end = _size;while (end >= pos && end != -1) //若pos为0呢?end!=-1{_str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}

同样要先判断空间大小,进行扩容操作。然后要进行数据的挪动,挪动的范围是pos到end位置,挪动的距离是n。这里要注意一个特殊情况,就是当pos为0时,也就是要将字符串整体向后移动时,标记当前挪动字符位置的变量end在对首字符挪动完之后,其值会自减为-1,但是end是一个无符号整形,因此此时的-1会被解释为该无符号整数的最大可能值,所以还有加上一个判断条件:end != -1。

5.删除字符或子字符串

string 类中,erase 函数用于从字符串中删除字符或子字符串。

		void erase(size_t pos, size_t len = -1){assert(pos < _size);if (len == -1 || pos + len > _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}}

pos表示删除的起始位置,len表示删除的字符串的长度,len设置成缺省参数,默认为最大值,即pos位置后面的字符全删,当pos+len大于字符串长度时也是全删。全删很简单,只要将pos位置赋值为'\0'就可以了。此外就是删除内部的子串了,定义一个变量end用于标记要删除的子串的末尾,将end后面的字符依次覆盖到pos后面的字符处,即可完成删除操作。

6.查找字符或子串

string 类中的 find 函数用于在字符串中搜索子字符串或字符,并返回第一次出现的位置:

        //找一个字符size_t find(char ch, size_t pos = 0){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return -1;}//找一个字符串size_t find(const char* str, size_t pos = 0){const char* ptr = strstr(_str,str);if (ptr){return ptr - _str;}else{return -1;}}

查找操作很容易实现,只需要对字符串进行遍历,需要说明的是查找字符串操作时用到的 strstr 函数:C 标准库函数 strstr 在字符串 _str 中查找第一次出现的子字符串 strstrstr 返回一个指向匹配子字符串的指针,如果未找到匹配项,则返回 nullptr。

7.获取子串

string 类中,substr 函数用于提取字符串的子串:

		//取子串string substr(size_t pos = 0, size_t len = -1){assert(pos <= _size);size_t n = len;if (n == -1 || pos + n > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}

pos表示子串的首元素位置,len表示子串长度,len同样设置成缺省参数,缺省值为最大值,即取的是pos后面的全部字符组成的子串。由于返回值类型是string类,所以我们需要声明一个tmp对象,用于存放子串的副本,用重载后的+=操作符即可实现子串的复制。

三、完整代码

#pragma once
#include<iostream>
#include<cassert>
using namespace std;
namespace Mystd
{class string{public:string(const char* str = "") //全缺省,常量字符串末尾默认'\0':_size(strlen(str)),_capacity(_size),_str(new char[_capacity + 1]){strcpy(_str, str);}string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const //const关键字进行函数重载,表示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对象调用这个,只读{assert(pos < _size);return _str[pos];}//迭代器typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;} //实现了迭代器也就实现了范围forconst_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;} //实现了迭代器也就实现了范围for//扩容void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;//可以直接指针复制,令两者指向同一块空间_capacity = n;}}//插入字符void push_back(char ch){if (_size == _capacity){//2倍扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';}//插入字符串void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){//至少扩容到_size+lenreserve(_size + len);}strcpy(_str + _size, str);_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){//至少扩容到_size+nreserve(_size + n);}//挪动数据size_t end = _size;while (end >= pos && end != -1) //若pos为0呢?end!=-1{_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){//至少扩容到_size+nreserve(_size + len);}//挪动数据size_t end = _size;while (end >= pos && end != -1) //若pos为0呢?end!=-1{_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 = -1){assert(pos < _size);if (len == -1 || pos + len > _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}}//找一个字符size_t find(char ch, size_t pos = 0){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return -1;}//找一个字符串size_t find(const char* str, size_t pos = 0){const char* ptr = strstr(_str,str);if (ptr){return ptr - _str;}else{return -1;}}//取子串string substr(size_t pos = 0, size_t len = -1){assert(pos <= _size);size_t n = len;if (n == -1 || pos + n > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}private:char* _str;size_t _size;size_t _capacity;static size_t npos;};size_t Mystd::string::npos = -1;
}

四、补充

        前面说到过:在C++中,对于无符号整数类型,-1 不是一个负数,而是一个非常大的正整数。这是由于无符号整数类型不能表示负数,因此用有符号整数的-1表示无符号整数时,会被解释为该无符号整数的最大可能值。因此我在处理一些返回值情况时,例如查找操作时,没找到指定字符则返回-1这可能导致问题,因为 size_t 是一个无符号整数类型,而 -1 是有符号整数。在 C++ 中,无符号整数和有符号整数之间的比较可能导致一些不直观的行为。

        所以最好用std::string::npos来表示-1(最大可能值)的情况。npos需设置成静态成员变量:

namespace Mystd
{class string{public://...private:char* _str;size_t _size;size_t _capacity;static size_t npos;};size_t Mystd::string::npos = -1;
}

写文不易,望多多支持~~

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Python必做100题】之第二十六题(小球反弹问题)

题目&#xff1a;一小球从100米高度自由落体落下&#xff0c;每次落地后反跳回原来高度的一半再落下&#xff0c;求它在第10次落地时&#xff0c;共经过多少米&#xff1f;第10次反弹多高&#xff1f; 思路&#xff1a;初始为100米&#xff0c;落下反弹为原来的一半&#xff1…

数据结构和算法-二叉排序树(定义 查找 插入 删除 时间复杂度)

文章目录 二叉排序树总览二叉排序树的定义二叉排序树的查找二叉排序树的插入二叉排序树的构造二叉排序树的删除删除的是叶子节点删除的是只有左子树或者只有右子树的节点删除的是有左子树和右子树的节点 查找效率分析查找成功查找失败 小结 二叉排序树 总览 二叉排序树的定义 …

模型推理加速系列 | 08:TensorRT-LLM助力LLM高性能推理

引言 ​ 日暮苍山远&#xff0c;天寒白屋贫。Created by DALLE 3 小伙伴们好&#xff0c;我是《小窗幽记机器学习》的小编&#xff1a;卖汤圆的小女孩&#xff0c;今天是冬至&#xff0c;祝福小伙伴们幸福安康吧。紧接前文&#xff1a; 万字长文细说ChatGPT的前世今生 Llam…

一款基于.NET Core的快速开发框架、支持多种前端UI、内置代码生成器

前言 经常看到有小伙伴在技术群里问有没有什么好用且快速的开发框架推荐的&#xff0c;今天就给大家分享一款基于MIT License协议开源、免费的.NET Core快速开发框架、支持多种前端UI、内置代码生成器、一款高效开发的利器&#xff1a;WalkingTec.Mvvm框架&#xff08;简称WTM…

论文推荐:大型语言模型能自我解释吗?

这篇论文的研究主要贡献是对LLM生成解释的优缺点进行了调查。详细介绍了两种方法&#xff0c;一种是做出预测&#xff0c;然后解释它&#xff0c;另一种是产生解释&#xff0c;然后用它来做出预测。 最近的研究发现&#xff0c;即使LLM是在特定数据上训练的&#xff0c;也不能认…

基于Spring自动注入快速实现策略模式+工厂模式优化过多的if..else

一、策略模式 1.1策略模式定义 在策略模式&#xff08;Strategy Pattern&#xff09;中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。 在策略模式定义了一系列算法或策略&#xff0c;并将每个算法封装在独立的类中&#xff0c;使得它们可以互相…

echarts:设置折线图线条和端点的颜色

1、代码 <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>Echarts折线图</title> </head> <body> <div id"main" style"width: 600px;height:400px;"></div> <sc…

爬虫概念简述

爬虫简述 ⼀、什么是爬虫&#xff1f;二、爬虫有什么用?三、爬虫的分类四、所谓的“爬虫学的好&#xff0c;牢饭吃到饱 !”五、爬虫的大致流程 ⼀、什么是爬虫&#xff1f; ​ 简言之&#xff0c;爬虫可以帮助我们把网站上的信息快速提取并保存下来。 ​ 我们可以把互联网比…

2016年第五届数学建模国际赛小美赛C题对超级细菌的战争解题全过程文档及程序

2016年第五届数学建模国际赛小美赛 C题 对超级细菌的战争 原题再现&#xff1a; 最近有很多关于我们抗生素耐药性危机的讨论。进化出的能够抵抗抗生素的细菌每年杀死70万人&#xff0c;越来越强大的细菌正在世界各地传播。研究人员担心&#xff0c;我们将进入一个后抗生素时代…

U-Net: Convolutional Networks for Biomedical Image Segmentation(CVPR2015)

文章目录 AbstractIntroductionNetwork ArchitectureConclusiontorch code hh 源代码 Abstract 人们普遍认为&#xff0c;深度网络的成功训练需要成千上万个带注释的训练样本。在这篇论文中&#xff0c;我们提出了一个网络和训练策略&#xff0c;该策略依赖于数据增强的强大使…

红队打靶练习:DIGITALWORLD.LOCAL: MERCY V2

目录 信息收集 1、arp 2、netdiscover 3、nmap 4、nikto 5、whatweb 6、总结 目录探测 1、gobuster 2、dirsearch WEB enum4linux枚举工具 smbclient工具 knock工具 CMS 文件包含漏洞 Tomcat 提权 系统信息收集 本地提权 get root 信息收集 1、arp ┌──…

redis 从0到1完整学习 (六):Hash 表数据结构

文章目录 1. 引言2. redis 源码下载3. dict 数据结构4. 哈希表扩容与 rehash5. 参考 1. 引言 前情提要&#xff1a; 《redis 从0到1完整学习 &#xff08;一&#xff09;&#xff1a;安装&初识 redis》 《redis 从0到1完整学习 &#xff08;二&#xff09;&#xff1a;red…