【C++ STL】模拟实现 string

标题:【C++ :: STL】手撕 STL _string

@水墨不写bug


(图片来源于网络)


        C++标准模板库(STL)中的string是一个可变长的字符序列,它提供了一系列操作字符串的方法和功能。

        本篇文章,我们将模拟实现STL的string类的部分功能,以增强对STL的熟练度,了解STL容器的工作原理,积累项目经验,也为将来自主实现和改造容器奠定坚实的基础。

        STL的string类是一个模板,而我们为了方便实现,以达到练习的目的,我们暂时先实现一个成员变量为(下图示)的string类。    

char* _str;
size_t _size;//字符串长度,不加上\0
size_t _capacity;

C++ STL的string类提供了以下常用的成员函数和接口:

  1. 构造函数和赋值操作函数接口:

    • 默认构造函数:创建一个空字符串。
    • 带string参数的构造函数:将一个string对象复制到另一个string对象中。
    • 带字符数组参数的构造函数:将字符数组转换为string对象。
    • 带整数参数的构造函数:将整数转换为字符串。
    • 赋值操作符:用另一个string对象、字符数组或字符来赋值。
  2. 访问字符串内容相关函数接口:

    • at():返回指定位置的字符。
    • operator[]:返回指定位置的字符。
    • front():返回第一个字符。
    • back():返回最后一个字符。
    • c_str():返回一个以空字符结尾的字符数组。
  3. 修改字符串内容接口:

    • insert():在指定位置插入字符、字符串或字符数组。
    • erase():删除指定位置的字符。
    • replace():替换指定位置的字符串或字符。
    • append():在字符串末尾添加字符、字符串或字符数组。
    • clear():清空字符串。
  4. 字符串操作接口:

    • size() 或 length():返回字符串的长度。
    • empty():判断字符串是否为空。
    • find():查找指定字符串或字符的位置。
    • substr():返回指定位置和长度的子字符串。
    • compare():比较两个字符串

 (具体用法在上一篇讲解:【Cpp::STL】标准模板库_ string详解) 


