关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论

目录

问题一:关于string的''\0''问题讨论

问题二:C++标准库中的string内存是分配在堆上面吗?

问题三:string与vector的capacity大小设计的特点

问题四:string的流提取问题

问题五:迭代器失效

 问题六:Vector 最大 最小值 索引 位置

问题7:反迭代器的实现(包含迭代器类的介绍)


前言:

前几篇文章我们已经介绍完了string,vector,list的使用与string的使用原理,但是仅仅知道这些对于我们日常使用来说已经够了,但是在我们日常使用的时候,不免会有报错与相关的疑惑,那么这里我介绍几个我认为有问题的地方,后续有问题的话,还会继续补充。

问题一:关于string的''\0''问题讨论

之前在某篇文章中看到,C语言字符串是以'\0'结尾的,但是C++string类型的字符串并不是以'\0'结尾。话不多说,直接放代码(vsX86环境):

#include<iostream>
#include<string>
using namespace std;
int main()
{string b("abc");cout << b.capacity() << endl;cout << b.size() << endl;if (b[3] == '\0')cout << "yes" << endl;elsecout << "no" << endl;return 0;
}

运行结果:

.

可以看到我们创建的这个string,他的容器大小为15,这个string存储大小为3,但是我们却可以通过越界访问  b[3]   ,并且通过验证字符串的结尾就是'\0'。此时我的内心是疑惑的,心想"abc"是C语言风格的字符串给b构造,肯定会把"abc"后面影藏的'\0'给构造进去,如果不会这样就会在迭代器里面不会遇见结束表示符。那么至于这里的结尾的最后一个'\0',从结果来说是大小size不计算的,所以大小size是3。

但是我们又尝试别的构造的话又会尝试别的疑惑,比如这个代码:

#include<iostream>
#include<string>
using namespace std;
int main()
{string b("abcd",3);//这种构造方法是通过字符串abcd,然后只取前3个字符进行构造string//但是这个字符串存放的其实是 abcd\0cout << b.capacity() << endl;cout << b.size() << endl;if (b[3] == '\0')cout << "yes" << endl;elsecout << "no" << endl;return 0;
}

结果跟上面一模一样。此刻我又想,构造函数会在末尾自动添加一个'\0',并且size和capacity函数都不计算'\0'的。

但是我们一开始是假设他跟c语言的风格相似的会把abc后面的'\0'会自动添加上,但是我们这个代码是只取了abcd\0这个字符串的前三个,没有'\0'啊~!

所以此刻,我肯定是矛盾的!!因为最开始说string字符串是不以'\0'结尾的,但是测试下来,确实是以'\0'结尾的。

哎呀~为什么呢?经过查阅资料后,才得知了其中的奥妙,奥妙如下:

std::string:标准中未明确规定需要\0作为字符串结尾。编译器在实现时既可以在结尾加\0,也可以不加。(因编译器不同,就比如vs就不用)

但是,当通过c_str()或data()(二者在 C++11 及以后是等价的)来把std::string转换为const char *时,会发现最后一个字符是\0。但是C++11,string字符串都是以'\0'结尾(这也是c++祖师爷为以前的自己的规定的优化)。



为什么C语言风格的字符串要以'\0'结尾,C++可以不要?

c语言用char*指针作为字符串时,在读取字符串时需要一个特殊字符0来标记指针的结束位置,也就是通常认为的字符串结束标记。而c++语言则是面向对象的,长度信息直接被存储在了对象的成员中,读取字符串可以直接根据这个长度来读取,所以就没必要需要结束标记了。而且结束标记也不利于读取字符串中夹杂0字符的字符串。



这里我们深入一下string的构造时的细节:

#include<iostream>
#include<string>
using namespace std;
int main()
{int aa = 0;printf("栈区的地址:%p\n", &aa);int* pl = new int;printf("堆区的地址:%p\n", pl);string a("abcddddddddddddddddddddddddd", 20);printf("a的地址:    %p\n", &a);printf("a[0]的地址: %p\n", &a[0]);a[1] = 'X';cout << a << endl;printf("a的地址:    %p\n", &a);printf("a[0]的地址: %p\n", &a[0]);string b("abc");printf("b的地址:    %p\n", &b);printf("b[0]的地址: %p\n", &b[0]);return 0;
}

然后通过运行的知,

