C++ STL初阶(2):string 的模拟实现

此文的背景是自己实现库中的string,由于string的模版实现较为困难,我们只实现最简单char版本。

1.命名空间分割

为了避免与库中的string冲突,我们使用一个自己的命名空间中来分离并实现所有内容,并且将所有的声明和定义相分离,因此需要使用相同名字的命名空间。申明和定义都在同一个命名空间中,就会自动合并。

         

                                                   (用一个命名空间来隔离。避免冲突)

为什么 # include " string.h"不会与库冲突?

在编译一讲提到过,双引号下的头文件名字先搜索自己的本地文件。又由于我们自己实现了

string.h在项目文件夹中,所以会优先使用我们自己实现的string


2.构造、析构函数

先在.h文件中声明:

                            

(VS下的string其实还包含一个16个字节的buffer数组,此处我们简化掉该buffer数组)

再到.cpp中去实现:

namespace lsnm {string::string(const char* str) :_str(str),_size(strlen(str)),_capacity(strlen(str)){}string::~string() {}
}

为什么不用sizeof而是用strlen?

szieof(_str)相当于计算一个指针的大小,因为这是一个常量字符串的指针,而不是数组名,因此不会计数整个数组的大小。 

  但是发现出现了报错:

为什么在初始化列表中不能直接用参数str来初始化?

由内存管理中的知识可知,如果用一个常量字符串赋值来初始化

(str和_str都是char* 类型的变量,我们没有拷贝,而是一直都在传指针,相当于传了一个常量区的指针去可读可写,扩大了权限)

常量字符串是存在于常量区的并且不可被修改的,直接

:_str(str),

 会使我们按照string s1="abcd"初始化的s1无法改变内容(无法插入删除修改等)

正确使用方法:

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

提醒:类函数定义需要指定类域,所以每一个函数前面都有一个 string ::

   此时的函数有一个问题:      

上文构造函数中,strlen要跑三次,效率较低,能不能按照下文方法写?

先在初始化列表中写size,只执行一次strlen呢?

这是经典错误。因为初始化列表会按照在private中的声明的顺序初始化。

解决方案:

1.在private中改声明顺序为适合的顺序:

           

但是这样不妥,如果一不小心改了private中的顺序就会出现报错。

解决方案2:

初始化列表虽然好,但是也不能死板的一直使用,此时就建议放在函数体中去定义。

复习关于初始化列表:

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

我们再快速实现一个c_str(因为现在还没有实现流提取的重载,所以c_str之后可以便于打印和检查),建议用后置const修饰this指针,也就是:

const char* c_str() const;

        这样的话const的string和非const修饰的string就都可以调用这个c_str() ,当然,同时也都不能修改由c_str返回的char形数组。

(此处的string都指的是我们自己实现的string)

注意:在.c文件中分离实现时,返回类型是写在域名的前面的。

           

namespace lsnm {string::string(const char* str) :_size(strlen(str)){_str = new char[strlen(str) + 1];strcpy(_str, str);_capacity = _size;}string::~string() {delete[] _str;_str = nullptr;_size = _capacity = 0;}const char*  string :: c_str() const {return this->_str;}
}

在之前的学习中,我们说到构造函数(尤其是包含自定义类型)最好实现默认构造。

因此我们再实现一个无参的string构造函数。

                                            

这样写又是很经典的错误。错误原因:与库中的功能不符合。库中直接string s1;

紧接着,s1可以被自由使用、打印,其里面只包含了一个'\0'

但是报错的原因不是delete, 因为free和delete的底层都是可以操作nullptr的.....

正确的做法:

  (new出来的自定义类型在后面用花括号赋值):

string::string() {_str = new char[1] {'\0'};_size = _capacity = 0;
}//也可以写成这样
string::string() : _str(new char[1]{""})
{_size = _capacity = 0;
}

然后合并无参和带参为全缺省:

                       

           

不写\0是因为作为char类型的数组,本身自带\0

声明和定义分离时,参数写在声明处。


3.方括号遍历与size函数

size_t string :: size() const{return this->_size;
}char& string:: operator[](size_t pos) {assert(pos<_size);return _str[pos];
}const char& string :: operator[](size_t pos) const {assert(pos<_size);return _str[pos];
}

4.实现用于遍历的迭代器

除了方括号遍历, 最常用的还有范围for循环。

我们如果想实现范围for,就需要先实现迭代器版本的遍历。(范围for循环的底层是编译为迭代器版本的循环)

因为范围for的底层是迭代器;

我们此处只实现原生指针版本的迭代器

先typedef一下

注意,返回类型和函数名都属于类域中,都需要单独用类域展开一下:

                    

