C++STL的string模拟实现

文章目录

  • 前言
  • string的成员变量
  • 成员函数
    • 构造函数
    • 拷贝构造
    • 赋值重载
  • 模拟实现string各种接口
    • print
    • 迭代器
      • 普通迭代器
      • const迭代器
    • string比较大小
    • push_back
    • insert 和 erase
      • insert
      • erase
    • reserve和resize
      • reserve
      • resize
    • swap
    • find
    • cout和cin
      • cout
      • cin

前言

今天要讲string的底层实现,通过自己来实现string,我们对string的理解才能更加的深刻。
我们对string其实既熟悉又陌生,熟悉sting其实就是字符串,陌生是在于管理字符串这样一个类。

string的成员变量

namespace but
{class string{private:char* _str;size_t _capaicty;size_t _size;};
}

我们为了避免自己定义的string于库里面的傻傻分不清,这里我们自己用了一个命名空间把自己写的string封装起来。

成员函数

构造函数

namespace but
{class string{public:string():_str(nullptr),_capaicty(0),_size(0){}string(const char* str):_str(str),_capaicty(strlen(str)),_size(strlen(str))//容量不包括'\0'{}private:const char* _str;//加上const,防止写构造函数时,权限放大编译不通过size_t _capaicty;size_t _size;};
}

简简单单写了上面的构造函数,其实这里面存在两个问题,下面我们通过一些使用来看一下。
第一个问题。

写个c_str,思考一下为什么程序会崩?

const char* c_str()
{return _str;
}
string s1;
string s2("hello world");
cout << s1.c_str() << endl;//上述代码都是写在类里面
cout << s2.c_str() << endl;

流插入是自动识别类型,它识别出const char*, 然后去解引用,然后遇到‘\0’结束,这样空指针的问题就暴露出来了。

继续看第二个问题

const char& operator[](size_t pos)//按照之前写的构造函数,必须加上const
{assert(pos < _size);return _str[pos];
}

这里面有个很坑的问题,我们是呆会是要修改pos位置的字符,并且如果空间不够还需要扩容,比如+=;那这里就变得非常矛盾。

这是什么原因呢?

string s2("hello world")

s2在常量区无法修改,扩容也无法扩。

如何解决这两个问题呢?
其实根源还是在于初始化列表,我们大多数情况下都是推荐把所有成员变量直接放到初始化列表初始化,这里比较特殊。
其次,我们要想修改pos位置的字符,还想扩容,在初始化的时候空间就不能直接赋值过去,最好new出来。

那经过修改之后,我们的代码

namespace but
{class string{public:string():_str(new char[1]),//要解决第一个问题,这里就不能是空_capacity(0),_size(0){_str[0] = '\0';}string(const char* str):_capacity(strlen(str)){_size = _capacity;//没有必要重复用strlen,strlen是o(N)的接口_str = new char[_capacity + 1];//扩容的时候应该+1,包括\0strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;_capacity =_size= 0;}const char* c_str(){return _str;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}private:char* _str;//加上const,防止写构造函数时,权限放大编译不通过size_t _capacity;size_t _size;};void test_string1(){string s1;string s2("hello world");cout << s1.c_str() << endl;cout << s2.c_str() << endl;s2[0]++;cout << s2.c_str() << endl;}
}

至此,把上面的问题都解决了。
在这里插入图片描述

拷贝构造还可以继续优化一下,优化成只有一个全缺省的构造函数。

//string(const char* str = nullptr)  //不可以,等下strlen解引用会崩
//string(const char* str = '\0')//不可以,类型不匹配
//string(const char* str = "\0")//可以
string(const char* str = "")//可以:_size(strlen(str))
{_capaicty = _size == 0 ? 3 : _size;_str = new char[_capaicty + 1];strcpy(_str, str);
}

拷贝构造

void test_string2()
{string s2("hello world");string s3(s2);cout << s2.c_str() << endl;cout << s3.c_str() << endl;
}

我们之前说过拷贝构造是默认成员函数,我们不写,编译器会自动生成一个,对自定义类型不做处理,对内置类型做值拷贝或浅拷贝。那我们看一下自动生成的拷贝构造。
在这里插入图片描述
这个是经典的值拷贝或浅拷贝问题,我们之前也讲过,接下来既然有一个具体的场景,就用调试带大家看一下。
在这里插入图片描述
看两个地址完全一摸一样。

这样会带来两个问题。
1.一个修改影响另外一个。
2.同一块空间会析构两次。

那我们需要自己写一个深拷贝的拷贝构造,怎么写呢?

//拷贝构造也有初始化列表
string(const string& s):_size(s._size), _capaicty(s._capaicty){_str = new char[s._capaicty + 1];strcpy(_str, s._str);}

赋值重载

赋值重载和拷贝构造也一摸一样,我们不写的话,编译器自动生成的会出问题。
写成这样,那就考虑的太不全面了

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

我们知道拷贝构造是一块已经存在的空间给另一块还没存在的空间。
而赋值重载是两块都已经存在的空间,所以赋值重载还需要从空间的角度去分析问题。

从空间大小考虑,总共有三种情况
在这里插入图片描述

但是存在一个问题,如果s3空间特别大,s1又非常小,把s1直接赋值过去,s3就会浪费很多空间,所以比较好的方式就是再开一块空间。
我们库里面的string实现不会这么麻烦,直接把旧的空间释放掉,开一块一样大的空间。

还要处理自己给自己赋值,以免造成不必要的麻烦。

string& operator=(const string& s){if (this != &s){//这种写法稍微不好一点//抛异常的时候会把s1给破坏掉/*delete[] _str;_str = new char[s._capaicty + 1];strcpy(_str, s._str);_size = s._size;_capaicty = s._capaicty;*/char* tmp = new char[s._capaicty + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capaicty = s._capaicty;}

模拟实现string各种接口

print

这里为什么报错?
在这里插入图片描述

这也涉及到我们之前讲过的。** cosnt成员变量不能调用非const成员函数,这样会权限放大。**

在这里插入图片描述
紧接着这里报错又怎么解决?
这说明我们需要两个【】,一个是给const对象调用的,不允许修改。
一个是给普通对象调用的,可以修改。它们构成函数重载,因为它们函数名相同参数不一样。
虽然普通对象也可以调用const成员函数,但是编译器非常聪明,他会调用最匹配的哪个。

迭代器

遍历的方式我们还可以用迭代器,这里我们再写一个迭代器

普通迭代器

在这里插入图片描述
要实现一个迭代器其实不难。

我们支持了迭代器,其实也就支持了范围for

for (auto ch : s1)
{cout << ch << " ";
}

const迭代器

const迭代器能不能修改?
可以修改,只是指向的内容不能修改。

string::const_iterator it = s1.begin();
while (it != s1.end())
{//*it = 'x';//不能修改,只能读不能改++it;
}
cout << endl;

反向迭代器这里先不讲,后面再讲,要用一个适配器来实现。

string比较大小

怎样比较大小?
比较ascll值,一个一个比。

// 不修改成员变量数据的函数,最好都加上constbool operator>(const string& s) const{return strcmp(_str, s._str) > 0;}bool operator==(const string& s) const{return strcmp(_str, s._str) == 0;}bool operator>=(const string& s) const{//return *this > s || *this == s;return *this > s || s == *this;}bool operator<(const string& s) const{return !(*this >= s);}bool operator<=(const string& s) const{return !(*this > s);}bool operator!=(const string& s) const{return !(*this == s);}

push_back

空间不够扩容的时候不能用realloc,那就和c++交叉了,容易出问题。

void push_back(char ch)
{if (_size + 1 > _capaicty){reserve(_capaicty * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';
}void append(const char* str)
{size_t len = strlen(str);if (_size+len > _capaicty){reserve(_size + len);}strcpy(_str + _size, str);//strcat(_str, str);//为什么不用strcat?strcat很挫自己要去找\0,\0就在size位置,能不用就不用_size += len;
}

在这里插入图片描述

我们喜欢使用的还是+=,直接复用push_back;

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

凡是你的扩容,析构上代码崩了,一般都是内存问题。

insert 和 erase

问个小小的问题,静态成员变量能不能给缺省值?
不能,因为缺省值是给初始化列表用的。静态列表不是在初始列表初始化的。
它属于整个类,不是属于某个对象。

insert

插入字符
insert有个巨坑给大家看一下下面的代码?
在这里插入图片描述

在这里插入图片描述
程序运行结果。

调试的时候发现这样,扯淡了。
在这里插入图片描述
因为end的类型是size_t;

void insert(size_t pos, char ch)
{assert(pos <= _size);if (_size + 1 > _capacity){reserve(2 * _capacity);}//int end=_size;//这样也不行,会发生类型转换,一般有符号转化为无符号。//改pos也不好,pos的类型一般规定都是size_tsize_t end = _size;//while(end>=pos(int))//强转也不推荐//while (end >= pos)//{//	_str[end + 1] = _str[end];//	--end;//}size_t end = _size + 1;while (end > pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;++_size;
}

我们最好的解决思路,巧妙的避开了小于0;
在这里插入图片描述
在这里插入图片描述

插入字符串

一定要画图,不然很容易出错。

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}// 挪动数据size_t end = _size + len;while (end > pos + len - 1)//强烈不建议用大于等于{_str[end] = _str[end - len];--end;}//这个比较简单,完美避开了循环结束条件的难题/*size_t end = _size;for (size_t i = 0; i < _size + 1; ++i){_str[end + len] = _str[end];--end;}*/// 拷贝插入strncpy(_str + pos, str, len);_size += len;return *this;}

erase

erase比较简单,从pos位置删除数据就可以了。

我们浅浅分析一下所有的情况
在这里插入图片描述

erase也是不考虑缩容的。

string& erase(size_t pos, size_t len = npos)
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);//不需要考虑覆盖的问题,所以可以直接用strcpy_size -= len;}return *this;}

白盒测试,把三种情况都验证一遍
在这里插入图片描述

reserve和resize

reserve

看一下我们之前写的扩容有什么问题?
在这里插入图片描述

它是没有考虑缩容的。继续看这样子就报错了。
在这里插入图片描述
为什么报错呢?
strcp的时候越界了。

简单修改一下代码就变成这样了。

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

resize

resize缩容吗?
不缩容。缩荣的代价还是很大的,首先是异地缩,先开另一块空间,然后把数据拷贝过去,接着把之前的空间释放掉。
待会插入数据空间不够又要扩容,这样就很麻烦。

接下来实现resize,我们得分情况讨论,以及明白resize功能上的一些细节。
在这里插入图片描述

void resize(size_t n, char ch = '\0')
{if (n < _size){// 删除数据--保留前n个_size = n;_str[_size] = '\0';}else if (n > _size){if (n > _capacity){reserve(n);}//如果调用系统的接口,我们可以用memsetsize_t i = _size;while (i < n){_str[i] = ch;++i;}_size = n;_str[_size] = '\0';}
}

swap

我们实现 一下swap,其实就知道库里面的swap和类里面的效率差距有多大

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

find

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

cout和cin

现在我有一个问题,cout和cin必须实现成友元函数?这句话对不对
不对,我们可以写一些函数来访问私有成员变量。

cout

首先,我们实现cout, 它不是成员函数。

能不能直接这样搞?
在这里插入图片描述
我们之前说过c_str()和cout是有区别的,它们最大的区别就是c_str()打印时是遇到\0终止,cout是根据size来打印的。

ostream& operator<<(ostream& out, const string& s)
{for (auto ch : s){out << ch;}return out;
}

cin

在这里插入图片描述
这样为什么不行?
调试一下就知道了,空格和换行不会进入缓冲区。为什么?
它会认为你输入的时候多个字符之间的间隔。

我们可以改成这样
在这里插入图片描述
仔细看一下上面的代码,功能是完善了但还有什么弊端。
在这里插入图片描述

在这里插入图片描述
这是没有把之前的数据清理掉。

还有一个问题有个流插入的数据比较长,那它会影响效率,那有没有什么方法能解决这个问题?
开小了不够,开多了浪费。这里有一个参考方式。
相当于换成字符串,可以这样理解。

istream& operator>>(istream& in, string& s)
{s.clear();char ch = in.get();char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;}ch = in.get();}//防止还有数据没有+=进去if (i != 0){buff[i] = '\0';s += buff;}return in;
}

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

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

相关文章

【夯实技术基本功】「底层技术原理体系」全方位带你认识和透彻领悟正则表达式(Regular Expression)的开发手册(正则符号深入解析 )

[TOC](【夯实技术基本功】「底层技术原理体系」全方位带你认识和透彻领悟正则表达式(Regular Expression)的开发手册&#xff08;正则符号深入解析 &#xff09;) 借鉴官网的速查表 基础匹配符号 反向匹配表 各种操作符的运算优先级 承接上文&#xff0c;在正则表达式中&…

Efficient physics-informed neural networks using hash encoding

论文阅读&#xff1a;Efficient physics-informed neural networks using hash encoding Efficient physics-informed neural networks using hash encoding简介方法PINN哈希编码具有哈希编码的 PINN 实验Burgers 方程Helmholtz 方程N-S 方程训练效率对比 总结 Efficient physi…

try catch 应该在 for 循环里面还是外面?(荣耀典藏版)

大家好&#xff0c;我是月夜枫&#xff0c;今天突然想到一个很有意思的问题&#xff0c; try catch 应该在 for 循环里面还是外面那&#xff1f;欢迎大家积极留言讨论。 一、使用场景 为什么要把 使用场景 摆在第一个 &#xff1f; 因为本身try catch 放在 for循环 外面 和里面…

谷歌Gemini刚发就惹质疑:测试标准有失偏颇,效果视频疑似剪辑

梦晨 克雷西 发自 凹非寺 量子位 | 公众号 QbitAI 谷歌憋了许久的大招&#xff0c;双子座Gemini大模型终于发布&#xff01;其中一图一视频最引人注目&#xff1a; 一图&#xff0c;MMLU多任务语言理解数据集测试&#xff0c;Gemini Ultra不光超越GPT-4&#xff0c;甚至超越了…

「Python编程基础」第6章:字典

文章目录 一、字典数据类型二、细说字典与列表三、重要的3个方法&#xff1a;keys()、values()、items()四、检查字典中是否存在键或值五、get()方法六、清空七、修改八、字典的嵌套字典套字典字典套列表字典套字典套列表 练习题 一、字典数据类型 编程的 「字典」 其实和现实中…

预训练--微调

预训练–微调 一个很简单的道理&#xff0c;如果我们的模型是再ImageNet下训练的&#xff0c;那么这个模型一定是会比较复杂的&#xff0c;意思就是这个模型可以识别到很多种类别的即泛化能力很强&#xff0c;但是如果要它精确的识别是否某种类别&#xff0c;它的表现可能就不…

frp内网穿透部署,轻松实现内网服务对外访问

FRP&#xff08;Fast Reverse Proxy&#xff09;是一种轻量级、高性能的反向代理工具&#xff0c;利用反向代理技术将公网请求转发至内网服务器上&#xff0c;并将内网服务器的响应再次转发至公网请求者。在实现内网穿透时&#xff0c;FRP能够将公网与内网之间的隔离突破&#…

ArkTS快速入门

一、概述 ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风格的基础上&#xff0c;对TS的动态类型特性施加更严格的约束&#xff0c;引入静态类型。同时&#xff0c;提供了声明式UI、状态管理等相应的能力&#xff0c;让开发者可以…

jmeter常用的性能测试监听器

jmeter中提供了很多性能数据的监听器&#xff0c;我们通过监听器可以来分析性能瓶颈 本文以500线程的阶梯加压测试结果来描述图表。 常用监听器 1&#xff1a;Transactions per Second 监听动态TPS&#xff0c;用来分析吞吐量。其中横坐标是运行时间&#xff0c;纵坐标是TPS…

火狐,要完了!

在过去几年中&#xff0c;关于Firefox 浏览器的衰落有过不少讨论。目前来说&#xff0c;很多公共的以及私营的大型网站都缺乏对Firefox的适当支持。但是Firefox也多次试图“自救”&#xff0c;甚至就在不久前&#xff0c;Mozilla 通过官博发文&#xff0c;表示 Firefox 在 2023…

一个 postman实现参数化让我丢掉了一份20k的offer

什么时候会用到参数化 比如&#xff1a;一个模块要用多组不同数据进行测试 验证业务的正确性 Login模块&#xff1a;正确的用户名&#xff0c;密码 成功&#xff1b;错误的用户名&#xff0c;正确的密码 失败 postman实现参数化 在实际的接口测试中&#xff0c;部分参数…

设计并实现一个多线程图书馆管理系统,涉及数据库操作

没有实现全部功能&#xff0c;希望路过的大佬&#xff0c;可以实现全部功能&#xff0c;在评论区聊聊 创建数据库library-demo CREATE DATABASE library-demo创建图书表book CREATE TABLE book (bookId int(11) NOT NULL AUTO_INCREMENT COMMENT 图书ID,bookName varchar(15)…