【C++之STL】摸清 string 的模拟实现(下)

【C++之STL】摸清 string 的模拟实现(下)

string的模拟实现系列文章:

  1. 模拟实现上
  2. 模拟实现中
  3. 模拟实现下

文章目录


7. 字符串操作

7. 1 c_str()date()

constchar*c_str()const;

返回string对象底层的字符串的指针,且不可通过返回的指针修改字符串。

实现时直接返回即可。

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

但是要注意的是,如果在使用该函数并将其返回值存储起来后,如果后续对string操作时导致string的字符串的地址发生了改变,那么之前存储起来的指针就已经失效了,是个野指针。

这个函数用来兼容一些C语言的不兼容string类型的接口。

date()接口和c_str()的效果是基本一样的。

7. 2 find()

首先是find(),它有四种重载:

size_t find(const string& str, size_t pos =0)const; size_t find(constchar* s, size_t pos =0)const; size_t find(constchar* s, size_t pos, size_t n)const; size_t find(char c, size_t pos =0)const;

虽然有四种,但它们的目的是一样的:从string对象的第pos(缺省为0)个元素开始向后查找到数据尾部,查找与 str/sn个字符完全相同的字符串,并返回第一个匹配项的第一个字符的位置,如果没有匹配项就返回npos

这里模拟实现一下第2个和第4个重载,其他重载也是同理。