iterator属于 char* ,所以此处的实现直接按照指针来就可以了

string::iterator string::begin() {return this->_str;}
string::iterator string::end() {return this->_str + _size;
}

我们操作的都是加了一层皮的char*   , begin()和end()返回的都是指针

切记,iterator是我们自己定义的。


但是,倘若我们把自定义的iterator全部换回char*

范围for还能通过吗?

答案是可以的,因为范围for的底层是去找begin()和end(),只要实现了begin()和end(),就都可以实现了。auto又能自动推导类型,将e作为char类型

但是如果我们把begin改成Begin,范围for就又不能通过了,因为找不到begin()

iterator的作用:

用iterator的方法是完成一种对底层逻辑的封装。因为iterator其实不确定到底是哪种类型,自定义类型还是内置类型都有可能作为iterator。

因为iterator的原生类型都不一样,不同的平台实现也可能不一样,所以规定都叫做iterator,便于使用。比如reverse算法函数,不关心你的访问方法是自定义、还是char*、还是int*,只管使用iterator

这样就能将分离的算法和数据结构相结合,也统一了不同的数据结构的使用方法。

所有的访问方式都能通过迭代器进行。对使用者更加方便。

这一点也能体现类和对象中的特点之一:封装。


除此之外,还有const修饰的iterator:

      

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

5.增添、删除、修改 

string作为一个相对复杂的顺序表

5.1 push_back和append

前者用来插入字符,后者用来插入字符串。

前者在扩容时可以直接扩二倍,但是增加字符串的时候可以只增加二倍吗?

因此,我们需要先引入扩容函数:

             

实现如下:

void string :: reserve(size_t n) {if (n <= this->_size) return;char* tmp = new char[n+1];//多开一个预留给\0strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;
}

只要我们希望reserve出的空间大于等于 _size+1 (还需要给'\0'留一个位置), 就都是合法的,可以在大于等于_size+1的情况下进行缩容。 

再实现两个填充内容的函数: 

关于开出空间的大小:如果需要n个空间,永远开n+1个空间,因为要预留一个给\0

自己开空间,拷贝内容,改变指针指向,再将原空间释放掉。

同时,对push_back和append是否需要扩容作出判断:

    

在push_back汇总同时处理\0:

对于append,我们可以使用最简单的for循环一个一个放进去:

void string::append(const char* str) {size_t len = strlen(str);reserve(_size + len);for (int i = 0; i < len; i++) {_str[_size++] = str[i];}_str[_size] = '\0';
}

  

也可以用C语言中的字符串函数如strcat去实现,(strcat能自主覆盖destination的\0并且移植新的\0)

strcat有什么弊端?

strcat的底层是从头开始找'\0',然后从\0的位置开始覆盖。这样固然没有问题,但是操作效率变低。我们清晰\0的位置  :   (_str+_size) ,那直接使用strcpy,跳过寻找\0的过程,提高效率。

                 

再实现类似功能(并且最好用)的 += 

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

 此处是实现类的内部函数,所以默认所有的push_pack或者append都是直接对this对应的元素使用。

记得传引用返回,提升效率,避免传值返回时重复复制。

5.2 insert和erase

先声明三个函数:

             

为什么不给pos加缺省参数?

要给pos加缺省参数就必须先给ch或者str加,因为缺省参数只能从右边开始赋值。

需要用到npos,我们自己定义一个static的npos.

static需要在第一次使用时就既声明又定义,但如果就像上图那样使用,string.cpp和string.h都会包含一次这个npos,导致重复定义,从而链接出错。

关于静态成员在不需要链接时候的使用如下:

C++:类与对象(2)-CSDN博客

static修饰的成员变量没有被保存在类中,而是保存在静态区中。

本文中,应当将npos的声明和定义相分离:在.h中声明,在.cpp中定义 ,在.test中可以直接使用。

链接时,类似于函数一样,因为先在头文件中声明过了所以编译能通过,最后去生成.o中找这个值。

                        

补充一点很奇怪的知识:

可以用const修饰之后给缺省值,并且只有整型可以

很奇怪,了解即可。


实现insert:

                 注意,要将\0一并移走,所以从end+1(\0所在位置)开始移动。

void string :: insert(size_t pos, char ch) {assert(pos <= _size);if (_size == _capacity) {size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}size_t end = _size;for (int i = _size - pos; i >= 0; --i) {_str[end + 1] = _str[end];end--;}_str[pos] = ch;
}

注意顺序表阶段的一个小问题,往哪边挪就得从哪边开始挪。

不用担心\0,\0也被一起挪动了。