用红色标注出来的是在栈上存储的,蓝色标注的时在堆上存储的,然而a,b就与指针类似,他们指向一片空间,空间内存储的对象信息, 对象地址分别是006FF6AC与006FF688,他俩的地址跟栈区地址最为接近所以该对象存储在栈区上。同理a[0]是堆区上,但是b[0]按道理也应该是在堆区上,但是为什么会是是在栈区上呢?其实这是c++的一个特殊处理,这里留下一个小疑问,(下一个问题进行解答,这里先给出为什么的答案:当string内存存储的个数在16以内(包括'\0')(后面解释为什么是16)在栈上,超过以后在堆上。)

所以,string在构造函数的时候,会在堆上开辟一块内存存放字符串,并且指向这块字符串。

(这里给大家提问一个小问题:就是为什么a先定义的,但是a对象地址为什么比b的大?)

解答:a、b是两个局部对象变量,栈是向下增长的,所以先入栈的变量地址高,即&a > &b,



问题二:C++标准库中的string内存是分配在堆上面吗?

例如我声明一个string变量。
string str;
一直不停的str.append("xxxxx");时,str会不停的增长。

我想问的是这个内存的增长,标准库中的string会把内存放置到堆上吗?

另外STL中的其他容器是否遵循相同的规则。

首先我们给出结论:16以内在栈上,超过以后在堆上。(这句话的答案省略上面的问题的前提条件:【在栈上构造的 string 对象】,如果string 是 new 出来的即在堆上构造的,当然内部的缓冲区总是在堆上的)。(vector也是如此,但是细节上略有不同)

为什么要这样做呢?

如果以动态增长来解释就是:

因为栈通常是一种具有固定大小的数据结构,如数组实现的栈在创建时会指定一个固定的容量。因此,一般情况下,栈是不支持动态增长的。 

所以是存储在堆上的。

其实还有另一个原因,那么下一个问题给出解答;

问题三:string与vector的capacity大小设计的特点

在我们设计string与vector的时候,你是否观察过他的capacity的大小呢?就比如vs里面为什么会让string与vector在其存储的内存个数小于16时会将数据存储在栈上,大于16存储在堆上呢?

这是因为string与vector第一次会在栈上开辟空间,直接开辟16个单位空间,然后挨个进行流提取,这样的话就会方便很多 ,就算要再添加数据,也不需要进行动态增长,然后这个16个单位空间就是string与vector的capacity。这里的证明可以通过调试自己查看他的capacity,当然编译器不同,可能这个首次开辟空间大小略有不同,但是不影响。

总的来说这两种解释都是解决的次要问题,他这样设计主要为了解决内存碎片的问题;如果存储的内容大小小于16,他就会先存在栈上的数组里面,当大于16,就会进行拷贝到堆上,然后栈上的数组就会进行浪费,这样达到了利用空间换时间的效果

问题四:string的流提取问题

首先如果我们自己实现string的流提取,我们会下意识认为会挨个提取输入的字符,然后挨个与s进行对接,代码试下如下: (这个代码实现的流提取是完全没有问题的)

