C++初阶:string类的模拟实现

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

 前言:

前面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。这里我们也会介绍一些常见接口的模拟实现。

string类各函数接口总览: 

#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;namespace fu
{class string{//短小又频繁调用的函数可以直接定义在类内,默认是inlinetypedef char* iterator;typedef const char* const_iterator;//默认成员函数string(const char* str = "");//构造函数string(string& s);//拷贝构造string& operator=(const string& s);//赋值运算符重载~string();//析构函数//迭代器相关的函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//容量和大小相关的函数size_t size();size_t capacity();void reserve(size_t n);void resize(size_t n, char ch = '\0');bool empty();//修改字符串相关的函数void push_back(char ch);void append(const char* str);  string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, char ch);string& insert(size_t pos, const char* str);string substr(size_t pos = 0, size_t len = npos);string& erase(size_t pos, size_t len);void clear();void swap(string& s);const char* c_str()const;//为啥没有参数?//访问字符串相关函数char& operator[](size_t i);const char& operator[](size_t i)const;size_t find(char ch, size_t pos = 0) const;size_t find(const char* str, size_t pos = 0) 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;bool operator==(const string& s)const;bool operator!=(const string& s)const;private:char* _str;  //储存字符串size_t size;//记录字符串当前有效长度的大小size_t capacity;//记录字符串当前的容量static const size_t npos;};//<<和>>运算符重载istream& operator >> (istream& in, string& s);ostream& operator << (ostream & out, const string& s);istream& getline(istream& in, string& s);
}

注意:这里我们把string类放在一个命名空间域里,防止与标准库库里的string类产生命名冲突。

默认成员函数

构造函数

给缺省值的时候,不能给成nulllptr,因为strlen(str)这里会让程序崩溃崩溃。可以给成\0,但是没有必要,这样字符串就会有两个\0了,常量字符串默认会带有\0,给成空字符串是最合适的。

string(const char* str="")//构造函数
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);
}

除了上面这种写法,我们也可以分开写,这里也有需要注意的点。

	//默认成员函数string():_str(nullptr), _size(0), _capacity(0){}string(const char* str){_size = strlen(str);//初始化时,长度为有效字符的长度_capacity = _size;//初始化时,容量为有效字符的长度_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);}

 上面的程序是存在错误的,我们在初始化_str时不能给成nullptr,因为_str是char*,我们要输出_str时存在对空指针解引用,会使得程序崩溃。

#include"string.h"namespace fu
{void test_string1(){string s1;string s2("hello world");cout << s1.c_str() << endl;cout << s2.c_str() << endl;}
}

s1会调用无参的构造函数,被初始化为nullptr (因为还没有模拟实现流插入和流提取,先用C字符串输出也没有太大区别),在输出的时候,就会对空指针解引用。

解决法案:

string():_str(new char[1]{'\0'}), _size(0), _capacity(0)
{}string(const char* str)
{_size = strlen(str);//初始化时,长度为有效字符的长度_capacity = _size;//初始化时,容量为有效字符的长度_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);
}

拷贝构造函数

在模拟实现拷贝构造之前,我们要先了解深拷贝和浅拷贝的区别。

浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。


深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。

 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

下面我们给出两种深拷贝的写法。

传统写法:

开辟一个能足够容纳源字符串大小的空间,将源对象指向的字符串拷贝过去,并将其容量和大小信息进行更新。

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

现代写法:

先根据源字符串的C字符串格式调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。

//现代写法
string(string& s)//拷贝构造
{string tmp(s._str);//中间变量确保原始对象的状态不会改变,也保证了自赋值的正确性swap(tmp);
} 

赋值运算符重载函数

与拷贝构造类似,在模拟实现的过程中涉及资源的管理,我们也要采用深拷贝。

下面提供两种深拷贝的写法。

传统写法:

不是自己给自己赋值的情况下,除了要先释放原来的旧空间,其他的与拷贝构造的模拟实现并无不同。