不过如果不引进变量i, 写成以下形式 ,并且进行头插(在pos=0的位置插入):

end作为一个无符号整形,不管如何加减,是不可能小于0的。

因此此时无法头插,会死循环:

                                              

那如果将end改为int呢?

依然死循环。

5.2.1无符号小于等于零都是坑

为什么将end的类型改成int之后依然会死循环呢??

对于一个双目操作符,当两侧数据的类型不一样时,会发生隐式类型转换。

其中的原则就是有符号的都会变为无符号的。因此在判断end >= pos时,会因为_size的类型是无符号整形,所以end也会被转换成无符号整形。

解决方法:

1.  while的条件中进行强转。

 2.  pos直接写成int,但是这样与库中不一样。

3.   将end指向更后面的一位,等于0的时候就会跳出循环。

4.   引入新变量int

                           


插入字符串的insert:

移动部分的逻辑同上:

紧接着我们利用库函数将传入的参数str直接插入*this

但是插入部分不能用strcpy,因为strcpy会自己补/0,提前结束字符串

因此使用strncpy或者memcpy来避免自动补\0的问题

关于C语言字符串函数中的弊端,我们稍加总结:strcat会从头开始找\0,效率较低;而strcpy会在插入结束后自动在末尾补\0,因此strcpy不能用于在一个字符串的中间插入;memcpy就是一个字节一个字节的拷贝,非常“朴实无华”) 

void string :: insert(size_t pos, const char* str) {assert(pos <= this->_size);size_t len = strlen(str);if (_capacity < _size + len)reserve(_size + len);size_t end = _size;//_str[_size + len + 1] = '\0';//"abcdefg\0"  "qwe\0"while (end >= pos) {_str[end + len] = _str[end];--end;}memcpy(_str + pos, str, len);_size += len;
}

依然有死循环的问题,我们改变end的类型并且在while条件处强转:

  


5.2.2erase

当触发len==npos,需要全部删完的时候:

             

不要考虑用delete,因为delete不能只删除部分空间。

直接将pos位置变成\0即可(pos位置后面的都不要了),同时改变_size

或者要删除的长度len大于可以被删除的部分(就像官网定义中的is too short的那样)


不全部删完:

直接平移覆盖即可。

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

也可以将for循环的覆盖写为:

                  


6.find函数

寻找字符:

默认找不到的时候不可能是无符号整形的最大值(42亿多,一个字符串不可能有四个G)

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

匹配子串:

用strstr(底层是BF算法)即可。因为KMP的算法在实际运用中效率并没有非常出色,非常依赖自身的重复性(需要自身的重复性来体现效率)。


7.拷贝构造

      如果我们执行这样一个代码:               ​​​​

由于没有实现拷贝构造,所以自动生成一个浅拷贝。

但是此处浅拷贝就会在析构的时候报错,因为对同一块堆上的数组空间析构了两次。

同时,也存在修改s1就会修改到s2的尴尬情况。

因此,我们换一个新逻辑:直接重新开一个一样的空间,进行strcpy即可。

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

8.运算符重载

8.1 赋值运算符重载:

先试试系统默认生成的:

为什么发生报错呢?

原因和之前的拷贝构造一样,默认生成的是浅拷贝,浅拷贝是一个字节一个字节的拷贝,会将_str的指针拷贝过去,在释放时会对同一个数组delete[]两次,因此报错。

除了两次delete[]会发生报错,还有以上两种情况证明浅拷贝是不够的: 

情况1空间不够,情况2空间浪费严重

解决方法:

我们简单粗暴的处理,直接开一个新空间调用strcpy,再释放掉原空间。     

不过倘若执行 s1=s1就亏了,再加一个判断条件:

此时再执行s2=s1就不会报错了:


 此时能使用swap完成s1和s2的交换吗?

8.2 swap

答案是可以的,因为swap是模版函数。

但是这个swap代价很大,通过观察swap的源码,我们发现要完成swap需要三次深拷贝。

所以我们自己在类中实现一个消耗小的:

直接改指针即可,并且使用库中的swap调换相应的数据:

            


    C++标准库自然也想到了这个问题,string作为一个容器,有专属于自己的swap,来避免深拷贝问题。

    为了避免使用者不小心调用库中的标准swap,c++考虑的非常周全,利用匹配原则(如上):直接调用swap(string)版本,并且还是全局实现的。

"the strings exchange references to their data, without actually copying the characters "

只交换指针,没有交换实质里的内容。


9.substr

       获取子串依然是一个涉及“len和pos”的问题,依然分两种情况讨论。

​​​​​​​​​​​​​​

                                        (直接从pos的位置去构造一个)