size_t string::find(char c, size_t pos)const{// 首先要断言,不然会数组越界访问assert(pos < _size);// 从pos位置开始找与字符 c 相同的字符for(size_t i = pos; i < _size; i++){if(_str[i]== c)return i;}// 没找到返回 nposreturn npos;} size_t string::find(constchar* s, size_t pos)const{assert(pos < _size);// strstr是C语言中用于比较两个字符串是否相同的库函数,使用方法请看下文。char* tmp =strstr(_str + pos, s);if(tmp ==nullptr)return npos;return tmp - _str;}

补充:strstr的使用,在第10章。

如果是带有size_t n形参的函数,可以先把str/s的前n个字符拷贝出来再进行相同的过程。

而与find()功能相似的一系列函数:

// 与find()相同,只是从后往前找rfind()// 在字符串中搜索与其参数中指定的任何字符匹配的第一个字符。find_first_of()// 从后往前找find_last_of()// 找第一个不是的find_first_of()// 从后往前找第一个不是的find_last_not_of()

可以自行尝试实现,与find()其实都大同小异,这里就不再赘述了。

8. 迭代器相关

上文第三章我们提到过,可以直接使用

typedefchar* iterator;

来模拟实现迭代器,这里做进一步补充。

8. 1 迭代器实现

尽管我们已经通过这种方式得到了迭代器,但迭代器除了普通迭代器之外还有const迭代器,对于一个被const修饰的string类型,如果尝试取出一个普通迭代器,会发生权限放大;以及在一些场景下,我们不希望迭代器有对对象中的数据进行修改的权限,就可以使用const迭代器,它的声明如下:

typedefconstchar* const_iterator;

注意不要修改const_iterator这个名称!

对于普通的迭代器,当需要需要修改数据时,直接进行解引用然后修改就行了,而const迭代器虽然也能解引用,但是不能修改,只能查看数据。

8. 2 begin()

直接返回_str的第一个元素位置。

注意要实现两个版本,一个是普通版本的,还有一个const版本的。

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

8. 3 end()

返回指向_str的最后一个元素的下一个位置的指针。

也要提供两个版本。

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

注意迭代器区间是左开右闭的,所以end()指向的是有效数据的下一位。

rbegin()rend的实现相对复杂,这里先不讲。

9. 运算符重载

9. 1 流插入和流提取

类和对象(中)5.1章第13点我们提到过,流插入和流提取必须重载为全局函数或者使用友元,这离我们依然将其重载为全局函数。

std::ostream&operator<<(std::ostream& out,const test::string& s); std::istream&operator>>(std::istream& in, test::string& s);
  1. 流插入比较简单,只需要直接将_str插入到流中就可以了。
ostream&operator<<(ostream& out,const string& s){for(size_t i =0; i < s.size(); i++){ cout << s[i];} cout << endl;return out;}
  1. 但流提取比较复杂,因为我们不知道会插入多长的数据,所以不能直接包装 cin>>_str,我们可以每次读取一个字符,然后判断和这个字符是不是' '或者'\n',如果是就停止读取,不是就插入到字符串中。但是每次都进行插入可能会有性能问题,为了缓解这一问题,我们可以创建一个字符数组,每次读取到字符之后先放到这个字符数组中,如果这个字符数组满了或输入结束,就把它加到string对象中,这样可以缓解性能问题。

补充:istream::get()类似于getchar(),可以从流中得到一个字符。

istream&operator>>(istream& in, string& s){char ch ='\0';// 流提取会把原来的数据全部删除 s.clear();// 先提取一次,避免错误数据被插入 in.get(ch);// 临时数组char tmp[256];int times =0;while(ch !='\n'&& ch !=' '){if(times ==255){ tmp[times]='\0'; s += tmp; times =0;} tmp[times++]= ch; in.get(ch);}// 如果是输入结束,也要把tmp再次追加到string对象后面if(times !=0){ tmp[times]='\0'; s += tmp;}return in;}

9. 2 比较运算符

booloperator<(const string& s1,const string& s2);booloperator<=(const string& s1,const string& s2);booloperator>(const string& s1,const string& s2);booloperator>=(const string& s1,const string& s2);booloperator==(const string& s1,const string& s2);booloperator!=(const string& s1,const string& s2);

比较操作符虽然可以在类中重载,但也可以将其重载为全局函数。

虽然看着很多,但实际上我们只需要实现两个——==>,其他的就可以全部进行复用这两个来快速解决这六个函数了。

另外在实现operator==operator>时,也可以直接使用C语言的库函数strcmp,使用方式可见:字符串函数第六章。

booloperator>(const string& s1,const string& s2){returnstrcmp(s1.c_str(), s2.c_str())>0;}booloperator==(const string& s1,const string& s2){returnstrcmp(s1.c_str(), s2.c_str())==0;}booloperator<(const string& s1,const string& s2){return!(s1 == s2 || s1 > s2);}booloperator<=(const string& s1,const string& s2){return s1 < s2 || s1 == s2;}booloperator>=(const string& s1,const string& s2){return!(s1 < s2);}booloperator!=(const string& s1,const string& s2){return!(s1 == s2);}

10. string模拟实现的现代写法

这是两个复制重载,你觉得哪一个更好?

// 注意这个形参没有引用符号 String&operator=(String s){swap(_str, s._str);return*this;} string& string::operator=(const string& s){ string tmp(*this);clear();if(s._size > _capacity){ size_t newcapacity =2* _capacity > s._size ?2* _capacity : s._size;reserve(newcapacity);}strcpy(_str, s._str);return*this;}

实际上从性能的角度分析,第一个写法是不如第二个写法的,但是如果面试中面试官想要你快速实现一个string类的框架,那么第一个写法可以更加快速地实现出来,在这种场景是更优秀的。

现代写法体现的是复用这一思想,可以更快地帮助我们开发,也能相对减少Bug出现的可能,所以除了对性能要求特别高的情况下,都可以使用类似的思想来加快开发。

本文的许多地方已经使用了现代写法,比如赋值操作符重载和比较运算符重载。

11. 补充阅读:写时拷贝

在一些编译器如g++中,有时不会使用深拷贝,而是使用写时拷贝(也称写时才拷贝)。

其原理大致为:

  1. 对于每一个不相关的string对象,其指向的字符数组_str都有一个对应的计数器time
  2. 当发生拷贝时,直接进行浅拷贝,并time++
  3. 当有指向这块空间的string对象析构时,time--,只有在time为0时才会释放这块空间
  4. 如果在析构之前,有对象尝试对这块空间的数据进行修改(写入),就会进行深拷贝,让这个对象指向一块新的空间。

那么通过这个原理我们可以看出,如果是一些string对象指向同一块空间并且都不进行修改的情况,写时拷贝可谓如鱼得水,效率相对我们之前使用的读时拷贝效率高了非常多。

但写时拷贝也有一定的缺陷:C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章

Read more

Qwen3-VL SDK发布:支持Python/Java/C#多语言调用

Qwen3-VL SDK发布:支持Python/Java/C#多语言调用 在智能应用日益依赖“看懂图像、理解语言”的今天,开发者面临一个现实难题:如何让AI真正理解一张截图里的错误提示,并像人类一样给出修复建议?过去这需要组合OCR、目标检测、自然语言模型等多个系统,工程复杂度极高。而现在,随着Qwen3-VL SDK的正式发布,这一切变得像调用一个函数那样简单。 这款新推出的软件开发工具包,首次将通义千问系列最强大的视觉-语言模型以标准化接口形式开放给Python、Java和C#开发者。它不再只是“能识别图片的文字”,而是可以分析界面布局、生成网页代码、执行GUI操作、甚至理解长达数小时的视频内容——所有这些能力,都可以通过几行代码接入现有系统。 多模态智能的进化:从感知到行动 传统视觉-语言模型大多停留在“描述性理解”阶段:输入一张图,输出一段文字说明。但真实世界的应用需求远不止于此。用户希望的是——看到表单就知道怎么填,看到报错就能自动修复,读完文档可以直接生成PPT。这就要求模型不仅“看得懂”,还要“会做事”。 Qwen3-VL正是朝着这个方向迈出的关键一步。

By Ne0inhk
【C++动态规划】1547. 切棍子的最小成本|2116

【C++动态规划】1547. 切棍子的最小成本|2116

本文涉及知识点 C++动态规划 LeetCode1547. 切棍子的最小成本 有一根长度为 n 个单位的木棍,棍上从 0 到 n 标记了若干位置。例如,长度为 6 的棍子可以标记如下: 给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。 你可以按顺序完成切割,也可以根据需要更改切割的顺序。 每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。 返回切棍子的 最小总成本 。 示例 1: 输入:n = 7, cuts = [1,3,4,5] 输出:16 解释:按 [1, 3, 4, 5]

By Ne0inhk
深入理解 C++ 智能指针:原理、使用与避坑指南

深入理解 C++ 智能指针:原理、使用与避坑指南

在 C++ 编程中,内存管理始终是核心挑战之一。手动使用new分配内存后,若因异常、逻辑疏忽等原因未执行delete,就会导致内存泄漏。智能指针作为 C++ 的核心工具,通过 RAII(资源获取即初始化)思想自动管理资源,完美解决了这一痛点。本文将从使用场景、设计原理、标准库实现、常见问题等方面,全面解析智能指针的技术细节。 一、智能指针的核心使用场景 手动管理动态内存时,异常场景下的资源释放问题尤为突出。看下面的示例代码: double Divide(int a, int b) { if (b == 0) throw "Divide by zero condition!"; return (double)a / (double)b; } void Func() { int*

By Ne0inhk
【C++11】可变参数模板/新的类功能/lambda/包装器--C++

【C++11】可变参数模板/新的类功能/lambda/包装器--C++

文章目录 * 一、可变参数模板 * 1、基本语法及原理 * 2、包扩展 * 3、empalce系列接口 * 二、新的类功能 * 1、默认的移动构造和移动赋值 * 2、成员变量声明时给缺省值 * 3、defult和delete * 4、final与override * 三、STL中一些变化 * 四、lambda * 1、lambda表达式语法 * 2、捕捉列表 * 3、lambda的应用 * 4、lambda的原理 * 五、包装器 * 1、function * 2、bind 一、可变参数模板 1、基本语法及原理 1. C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:

By Ne0inhk