string& operator=(const string& s)//赋值运算符重载
{if (this != &s){delete[]_str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;//支持连续赋值
}

 现代写法:

这里的写法与拷贝构造的现实写法十分类似,不同的是,拷贝构造是先通过调用构造函数实例化出一个对象,然后用该对象与拷贝的对象进行交换;而赋值运算符重载则是采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。

	//现代写法string& operator= ( string& s){swap(s);return *this;//支持连续赋值}

析构函数

由于string对象的成员变量_str指向一块空间,我们需要手动写析构函数,避免内存泄漏。

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

迭代器相关的函数

在实现string类是,我们用到的迭代器就是字符指针,只是给字符指针起了一个新的名字。要注意的是并不是所有的迭代器本质上都是指针类型。

typedef char* iterator;
typedef const char* const_iterator;

begin

begin函数的作用就是返回字符串中第一个字符的地址:

iterator begin()
{return _str;
}​
const_iterator begin() const
{return _str;
}

end

end函数的作用就是返回字符串最后一个字符的地址,即‘\0’的地址。

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

容量和大小相关的函数

size和capacity

由于_size和_capacity都是成员变量,受访问限定符的限制,在类外不能访问,size和capacity函数就是为了在类外获取成员变量而设置的。

size_t size()
{return _size;
}
size_t capacity()
{return _capacity;
}

 reserve和resize

reserve和resize的区别一定要区分。

reserve:

当n>capacity的时候,要扩容

当n<capacity的时候,不会缩容,capacity不会缩为n

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

 resize:

当n>capacity的时候,会扩容多出来的部分用字符ch填充,如果ch 为给,默认是\0。

当n<capacity的时候,也不会缩容,但是字符串的有效长度会减到n。

void string::resize(size_t n, char ch )
{if (n < _capacity){_size = n;_str[_size] = '\0';}else{if (n > _capacity){reserve(n);//扩容}for (size_t i = _size; i < n; i++){_str[i] = ch;//填充数据}_size = n;//更新_size_str[n] = '\0';}
}

empty

字符串的有效长度为0时就为空了,不需要容量capacity也为0。

bool empty()
{return _size  == 0;
}

修改字符串的相关函数

push_back

这个实现逻辑就是尾插一个字符,尾插前要先判断空间是否足够,不够进行扩容,十分重要的一点的是不要忘记更新size,并且要手动添加\0。

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

append

这里的实现逻辑就是尾插一个字符串,插入的第一步还是要先判断空间大小,不够就扩容,之后将要插入对的字符串插入到源对象的字符串末尾即可,这里因为要插入的字符串带有\0,就不需要我们手动添加\0了。

void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){//小于2倍按2倍扩,大于2倍,按需扩reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}strcpy(_str + len, str);_size += len;
}

 operator+=

这里直接复用上面已经实现好的功能即可。

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

 insert

在指定位置插入一个字符或者字符串。第一步判断pos位置的合法性和空间是否够用,第二步挪动数据,第三步,插入数据,第四步更新size。

string& string::insert(size_t pos, char ch)
{assert(pos <= _size);//等于的时候有意义,尾插if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;//从\0开始挪while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;
}

 在指定位置插入一个字符串。首先也是判断pos的合法性,再判断是否需要扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。

string& string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){//小于2倍按2倍扩,大于2倍,按需扩reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}size_t end = _size + len;while (end > pos+len-1){_str[end] = _str[end - 1];end--;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}

erase 

erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,删除字符串有两种情况。

第一种是pos位置及其之后的有效字符都需要被删除。这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。

第二种是pos位置及其之后的有效字符只需删除一部分。这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾有’\0’了。

string& string::erase(size_t pos, size_t len)
{assert(pos < _size);if (len > _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i < _size; i++){_str[i - len] = _str[i];}_size -= len;}return *this;
}

clear 

将源对象的字符串的第一个字符改为\0,就能达到清空字符串的效果了,不要忘了更新size哦。

void string:: clear()
{_str[0] = '\0';_size = 0;
}

swap 

调用库里面的交换函数即可。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);
}

c_str 

返回一个C格式的字符串。

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

访问字符串相关函数

operator[]

通过下标获取字符串对应位置的字符,也可以对其进行修改操作,当不需要修改时,我们就走下面那个,这里也要注意下标的合法性。

char& string:: operator[](size_t i)//可读可写
{assert(i < _size);return _str[i];
}const  char& string:: operator[](size_t i)const//只读
{assert(i < _size);return _str[i];
}

find

用于在字符串中查找一个字符或者字符串。find是从指定位置开始往后找,找到了返回对应位置的下标,找不到就返回npos。

size_t  string::find(char ch, size_t pos = 0) const
{assert(pos < _size);for (size_t i = 0; i < _size; i++){if (_str[i] == ch)return i;}return npos;
}
size_t  string::find(const char* str, size_t pos = 0) const
{assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr)return npos;elsereturn ptr - _str;
}

 关系运算符重载

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

<<和>>运算符重载

>>

重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。