会报错,此处的问题在于不能传引用返回,因为sub会被销毁。

string string::substr(size_t pos, size_t len) {if (pos + len >= _size) {string sub(_str + pos);return sub;}else {string sub;sub.reserve(_size + 1);for (int i = 0; i < len; i++) {sub += _str[pos + i];}return sub;} 
}


10.其他常用运算符

如+ - += -=等等

我们借助strcmp来实现小于和等于。

实现小于和等于之后·,其他都可以直接复用。

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

11.流插入和流提取

由于运算符中操作数顺序的问题(cout<<s1),流插入不适于写在类内部(类内部函数的第一个参数是this)。

但是此处不需要写成友元函数,因为可以不访问类内部的数据(访问一个_size和使用一个public函数 operator[ ]),就可以直接访问公有元素。

因为[ ]运算符在重载之后的本质是一个返回char&的函数,而该函数是在public中的,所以可以直接使用

最后return的目的是为了便于连续输出。

留提取:

我们此时只输入4个x试试:

这是因为只提取了一次,,,,

需要多次提取:

依然拿不到换行。

看看测试函数:

                

因为cin拿不到空格和换行。cin会将空格和换行默认当作操作者这次输入与下次输入之间的隔阂。

正确使用(能拿到换行):用is.get()

还需要一个clear,cin之前的要清空。

综上所述,

流插入和流提取不能写作成员函数   (正确)

流插入和流提取需要写成友元函数   (错误)

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

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

相关文章

Etcd Raft架构设计和源码剖析1:宏观架构

Etcd Raft架构设计和源码剖析1&#xff1a;宏观架构 | Go语言充电站 序言 Etcd提供了一个样例contrib/raftexample&#xff0c;用来展示如何使用etcd raft。这篇文章通过raftexample介绍如何使用etcd raft。 raft服务 raftexample是一个分布式KV数据库&#xff0c;客户端可…

vivado BEL_PIN、CELL

BEL_PIN是BEL对象上的接点或连接点。 BEL_PIN是一个设备对象&#xff0c;与网表对象&#xff08;如逻辑上的PIN&#xff09;相关 CELL&#xff0c;它是NET的连接点。 相关对象 如图所示&#xff0c;BEL_PIN-对象与BEL和SITE设备资源相关&#xff0c;并且 PIN和NET网表对象。您可…

Nginx配置详细解释:(3)http模块及server模块,location模块

目录 环境概述&#xff1a; http模块中的全局模块 1. root配置主要是对主web页面的路径访问。 2.server虚拟主机 2.1基于IP&#xff1a; 2.2基于域名&#xff1a; 3.alias别名 4.location匹配 5.access模块&#xff1a; 6.验证模块 7.自定义错误页面 8.日志存放位置…

王道408数据结构CH3_栈、队列

概述 3.栈、队列和数组 3.1 栈 3.1.1 基本操作 3.1.2 顺序栈 #define Maxsize 50typedef struct{ElemType data[Maxsize];int top; }SqStack;3.1.3 链式栈 typedef struct LinkNode{ElemType data;struct LinkNode *next; }*LiStack;3.2 队列 3.2.1 基本操作 3.2.2 顺序存储…

【Intro】Cora数据集介绍

https://graphsandnetworks.com/the-cora-dataset/ Graph Convolutional Network (GCN) on the CORA citation dataset — StellarGraph 1.0.0rc1 documentation pytorch-GAT/The Annotated GAT (Cora).ipynb at main gordicaleksa/pytorch-GAT GitHub Cora数据集 Cora数据…

Windows上虚拟机安装OpenGaus22.03

在Windows上安装OpenGauss并不像在Linux上那么直接&#xff0c;因为OpenGauss主要面向OpenEuler系统设计。可以通过使用虚拟机或者Docker来在Windows上运行OpenGauss。虚拟机比Docker提供更完整的操作环境。以下是采用虚拟机的详细步骤&#xff1a; 通过虚拟机安装OpenGauss …

运放应用1 - 反相放大电路

1.前置知识 反相放大电路存在 负反馈电路 &#xff0c;工作在线性区&#xff0c;可以利用 虚短 概念来分析电路。 注&#xff1a;运放的 虚断 特性是一直存在的&#xff0c;虚短特性则需要运放工作在 线性区 有关运放的基础知识&#xff0c;可以参考我的另外一篇文章&#xff…

ASCE(美国土木工程师学会)文献校外去哪里查找下载

