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.mzph.cn/news/879219.shtml

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

相关文章

7系列FPGA HR/HP I/O区别

HR High Range I/O with support for I/O voltage from 1.2V to 3.3V. HP High Performance I/O with support for I/O voltage from 1.2V to 1.8V. UG865&#xff1a;Zynq-7000 All Programmable SoC Packaging and Pinout

企业四要素核验是什么?如何应用

企业四要素核验是指对企业名称、社会统一信用代码&#xff08;或注册号&#xff09;、法人名称以及法人身份证号码等四项关键信息进行核验的过程。这一核验步骤是确保企业信息真实性和合法性的重要手段&#xff0c;广泛应用于各个行业领域&#xff0c;以防范企业身份被冒用、欺…

Android 车联网——CarProperty使用实例(二十三)

在熟悉了 Car 下的相关 Manager 和 Service 后,这里我们通过需求来实现 CarProperty 的使用实例。 一、使用实例 1、需求分析 假如有这样一个需求,需要封装一个 SDK 为 APP 提供车辆信息、空调状态以及驾驶信息等相关属性的值及变化情况。这里我们首先需要确认各种属性对应…

JVM锁的优化与逃逸分析

锁消除 是指JVM即时编译器在运行时&#xff0c;对一些代码上要求同步&#xff0c;但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。JIT&#xff08;Just-In-Time&#xff0c;即时编译&#xff09;&#xff1a;是一种在程序运…

【STM32 Blue Pill编程】-定时器计数模式

定时器计数模式 文章目录 定时器计数模式1、定时器计数模式介绍2、硬件准备及接线3、模块配置3.1 定时器计数模式配置3.2 定时器中断配置3.3 串口配置4、代码实现在本文中,我们将讨论如何在计数器模式下配置 STM32 Blue Pill 定时器模块。 要将定时器用作计数器,我们将其配置…

【mechine learning-六-supervise learning之线性回归模型】

监督学习之线性回归模型 线性回归模型线性模型回归模型 如何使用线性模型实现智能化预测呢寻找数据训练模型输入、特征、目标、预测值、模型代价函数 线性模型是人工智能监督学习中最广泛的应用&#xff0c;所以有必要先学习一下这个基础模型&#xff0c;做好基石。 线性回归模…

在 CentOS 中永久关闭防火墙的步骤

在 CentOS 中永久关闭防火墙的步骤 在 CentOS 系统中&#xff0c;防火墙通常由 firewalld 服务管理。如果你希望在系统中永久关闭防火墙&#xff0c;可以按照以下步骤操作&#xff1a; 1. 停止防火墙服务 首先&#xff0c;你需要停止当前正在运行的防火墙服务。可以使用以下…

猎板PCB大讲堂:IPHONE16的线路板的升级猜测

iPhone 16 系列与 iPhone 15 系列在 PCB (印刷电路板) 设计上的主要差异可能体现在材料和技术上。根据 TrendForce 的分析&#xff0c;iPhone 16 预计将采用树脂涂覆铜箔&#xff08;RCC&#xff09;作为新的印刷电路板&#xff08;PCB&#xff09;材料&#xff0c;这一改变将使…

使用 ShuffleNet 模型在 CIFAR-100 数据集上的图像分类

简介 在深度学习领域&#xff0c;图像分类任务是衡量算法性能的重要基准。本文将介绍我们如何使用一种高效的卷积神经网络架构——ShuffleNet&#xff0c;来处理 CIFAR-100 数据集上的图像分类问题。 CIFAR-100 数据集简介 CIFAR-100 数据集是一个广泛使用的图像分类数据集&…

QT的绘画事件和网络通信

画一个时钟 #include "widget.h" #include "ui_widget.h" #include <QPainter> #include <QDebug> #include <QTime> #include <QTimer>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(…

11_跳表(Skip List)