istream& operator >> (istream& in, string& s)
{s.clear();//清空字符串char ch = in.get();//读取一个字符while (ch != ' ' || ch != '\n')//遇到空格或者换行结束{s += ch;//尾插ch = in.get();//继续读}return in;//支持连续读取
}

<<

重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可。

ostream& operator << (ostream& out, const string& s)
{for (auto ch : s)//范围for{out << ch;}return out;//支持连续输出
}

 getline 

getline与scanf,getchar等不同的是,getline能够读取空格,遇到换行符才会停止。与>>的实现逻辑相同。

istream& getline (istream& is, string& str, char delim);在string类的里的这个delim也可以指定字符,当遇到该指定的字符时停止。
istream& getline(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //遇到换行结束{s += ch; //尾插ch = in.get(); //继续读取字符}return in;}

总结:

除了string类模拟实现比较重要的默认成员函数,我们也实现了很多常见的接口,感兴趣的话也可以自己对着string类的将剩下的接口模拟实现一下

感谢各位的观看,创作不易,还请一键三连哦 ~

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

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

相关文章

[数据集][目标检测]井盖丢失未盖破损检测数据集VOC+YOLO格式2890张5类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2890 标注数量(xml文件个数)&#xff1a;2890 标注数量(txt文件个数)&#xff1a;2890 标注…

QGIS 如何连接空间库,并实时编辑空间表?编辑后库表如何刷新,保证是最新数据?

文章目录 一、什么是 qgis&#xff1f;二、qgis 如何连接数据库三、实时编辑空间表四、编辑后库表如何刷新&#xff0c;保证是最新数据&#xff1f;五、总结 一、什么是 qgis&#xff1f; QGIS&#xff08;原称Quantum GIS&#xff09;是一个用户界面友好的开源桌面端软件&…

htop、free -h对于可用内存显示不同的区别

htop中Mem包含了缓存和缓存区&#xff0c; free -h查看 used free buff/cache 上面htop显示的mem&#xff0c; 1、我看我还能用多少内存&#xff0c;看哪里 看free -h 中的free 2、buff/cache 是啥 缓存缓存区占用&#xff0c;htop显示的效果是把这个也算在一块了&#…

TIDB的整体架构和主要功能

1. 基础架构 PD &#xff1a;负责集群管理和调度。TiDB Server &#xff1a;负责 SQL 查询处理。TiKV/TiFlash&#xff1a;负责数据存储和事务处理。 1.1 PD (Placement Driver) Server 1.1.1 基础介绍 整个 TiDB 集群的元信息管理模块&#xff0c;负责存储每个 TiKV 节点实时…

哪款骨传导耳机适合运动?健身党无广安利五款有用的骨传导耳机!

作为一名耳机爱好者&#xff0c;我的耳机收藏可以说是丰富多样&#xff0c;从追求极致音质的头戴式&#xff0c;到便于携带的入耳式&#xff0c;再到近年来兴起的骨传导耳机&#xff0c;我都有所体验。在众多选择中&#xff0c;我最终偏爱上了骨传导耳机&#xff0c;它以其独特…

vue3 使用 codemirror 实现yaml文件的在线编辑

vue3 使用 codemirror 实现yaml文件的在线编辑 1. 使用情形2. 插件下载3. 封装yaml编辑器组件4. 父组件使用5. js-yaml 使用6. 备注 1. 使用情形 需要对yaml文件进行在线编辑&#xff0c;并且进行基础格式验证 2. 插件下载 vue-codemirror 在线代码编辑器插件 js-yaml 用于转…

容联云容犀Copilot&Agent入选《中国 AI Agent 产品罗盘》

近日&#xff0c;InfoQ研究中心推出《中国AI Agent应用研究报告》&#xff0c;并在报告中对现行的中国AI Agent产品进行梳理总结&#xff0c;并形成《中国AI Agent产品罗盘》。 作为“营销服”领域垂直类Agent&#xff0c;容联云容犀Copilot&#xff06;Agent入选2024中国AI A…

java8+springboot2.3升级jdk17+springboot2.7.9踩坑

一.问题: java.lang.ExceptionInInitializerError: nullat java.base/java.lang.Class.forName0(Native Method)at java.base/java.lang.Class.forName(Class.java:375)。。。。。。内部保密。。。。at org.springframework.context.annotation.ParserStrategyUtils.invokeAwa…

封装智能指针 qt实现登录界面

1.封装独占智能指针——unique_ptr #include <iostream> #include <utility> // For std::move// 命名空间 namespace custom_memory { template <typename T> class myPtr { public:// 使用初始化列表进行初始化explicit myPtr(T* p nullptr) noexcept : …

ThinkPHP8出租屋管理系统

有需要请加文章底部Q哦 可远程调试 ThinkPHP8出租屋管理系统 一 介绍 此出租屋管理系统基于ThinkPHP8框架开发&#xff0c;数据库mysql&#xff0c;前端Vue3&#xff0c;前后端不分离&#xff0c;系统主要角色为管理员。房租计算器&#xff0c;房东记账收租管理&#xff0c;房…

NX二次开发—柱面中心线工具

设计一个柱面中心线工具,可以实现选择对象,画出圆柱的中心线,可以更改中心的线的颜色、线型、线宽和图层,是否延长,是否关联。 先在NX上进行界面设计 添加选择对象,并设置标题,选择设置为多选 添加组,在组里添加线条颜色/线型/线宽,设置颜色ColorValue和线型Value 这…

C++详解string(全面解析)

目录 string的概念&#xff1a; string的框架&#xff1a; 1、成员函数 2、迭代器&#xff08;Iterators&#xff09;​编辑 3、容量 4、元素访问 5、修改 6、非成员函数重载 string的构造和拷贝构造&#xff1a; string的析构&#xff1a; string的访问&#xff1a;…

单片机,传感器等低功耗管理

**有些客户需求&#xff0c;把设备做成低功耗管理&#xff0c;这样就可以节省电池的电量&#xff0c;也可以增加传感器的使用寿命 HCLK为CPU提供时钟&#xff0c;内核执行代码。当CPU不需要继续运行时&#xff0c;可以利用多种低功耗模式&#xff0c;等待某个事件触发 ① 睡眠…

单链表的实现(C语言)

目录 1.单链表 1.1 实现单链表 1.1.1 文件创建 1.1.2 链表功能了解 1.1.3 链表的结点 1.1.4 链表的函数声明 1.1.5 链表功能的实现 链表是一种链式结构&#xff0c;物理结构不连续&#xff0c;逻辑结构是连续的&#xff0c;在计算机中链表的实际存储是按照一个结点内存放…

pod install 报错处理

由于墙的原因&#xff0c;pod install 、 pod update经常报错 有效的解决方案(推荐)&#xff1a; 以SnapKit为例 找不报错的同事要以下两个文件&#xff08;指定的版本&#xff09; 1. /Users/xxx/Library/Caches/CocoaPods/Pods/Release/SnapKit 2. /Users/xxx/Library/Cac…

95. UE5 GAS RPG 实现创建多段飞弹攻击敌人

从这篇开始&#xff0c;我们将实现一些技能&#xff0c;比如多段火球术&#xff0c;闪电链等等。 在这一篇里&#xff0c;我们先实现多段火球术&#xff0c;技能可以通过配置发射出多个火球术进行攻击。 创建多段火球函数 首先在我们之前创建的RPGFireBolt.h类里面增加一个生…

(11)(2.1.1) PWM、OneShot和OneShot125 ESC(一)

文章目录 前言 1 PWM 2 OneShot 3 参数说明 前言 大多数 ArduPilot 飞行器使用由无刷电机 ESC 控制的无刷电机。这些 ESC 使用的最常见协议是PWM、OneShot、OneShot125 和 DShot。本页介绍前三种&#xff08;PWM、OneShot 和OneShot125&#xff09;。 &#xff01;Warning…

从C语言过渡到C++

&#x1f4d4;个人主页&#x1f4da;&#xff1a;秋邱-CSDN博客☀️专属专栏✨&#xff1a;C &#x1f3c5;往期回顾&#x1f3c6;&#xff1a;单链表实现&#xff1a;从理论到代码-CSDN博客&#x1f31f;其他专栏&#x1f31f;&#xff1a;C语言_秋邱的博客-CSDN博客 目录 ​…

数学建模笔记—— 模糊综合评价

数学建模笔记—— 模糊综合评价 模糊综合评价1. 模糊数学概述2. 经典集合和模糊集合的基本概念2.1 经典集合2.2 模糊集合和隶属函数1. 基本概念2.模糊集合的表示方法3. 模糊集合的分类4. 隶属函数的确定方法 3. 评价问题概述4. 一级模糊综合评价模型典型例题 5. 多层次模糊综合…

【鸿蒙开发工具报错】Build task failed. Open the Run window to view details.

Build task failed. Open the Run window to view details. 问题描述 在使用deveco-studio 开发工具进行HarmonyOS第一个应用构建开发时&#xff0c;通过Previewer预览页面时报错&#xff0c;报错信息为&#xff1a;Build task failed. Open the Run window to view details.…