今天要讲的数据库是ASCE&#xff08;美国土木工程师学会&#xff09;&#xff0c;该数据库每年出版5万多页的专业期刊、杂志、会议录、专著、技术报告、实践手册和标准等。目前&#xff0c;ASCE数据库中包含35种期刊(1983年至今)、近700卷会议录( 1996年至今)、Civil Engineeri…

htb_solarlab

端口扫描 80,445 子域名扫描 木有 尝试使用smbclient连接445端口 Documents目录可查看 将Documents底下的文件下载到本地看看 xlsx文件里有一大串用户信息&#xff0c;包括username和password 先弄下来 不知道在哪登录&#xff0c;也没有子域名&#xff0c;于是返回进行全端…

C++缺省参数函数重载

缺省参数 大家知道什么是备胎吗&#xff1f; C中函数的参数也可以配备胎。 3.1缺省参数概念 缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时&#xff0c;如果没有指定实参则采用该默认值&#xff0c;否则使用指定的实参。 void TestFunc(int a 0…

智慧医疗新纪元:可视化医保管理引领未来

在数字化浪潮席卷全球的今天&#xff0c;我们的生活正在经历前所未有的变革。其中&#xff0c;智慧医保可视化管理系统就像一股清新的风&#xff0c;为医疗保障领域带来了全新的活力与可能。 想象一下&#xff0c;在繁忙的医院里&#xff0c;患者和家属不再需要为了查询医保信息…

关于nginx的一些介绍

一、Nginx 简介 中文简介文档 二、Centos 安装 Nginx 2.1 安装编译工具及库文件 $ yum -y install make zlib zlib-devel gcc-c libtool openssl openssl-devel2.2 安装 pcre pcre 作用是 Nginx 支持 Rewrite 功能 $ cd /usr/local/src $ wget http://downloads.sourcef…

VBA信息获取与处理第二个专题第五节:实际场景中随机数的利用

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

Vxe UI vue 使用 VxeUI.previewImage() 图片预览方法

Vxe UI vue 使用 VxeUI.previewImage() 图片预览方法的调用 查看 github 代码 调用全局方法 VxeUI.previewImage() 参数说明&#xff1a; urlList&#xff1a;图片列表&#xff0c;支持传字符串&#xff0c;也可以传对象数组 [{url: xx’l}] activeIndex&#xff1a;指定默…

2. redis配置文件解析

redis配置文件解析 一、redis配置文件1、监听地址2、监听端口3、redis接收请求的队列长度3.1 修改系统参数/内核参数 4、客户端空闲的超时时间5、指定redis的pid文件6、定义错误日志7、定义数据库的数量8、定义持久化存储9、设置redis密码10、redis并发连接11、最大内存策略 二…

Windows Server FTP详解

搭建&#xff1a; Windows Server 2012R2 FTP服务介绍及搭建_windows2012server r2ftp怎么做&#xff1f;-CSDN博客 问题&#xff1a; https://www.cnblogs.com/123525-m/p/17448357.html Java使用 被动FTP&#xff08;PASV&#xff09; 被动FTP模式在数据连接建立过程中…

计算机网络 ——数据链路层(广域网)

计算机网络 —— 广域网 什么是广域网PPP协议PPP协议的三个部分PPP协议的帧格式 HDLC协议HDLC的站HDLC的帧样式 PPP和HDLC的异同 我们今天来看广域网。 什么是广域网 广域网&#xff08;Wide Area Network&#xff0c;简称WAN&#xff09;是一种地理覆盖范围广泛的计算机网络…

Redis篇 list类型在Redis中的命令操作

list在redis基本的命令 一.基本命令1.lpush和range2.lpushx rpushx3.lpop rpop4.lindex linsert llen5.lrem6.ltrim lset7.blpop brpop 一.基本命令 list在redis中相当于数组或者顺序表. 1.lpush和range 2.lpushx rpushx 3.lpop rpop 4.lindex linsert llen 如果要插入的列表中…

【Kubernetes】9-Pod控制器

一、什么是 pod 的控制器 Pod控制器&#xff0c;又称之为工作负载&#xff08;workload&#xff09;&#xff0c;是用于实现管理pod的中间层 确保pod资源符合预期状态&#xff1b;pod的资源故障时会进行重启&#xff1b; 当重启策略无效时&#xff0c;则会重新新建pod的资源 二…

一维时间序列信号的小波时间散射变换(MATLAB 2021)

小波散射变换的目的在于获取第一层次的特征信息&#xff0c;即免疫平移、轻微形变的信息。而低通的滤波器能够获取输入信号的概貌&#xff0c;获取反映其整体大尺度特征的信息&#xff0c;以图像为例&#xff0c;由低通滤波器选取的信号对于图像的平移、伸缩、旋转等局部变化有…