(一)头文件

        我们在C语言阶段实现声明和定义分离的时候,只是单一的把函数的定义放在.c(源)文件,把函数的声明,头文件的包含,宏定义等放在.h(头)文件。

        但是,在C++,不仅要遵守以上的规则,由于类的出现,需要域作用限定符(::)来限定方位;由于成员的访问权限的出现,需要考虑访问权限的问题;此外不同类型的成员的定义的位置也有讲究,比如静态成员尽量不要直接定义在头文件中,因为这会引发  多次包含多文件   在链接时的  头文件内的对象的重定义问题。

        本文根据STL标准模板库的功能,给出头文件,包括string类的定义,众多成员函数,部分非成员函数(流插入,流提取的重载),并在后半节详细讲解各个函数的实现思路。

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;namespace ddsm 
{class string {friend ostream& operator<<(ostream& out, const string& s1);public://迭代器typedef char* iterator;typedef const char* const_iterator;iterator begin();const_iterator begin() const;iterator end();const_iterator end() const;//传参构造,默认构造,给默认值为空串,巧妙string(const char* str = "");string(const string& s);//copy constructor//string& operator=(const string& s);传统写法string& operator=(const char* s);string& operator=(string s);//现代写法//析构~string();//C类型字符串const char* c_str() const;//保留void reserve(int n);string& push_back(const char ch);//尾插字符string& append(const char* str);//尾插字符串string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, const char ch);string& insert(size_t pos, const char* str);//缺省值代表最一般的情况string& erase(size_t pos = 0,size_t len = npos);//找一个字符size_t find(const char ch, size_t pos = 0);//找一个子串size_t find(const char* str, size_t pos = 0);void swap(string& s);string substr(size_t pos = 0,size_t len = npos);string& clear();private:char* _str;size_t _size;//字符串长度,不加上\0size_t _capacity;//特例,const静态整形对象可声明定义和一,但是可能造成链接时的错误static size_t npos;};istream& operator>>(istream& in, string& s);};

(二)string类的功能实现

(1)默认成员函数

i,构造函数

        我们知道,构造函数的作用是在对象实例化时初始化对象,对于string类对象,含有三个基本成员变量:

        char* _str;size_t _size;//字符串长度,不加上\0size_t _capacity;

        经过分析,我们得知在构造函数内部,需要申请动态的堆区空间给_str;需要根据_str的长度变化来动态更新_size;同时根据申请的动态空间的长度来更新_capacity。

        于是,我们理所当然的想到这样写构造函数:

string::string(const char* str = "")
// 缺省参数为一个空字符串,如果不传参,空字符串就是一个单独的'\0':_size(strlen(str)),_capacity(strlen(str))
{_str = new char[_size + 1];strcpy(_str, str);
}

        但是,这种简单易懂的写法也暴露出了弊端:多次无意义的重复调用strlen,这会造成额外的消耗。于是,为了减少strlen的调用次数,我们考虑这样修改:

        

string::string(const char* str):_size(strlen(str)),_capacity(_size)
{_str = new char[_size + 1];strcpy(_str, str);
}

        这样修改虽然解决了strlen重复无意义调用的问题,但是也带来了新的问题:

程序稳定性下降的问题:

¥¥我们知道:初始化列表的初始化顺序是成员函数在类中的声明顺序:按照此例:

        char* _str;size_t _size;//字符串长度,不加上\0size_t _capacity;

        先初始化_size,再初始化_capacity;在这种背景下,如果代码有一些微小的改变,或许就会造成意想不到的问题。

        如果改变成员变量的顺序,那么初始化列表就会按照不同的顺序初始化。具体来说,如果_capacity在_size之前,初始化列表就会先初始化_capacity:

        char* _str;size_t _capacity;size_t _size;//字符串长度,不加上\0

        这时_size还没有初始化,是随机值,那么就造成了_capacity为随机值的问题。

解决这个问题其实很简单,将对_capacity的初始化放入函数体:

string::string(const char* str)//strlen较低效,调用一次用size记录返回值//size/capacity不包含\0,但是其需要存储:_size(strlen(str))
{_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);
}

        这样就确定了是先初始化_size,再初始化_capacity。¥¥

        (将声明和定义分离,需要将缺省参数放在声明处,同时函数名之前需要加上域作用限定符,表示这个函数在你实现的string类里面声明过。)

ii,析构函数

         析构函数的作用是:清理资源。

由于比较简单,这里直接给出实现:

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

(函数名之前需要加上域作用限定符,表示这个函数在你实现的string类里面声明过。)

iii,拷贝构造

       拷贝构造,完成创建对象时的初始化。

一般情况下,我们会这样写:

//拷贝构造
string::string(const string& s)
{char* tem = new char[s._capacity+1];//多开一个,存储'\0'strcpy(tem, s._str);delete[] _str;//销毁原空间_str = tem;_size = s._size;_capacity = s._capacity;
}

但是,其实有更简单的写法:

void string::swap(string& s)
{//调用模板swap交换内置类型,损失不大std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);
}
//拷贝构造的现代写法
string::string(const string& s):_str(nullptr)
{string tem(s._str);swap(tem);
}

仔细分析,我们其实在无形之中让构造函数给我们“打工”了:

string tem(s._str);

就是用拷贝对象的字符串来构造一个tem对象,而这个tem对象就是我们需要的,所以我们实现一个swap函数,将*this与tem完全交换,同时tem在出作用域时也会自动析构,同样也达到了拷贝构造的目的。

iv,赋值重载

赋值重载:实现对象之间的赋值。

我们一般会这样实现:

//赋值重载
string& string::operator=(const char* s)
{int len = strlen(s);char* tem = new char[len + 1];strcpy(tem, s);delete[] _str;_str = tem;_size = _capacity = len;return *this;
}

 但是,同样也有更简单的写法:

void string::swap(string& s)
{//调用模板swap交换内置类型,损失不大std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);
}//赋值重载的现代写法 
string& string::operator=(string tem)
{//自动调用拷贝构造swap(tem);//出作用域自动完成析构return *this;
}

在无形之中,我们让拷贝构造为我们“打工”。

我们通过传值传参,拷贝构造一个临时对象tem,这个tem就是我们需要的,所以完全交换*this就得到了构造的对象,同时tem出作用域也会自动析构。

(2)迭代器

         对于迭代器,本质上是一个指针,也可以是一个类(对指针的封装),在这里,我们不妨用指针来作为迭代器:

//声明:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
    //定义string::iterator string::begin(){return _str;}string::const_iterator string::begin() const{return _str;}string::iterator string::end(){return _str + _size;}string::const_iterator string::end() const{return _str + _size;}

        const迭代器用于const对象调用;普通迭代器用于普通迭代器调用。

普通迭代器可读可写,const迭代器只可读不可写。

(3)容量和长度

 i.reserve()

        改变string的容量,若要求值n大于现在的容量,则容量扩大到n;若要求值小于等于现有容量,则改变容量。

        reserve对于size没有影响,不会改变string的内容。

实现如下:

//保留指定容量,容量只增不减
void string::reserve(int n)
{//要求保留的大于现有容量,需要扩容if (n > _capacity){char* tem = new char[n + 1];// 申请新空间完毕,转移数据strcpy(tem, _str);delete[] _str;_str = tem;_capacity = n;//reserve不改变size}
}

ii,resize()

    //resize()不改变capacity,可能改变sizevoid string::resize(int size,int ch)//size为设定值,_size为现有值{if (size < _size){_size = size;_str[size] = '\0';}else if (size > _size){if (size > _capacity){reserve(size);}int i = _size;while (i != size){_str[i++] = '\0';}_size = size;_str[_size] = '\0';}}

        如果设定值小于现有值,减小_size,相当于截断_str;

        如果设定值等于现有值,不做处理;

        如果设定值大于现有值,有三种情况:

                size <_capacity:        不扩容,并在[ _size,size)之间补0;

                size == _capacity:        不扩容,并在[ _size,size)之间补0;

                size > _capzcity:        扩容,并在[ _size,size)之间补0;

(4)元素访问

 i,operator[]

        下标的随机访问:

//声明
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
//定义
char& string::operator[](size_t pos)
{assert(pos >= 0 && pos < _size);return _str[pos];
}
const char& string::operator[](size_t pos) const
{assert(pos >= 0 && pos < _size);return _str[pos];
}

对于at,front,back可以复用operator[]来实现。

(5)修改方式

i,push_back()

        实现尾插字符,实现如下:

//尾插字符,由于是一个一个插入,扩容不能太频繁,所以采用二倍扩容
string& string::push_back(const char ch)
{if (_size == _capacity)//不一定需要扩容,若长度等于容量,再次插入需要扩容{int Newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(Newcapacity);}//扩容完毕,尾插字符_str[_size++] = ch;_str[_size] = '\0';return *this;
}

        这里使用了一个扩容技巧,就是二倍扩容。

ii,append()

        追加,这里简化为追加一段字符串。

//尾插字符串,直接reserve到指定长度字符串
string& string::append(const char* str)
{int len = strlen(str);if (len + _size > _capacity){reserve(len + _size);//不改变size}//扩容完毕strcpy(_str + _size, str);_size += len;return *this;
}

        首先要先保存原来的len,这样如果需要扩容,在扩容完毕之后,只需更新_size为原_size+=len即可。

        否则,如果不保存len,在需要扩容的情况下,就会出现问题了:

##

()

##

iii,operator+=复用上两函数即可

        尾插一个字符

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

        尾插一个字符串

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

iv,insert()

        在任意位置插入一个字符

//插入一个字符
//用push_back逻辑来扩容
string& string::insert(size_t pos, const char ch)
{assert(pos >= 0 && pos <= _size);if (_size == _capacity){int Newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(Newcapacity);//不改变size}int end = _size+1;//细节问题,int与size_t参与比较,//int隐式类型转化为size_t//size_t(-1)会变成很大的整数while(end>pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;_size += 1;return *this;
}

         在任意位置插入一个字符串
//插入一个字符串
//用reserve逻辑扩容
string& string::insert(size_t pos, const char* str)
{assert(pos >= 0 && pos <= _size);int len = strlen(str);if (len + _size > _capacity){reserve(len+_size);}int end = _size + len;while (end>pos+len-1){_str[end] = _str[end - len];--end;}memmove(_str + pos, str, len);_size += len;return *this;
}

v,erase()

        在任意位置处删除长度为len的字符串:

string& string::erase(size_t pos, size_t len)//两种情况;删除部分string,pos之后全删
{assert(pos >= 0 && pos <= _size);if ((len == npos) ||(pos + len >= _size))//全删的情况{_str[pos] = '\0';_size = pos;}else//删除部分string{int end = pos + len;while (_str[end]!='\0'){_str[end - len] = _str[end];++end;}_str[end-len] = '\0';}return *this;
}

(6)串操作

i,find()

        找字符

size_t string::find(const char ch, size_t pos)
{for (size_t i = pos; i < _size; ++i){if (_str[i] == ch){return i;}}return npos;
}

        找字符串

        用到了strstr():字符串匹配函数。

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

ii,c_str()

        返回C类型的字符串:

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

iii,substr()

        得到字符串的子串:

string string::substr(size_t pos, size_t len){assert(pos >= 0 && pos <= _size);if ((len == npos) || (pos + len >= _size)){string sub(_str + pos);return sub;}else{ string sub;sub.reserve(len);for (size_t i = 0; i < len; ++i){sub._str[i] = _str[pos + i];}sub._str[len] = '\0';sub._size =sub._capacity =  len;return sub;}}

(7)成员常量

//特例,const静态整形对象可声明定义和一,但是可能造成链接时的错误
const static size_t npos = -1;

        无符号整数size_t(-1)是一个很大的整数。

(8)流插入和流提取

i,operator<<()

ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s._size; ++i){cout << s._str[i];}cout << endl;return out;
}

ii,operator>>()

        cin的get()函数可以提取空白字符和‘\n’,这也是循环逻辑结束的条件。

//流提取改进,用buf临时数组,防止string频繁扩容
istream& operator>>(istream& in,string& s)
{s.clear();char buff[128] = { 0 };char ch = in.get();int i = 0;while(ch != ' ' && ch != '\n'){buff[i++] = ch;ch = in.get();if (i == 127){buff[i] = '\0';s += buff;i = 0;}}buff[i] = '\0';if (i != 0){s += buff;}return in;
}

        整体使用了用临时栈区数组的方式来减少扩容次数,提高效率。


完~

未经作者同意禁止转载 

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

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

相关文章

ipables防火墙

一、Linux防火墙基础 Linux 的防火墙体系主要工作在网络层&#xff0c;针对 TCP/IP 数据包实施过滤和限制&#xff0c;属于典 型的包过滤防火墙&#xff08;或称为网络层防火墙&#xff09;。Linux 系统的防火墙体系基于内核编码实现&#xff0c; 具有非常稳定的性能和高效率&…

VB7/64位VB6开发工具office插件开发-twinbasic

全新的VB7&#xff0c;twinbasic&#xff0c;支持64位开发&#xff0c;支持EXCEL插件开发&#xff0c;老外连续3年闭关修练终成正果 官方最新版下载&#xff1a;https://github.com/twinbasic/twinbasic/releases 汉化工具用法&#xff1a;把工具和Lang_Tool目录复制到Twinbasi…

SAP PP学习笔记18 - MTO(Make-to-Order):按订单生产(受注生産) 的策略 20,50,74

前面几章讲了 MTS&#xff08;Make-to-Stock&#xff09;按库存生产的策略&#xff08;10&#xff0c;11&#xff0c;30&#xff0c;40&#xff0c;70&#xff09;。 SAP PP学习笔记14 - MTS&#xff08;Make-to-Stock) 按库存生产&#xff08;策略10&#xff09;&#xff0c;…

ChatTTS 开源文本转语音模型本地部署、API使用和搭建WebUI界面(建议收藏)

ChatTTS&#xff08;Chat Text To Speech&#xff09;是专为对话场景设计的文本生成语音(TTS)模型&#xff0c;特别适用于大型语言模型(LLM)助手的对话任务&#xff0c;以及诸如对话式音频和视频介绍等应用。它支持中文和英文&#xff0c;还可以穿插笑声、说话间的停顿、以及语…

计算机网络ppt和课后题总结(下)

常用端口总结 计算机网络中&#xff0c;端口是TCP/IP协议的一部分&#xff0c;用于标识运行在同一台计算机上的不同服务。端口号是一个16位的数字&#xff0c;范围从0到65535。通常&#xff0c;0到1023的端口被称为“熟知端口”或“系统端口”&#xff0c;它们被保留给一些标准…

基于百度接口的实时流式语音识别系统

目录 基于百度接口的实时流式语音识别系统 1. 简介 2. 需求分析 3. 系统架构 4. 模块设计 4.1 音频输入模块 4.2 WebSocket通信模块 4.3 音频处理模块 4.4 结果处理模块 5. 接口设计 5.1 WebSocket接口 5.2 音频输入接口 6. 流程图 程序说明文档 1. 安装依赖 2.…

RHEL8/Centos8 install for PXE

PXE介绍 PXE&#xff08;Preboot Execution Environment&#xff09;是预引导执行环境的缩写。它是由Intel设计的&#xff0c;允许客户端计算机通过网络从服务器上加载操作系统镜像。PXE通常用于大规模部署操作系统&#xff0c;例如在企业或学校环境中。 PXE工作流程如下&…

【复现】含能量路由器的交直流混合配电网潮流计算

目录 1 主要内容 2 理论及模型 3 程序结果 4 下载链接 1 主要内容 程序复现《含能量路由器的交直流混合配电网潮流计算》&#xff0c;主要是对算例4.1进行建模分析&#xff0c;理论和方法按照文献所述。能量路由器&#xff08;ER&#xff09;作为新兴的电力元器件&#xff…

Spring Boot通过自定义注解和Redis+Lua脚本实现接口限流

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

FuTalk设计周刊-Vol.040

&#x1f525;AI漫谈 热点捕手 1、零代码定制游戏NPC&#xff0c;百川智能发布角色大模型 百川智能此次推出了“角色创建平台搜索增强知识库”的定制化解决方案。通过这一方案&#xff0c;游戏厂商无需编写任何代码&#xff0c;只需通过简单的文字描述&#xff0c;便可以快速…

IT人的拖延——都是“分心”惹的祸?

典型表现 我们说到拖延的原因有很多&#xff0c;还有一个原因是因为“分心太多“造成的&#xff0c;分心太多的拖延大致上有以下表现&#xff1a; 无法集中注意力&#xff1a; 分心太多会导致我们无法集中注意力在当前的工作任务上&#xff0c;我们可能会经常性地走神或者在工…

Vue12-计算属性

一、姓名案例 1-1、插值语法实现 1、v-bind v-bind的问题&#xff1a; 所以&#xff1a;v-bind是单向绑定。 2、v-model 解决v-bind的问题。 3、输出全名 方式一&#xff1a; 方式二&#xff1a; 需求优化&#xff1a;全名中的姓氏&#xff0c;只取输入框中的前三位&#xf…

VSCode数据库插件

Visual Studio Code (VS Code) 是一个非常流行的源代码编辑器&#xff0c;它通过丰富的插件生态系统提供了大量的功能扩展。对于数据库操作&#xff0c;VS Code 提供了几种插件&#xff0c;其中“Database Client”系列插件是比较受欢迎的选择之一&#xff0c;它包括了对多种数…

使用C++结合OpenCV进行图像处理与分类

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的在读研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三…

基于STC12C5A60S2系列1T 8051单片机实现串口调试助手软件与单片机相互发送数据的RS485通信功能

基于STC12C5A60S2系列1T 8051单片机实现串口调试助手软件与单片机相互发送数据的RS485通信功能 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机串口通信介绍STC12C5A60S2系列1T 8051单片机串口通信的结构基于STC12C5A60S2系列1T 8051单片机串口通信的特殊功…

力扣 74.搜索二维矩阵

题目描述&#xff1a; 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&am…

决策树Decision Tree

目录 一、介绍发展优点缺点基本原理 二、熵1、熵2、条件熵3、信息增益4、信息增益率 三、基尼系数四、ID3算法1、建树过程2、优点3、缺点 五、C4.51、二分法处理连续变量1、流程&#xff1a;2、示例 2、缺点 六、CART1、连续数据处理2、离散数据处理3、CART回归原理1、均方误差…

【机器学习】机器学习引领AI:重塑人类社会的新纪元

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀机器学习引领AI &#x1f4d2;1. 引言&#x1f4d5;2. 人工智能&#xff08;AI&#xff09;&#x1f308;人工智能的发展&#x1f31e;应用领…

每日两题6

文章目录 删除并获得点数粉刷房子 删除并获得点数 分析 class Solution { public:int deleteAndEarn(vector<int>& nums) {const int N 10001;// 预处理int arr[N] {0};for (int& e : nums)arr[e] e;// 在 arr 上进行 打家劫舍 问题vector<int> f(N),…

【Python机器学习】NMF——模拟数据

与使用PCA不同&#xff0c;我们需要保证数据是正的&#xff0c;NMF能够对数据进行操作。这说明数据相对于原点(0,0)的位置实际上对NMF很重要。因此&#xff0c;可以将提取出来的非负向量看作是从(0,0)到数据的方向。 举例&#xff1a;NMF在二维玩具数据上的结果&#xff1a; …