istream& operator>>(istream& in, string& s)
{s.clear();char ch;ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

但是这样写会有一个弊端,就是会多次进行扩容,俗话常说:扩容本身就是一件麻烦的时,浅拷贝就不多说了,深拷贝就更麻烦了;

所以后来就进行了优化,会先开辟一个数组,然后将流提取的字符挨个放到数组里面,当数组满的时候(或者流提取的字符提取完了)我们当让s+=数组;这样既保证了存储的数据在堆上,也避免了多次进行扩容;(需要注意的是我们要自己添加 '\0' 在string的末尾)

	istream& operator>>(istream& in, string& s){s.clear();char buff[129];size_t i = 0;char ch;//in >> ch;ch = in.get();s.reserve(128);while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;}//in >> ch;ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}

当然这上面的两个问题都是存在于string于vector上的,因为他们存储的数据是连续的,二list作为链表就不存在这样的问题。 

问题五:迭代器失效

然而迭代器失效就不一样了,string,vector,list都存在。

在我们使用迭代器进行遍历的时候,不免会出现不正当的使用而使其迭代器失效;

失效的主要原因就是:迭代器对应的指针所指向的空间已经被销毁了,而使用一块已经被释放的空间的时候,就会造成程序崩溃(即如果继续使用已经失效的迭代器, 程序可能会崩溃)。俗话来说就是野指针了。

前面我们都在用string来进行解释,这里我们使用vector来解释,

1

就比如下面这个代码:

include<iostream>
#include<vector>
using namespace std;int main()
{vector<int> v(10, 1);auto it = v.begin();v.insert(it, 0);(*it)++;return 0;
}

看起来没有问题,但是我们是先给迭代器赋值,然后进行插入,但是有一点问题就是如果插入时恰好进行扩容,并且时异地扩容,那么这个it就会变为野指针。从而达到迭代器失效的问题。

2

同样插入存在异地扩容,当然删除也存在着迭代器失效的问题;

#include<iostream>
#include<vector>
using namespace std;int main()
{vector<int> v(10, 1);auto it = v.end() - 1;v.erase(it);(*it)++;return 0;
}

这时候如果再进行使用it,那么就会报错。

注意:

  1. vs 对于迭代器失效检查很严格,如使用了 erase 之后,之前的迭代器就不允许使用,只有重新给迭代器赋值,才可以继续使用
  2. Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

 问题六:Vector 最大 最小值 索引 位置

#include<iostream>
#include<vector>
using namespace std;int main()
{vector<double> v{ 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0 };vector<double>::iterator biggest = max_element(begin(v), end(v));cout << "Max element is " << *biggest << " at position " << distance(begin(v), biggest) << endl;auto smallest = min_element(begin(v), end(v));cout << "min element is " << *smallest << " at position " << distance(begin(v), smallest) << endl;return 0;
}

运行结果:

问题7:反迭代器的实现

在上一篇文章中的list的迭代器是没有进行实现的,关于list的迭代器他的实现还是有点特殊的地方; 

迭代器类存在的意义

之前模拟实现string和vector时都没有说要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?

因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针。

然而对于list的来说,他的每个结点的存储都不是连续的,是随机的,不可以像string,vector那样仅仅通过与简单的自增,自减以及进行解引用等操作对相应的结点做操作。 

而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。

既然list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。就比如,当你使用list当中的迭代器进行自增操作时,实际上执行了p = p->next语句,只是你不知道而已,这一步迭代器替你进行了复杂的操作,这样就可以在各种操作上进行了统一。

总结: list迭代器类,实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样。(例如,对结点指针自增就能指向下一个结点)

迭代器类的模板参数说明 

查阅相关std源文件库里面的设计,发现迭代器类的模板参数的设计为3个。

template<class T, class Ref, class Ptr>

这里就引发出来思考为什么要这样设计呢?

在list的模拟实现当中,我们typedef了两个迭代器类型,普通迭代器和const迭代器。

typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;

 这里我们就可以看出,迭代器类的模板参数列表当中的Ref和Ptr分别代表的是引用类型和指针类型

 当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象。

若该迭代器类不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器。(换句话来说,按照与string与vector的思路来写list的const与非const迭代器再使用的时候会报错,编译器不知道走那个迭代器)

那么就再前面文章的基础上加上迭代器类吧。

template <class T>
struct list_node
{T _data;list_node<T>* _prev;list_node<T>* _next;list_node(const T& x = T()):_data(x), _prev(nullptr), _next(nullptr){}
};
template<class T, class Ref, class Ptr>
struct __list_iterator 
{typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;		__list_iterator(Node* node):_node(node){}
}
class list
{typedef list_node<T> Node;
public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}iterator begin(){return _head->_next;}iterator end(){return _head;}
private:Node* _head;size_t _size;
};

 我们们迭代器类的构造函数就是用我们传的结点参数来进行初始化。

 运算符重载需要注意要返回self就行

self是当前迭代器对象的类型:

介绍完迭代器类,下面就介绍反迭代器是怎么实现的吧;

同样反迭代器我们也需要设计一个反迭代器类;

但是反迭代器的实现由于正向迭代器实现的思路又有所不一样

其中他的成员变量是正向迭代器

大致如图所示:

template<class Iterator, class Ref, class Ptr>
class Reserve_iterator
{typedef Reserve_iterator<Iterator, Ref, Ptr> Self;public:Reserve_iterator(Iterator it):_it(it){}
private:Iterator _it;
};

 同样他与正向迭代器一样,为了方便会进行typedef

rbegin与rend 

rbegin是其实是返回的end,rend其实是返回的begin,弄清楚这一点就比较好说了,只需要将begin传到反迭代器类的rendend传到反迭代器类的rbegin就可以了;

reserve_iterator rbegin()
{return reserve_iterator(end());
}
reserve_iterator rend()
{return reserve_iterator(begin());
}
const_reserve_iterator rbegin() const
{return const_reserve_iterator(end());
}
const_reserve_iterator rend() const
{return const_reserve_iterator(begin());
}
 operator++

对于反迭代器的++其实对应的就是正向迭代器的--

所以在实现的时候只需要进行减减就可以

	Self& operator++(){--_it;return *this;}

 这里返回的是引用其实很好理解,因为这里的++产生的效果是前置++,所以直接在原来的基础上进行操作就可以,返回进行返回引用;

 operator--

同样还有--,对应的也是正向迭代器的++,还是返回引用就可以

	Self& operator--(){++_it;return *this;}

 

这里就不一样了,一个是返回的ref一个是ptr,这是因为我们在开始的情况下 ,就将ref为引用,ptr为解引用。



到这里就完了,写作不易还请点赞;

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

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

相关文章

个人开发实现AI套壳网站快速搭建(Vue+elementUI+SpringBoot)

目录 一、效果展示 二、项目概述 三、手把手快速搭建实现本项目 3.1 前端实现 3.2 后端方向 五、后续开发计划 一、效果展示 默认展示 一般对话展示&#xff1a; 代码对话展示&#xff1a; 二、项目概述 本项目是一个基于Web的智能对话服务平台&#xff0c;通过后端与第…

【C语言】指针(4):深入理解指针

目录 ​编辑 一、回调函数 二、qsort使用举例 2.1 使用qsort排序整型数据 2.2 使用qsort排序结构体数据 三、qsort的模拟实现 四、NULL、\0、0、0、null、NUL的区别 五、C99中的变长数组 一、回调函数 函数指针是将函数的地址取出来&#xff0c;再通过函数地址去调用&a…

untiy 在菜单栏添加自定义按钮 点击按钮弹出一个Unity窗口,并在窗口里添加属性

using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Rendering.PostProcessing;public class AutoGenerateWindow : EditorWindow //这是定义一个窗口 {public string subjecttName "科目名字";//科目的名字public GameOb…

url链接地址,#前的参数 和 #后的参数有什么区别

例如 http://localhost:8080/?beforeParams1#/workSchemelist/index?afterParams1 beforeParams 和 afterParams 区别 打印出来可以发现&#xff1a; beforeParams 是 url 的search参数&#xff0c;通过window.location.search获取 afterParams 是 route 的query参数&#…

外贸网站设计的要点

外贸网站设计是一种专门针对国际贸易领域的网站设计&#xff0c;需要考虑到不同国家和文化背景的用户&#xff0c;因此设计过程要更加细致和精准。以下是外贸网站设计的关键要点&#xff1a; 首先&#xff0c;多语言支持是不可或缺的&#xff0c;因为外贸网站的用户可能来自不同…

[Python自动化办公]--从网页登录网易邮箱进行邮件搜索并下载邮件附件

[Python自动化办公]–从网页登录网易邮箱进行邮件搜索并下载邮件附件 使用说明 ​ 本文使用Python的selenium库进行操作邮箱登录、固定名称搜索邮件并下载附件&#xff0c;Python版本&#xff1a;3.9.16, selenium版本&#xff1a;4.19.0&#xff0c;EdgeBrowser版本:126.0.2…

LVS集群及其它的NAT模式

1.lvs集群作用&#xff1a;是linux的内核层面实现负载均衡的软件&#xff1b;将多个后端服务器组成一个高可用、高性能的服务器的集群&#xff0c;通过负载均衡的算法将客户端的请求分发到后端的服务器上&#xff0c;通过这种方式实现高可用和负载均衡。 2.集群和分布式&#…

用户增长 - 私域 - 社群运营自检清单SOP(社群运营30问)

Check List: 1.你的目标用户是谁&#xff1f; 2.你的目标用户有哪些需要立马解决的需求&#xff1f;有哪些长期需求&#xff1f;这些需求的优先级是什么&#xff1f; 3.做社群的目的是什么&#xff1f; 4.你的用户和业务是否适合做社群&#xff1f; 5.你做哪类社群才能更好的帮…

确定适合您需求的负载组

大多数关键任务行业都使用 UPS 和发电机等备用电源在停电期间为其设施提供持续电力。负载组允许您在需要时测试电源&#xff0c;以确保在您最需要的时候提供可靠的电力。 选择正确的负载组对于准确的电源测试至关重要。为了帮助您找到最适合您设施需求的负载组&#xff0c;EAK…

【机器学习】主成分分析(PCA):数据降维的艺术

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 主成分分析&#xff08;PCA&#xff09;&#xff1a;数据降维的艺术引言PCA的基…

技术成神之路:设计模式(四)工厂方法模式

1.定义 工厂方法模式&#xff08;Factory Method Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一种创建对象的接口&#xff0c;而不是通过具体类来实例化对象。工厂方法模式的主要作用是让子类决定实例化哪一个类&#xff0c;从而实现对象创建的延迟到具体子类…

2024年6月国产数据库大事记-墨天轮

本文为墨天轮社区整理的2024年6月国产数据库大事件和重要产品发布消息。 目录 2024年6月国产数据库大事记 TOP102024年6月国产数据库大事记&#xff08;时间线&#xff09;产品/版本发布兼容认证代表厂商大事记厂商活动相关资料 2024年6月国产数据库大事记 TOP10 2024年6月国…

【Python】已解决:SyntaxError invalid syntax

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决&#xff1a;SyntaxError invalid syntax 一、分析问题背景 在Python编程中&#xff0c;SyntaxError: invalid syntax是一个常见的错误&#xff0c;它通常表示代码中存在语法…

案例|水上水下一体化测量,为九寨沟精准把脉

​ 九寨沟&#xff0c;被誉为“人间仙境”&#xff0c;其湖群以独特的地理位置和优美的自然景观吸引着世界各地的游客&#xff0c;更是九寨沟生态系统中不可或缺的重要组成部分。因此&#xff0c;精准地掌握湖群的地形数据、水体分布及变化情况&#xff0c;能够揭示水下生态系…

【数据结构与算法基础】算法复杂度

欢迎光顾我的homepage 前言 算法就是定义良好的计算过程&#xff0c;它取一个活一组的值输入&#xff0c;并产生出一个或一组值作为输出。简单来说&#xff0c;算法就是一系列的计算步骤&#xff0c;用来将输入数据转化成输出结果。 一、算法效率 如何去衡量一个算法的好坏&am…

[C++]——同步异步日志系统(3)

同步异步日志系统 一、日志系统框架设计1.1模块划分1.1.1 日志等级模块1.1.2 日志消息模块1.1.3 日志消息格式化模块1.1.4 日志落地模块&#xff08;日志落地的方向是工厂模式&#xff09;1.1.5 日志器模块&#xff08;日志器的生成是建造者模式&#xff09;1.1.6 异步线程模块…

强化学习总结(有具体代码实现)

文章目录 第一部分 强化学习基础第1章 强化学习概述1.1 强化学习概念1.2 强化学习的环境1.3 强化学习的目标1.4 强化学习的数据 第2章 多臂老虎机问题&#xff08;MAB问题&#xff09;2.1 问题描述2.1.1 问题定义2.1.2 形式化描述2.1.3 累积懊悔2.1.4 估计期望奖励 2.2 解决方法…

【机器学习】必会数学知识:一文掌握数据科学核心数学知识点(上),值得收藏~

核心数学知识点 1、引言2、数据科学必会数学知识2.1 线性代数2.2 微积分2.3 概率论2.4 数理统计2.5 随机过程2.6 数据分布2.7 贝叶斯统计2.8 线性回归2.9 逻辑回归2.10 矩阵分解2.11 主成分分析&#xff08;PCA&#xff09;2.12 奇异值分解&#xff08;SVD&#xff09; 3、总结…

【人工智能大语言模型技术发展研究报告 2024】

文末‍有福利&#xff01; 人工智能作为引领新一轮科技产业革命的战略性技术和新质生产力重要驱动力&#xff0c;正在引发经济、社会、文化等领域的变革和重塑&#xff0c;2023 年以来&#xff0c;以 ChatGPT、GPT-4 为代表的大模型技术的出台&#xff0c;因其强大的内容生成及…

提升教师健康,聚焦智慧校园人事系统的职工体检功能

智慧校园人事管理系统内置的职工体检管理&#xff0c;是专为教职员工设计的一项健康管理创新实践&#xff0c;巧妙融合先进信息技术&#xff0c;致力于为教职工提供更加便捷、易懂且持续性的健康检查与管理支持。该服务从多个维度出发&#xff0c;全面呵护教职工的身心健康。 该…