菜鸟: 老鸟&#xff0c;我最近在处理一个数据操作的时候遇到了性能问题。我在一个有序数组中查找元素&#xff0c;发现查找速度有点慢&#xff0c;尤其是数据量大的时候。你有什么好的建议吗&#xff1f; 老鸟: 这是个好问题&#xff0c;有许多数据结构可以优化查找操作。你听…

Python爱心射线(完整代码)

目录 系列目录 写在前面​ 完整代码 下载代码 代码分析 写在后面 系列目录 序号直达链接表白系列1Python制作一个无法拒绝的表白界面2Python满屏飘字表白代码3

人工智能领域各方向顶级会议和期刊

会议 人工智能基础与综合&#xff1a;AAAI、CICAI (!)、UAI、IJCAI 机器学习&#xff1a;COLT、ICLR、ICML、NeurIPS 模式识别与计算机视觉&#xff1a;ECCV、CVPR、ICCV 语言与语音处理&#xff1a;ACL、EMNLP 知识工程与数据挖掘&#xff1a;SIGKDD、SIGMOD、ICDE、SIGIR、V…

web知识

sql注入的万能密码:1’ or true#如果页面没有什么东西可见&#xff0c;首先可以用diresearch看看有没有什么隐藏的目录&#xff0c;或者检查源代码&#xff0c;如果这些都没成功可以用 dirsearch如果没有找到东西&#xff0c;可能需要调低线程 dirsearch.py -u url -e * --ti…

【60天备战软考高级系统架构设计师——第十五天:项目管理——风险管理】

风险管理是项目成功的重要保障&#xff0c;通过有效的风险识别、评估和应对&#xff0c;确保项目能够顺利推进。 学习内容&#xff1a; 风险识别 学习内容&#xff1a;识别项目中的潜在风险&#xff0c;包括技术风险、管理风险、市场风险等。了解常用的风险识别工具和技术&…

语音转文字工具全解析

无论是学生群体记录课堂笔记&#xff0c;职场人士整理会议纪要&#xff0c;还是自媒体创作者捕捉灵感火花&#xff0c;录音转文字软件都以其独特的便利性和高效性赢得了广泛的好评。今天&#xff0c;就让我们一起探索那些深受大家喜爱的录音转文字工具吧。 1.365在线转文字 链…

【Python】由二维列表初始化导致修改元素时会修改相同位置元素的引用问题f = [[0] * len(matrix[0])] * len(matrix)

背景&#xff1a; 在刷Leetcode过程中&#xff0c;需要初始化一个与另一个矩阵&#xff08;如 matrix&#xff09;尺寸相同的二维列表&#xff08;如 f&#xff09;&#xff0c;并填充初始值&#xff08;如 0&#xff09;。一开始用的是这种方法试图创建一个所有元素均为 0 的…

django自用教程

编程软件: pycharm django介绍:django是Pythonweb的一个框架&#xff0c;是用来构建网站的工具。 要想使用django&#xff0c;首先需要下载django模块&#xff0c;通过使用以下代码实现: pip install django 安装完成后&#xff0c;在django的目录下有一个文件django-admin&am…

docker基础知识-docker0网桥

文章目录 示意图Docker 网桥的工作原理Docker 网桥的优势Docker 网桥的局限性自定义网桥网络 Docker 网桥&#xff08;Docker bridge network&#xff09;是 Docker 默认的一种网络模式&#xff0c;它允许 Docker 容器之间通过一个虚拟的交换机进行通信。Docker 网桥网络为容器…

Linux shell编程学习笔记79:cpio命令——文件和目录归档工具(下)

在 Linux shell编程学习笔记78&#xff1a;cpio命令——文件和目录归档工具&#xff08;上&#xff09;-CSDN博客https://blog.csdn.net/Purpleendurer/article/details/142095476?spm1001.2014.3001.5501中&#xff0c;我们研究了 cpio命令 的功能、格式、选项说明 以及 cpi…