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

news2024/11/12 18:48:09

目录

问题一:关于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;
	else
		cout << "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\0
	cout << b.capacity() << endl;
	cout << b.size() << endl;

	if (b[3] == '\0')
		cout << "yes" << endl;
	else
		cout << "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.coloradmin.cn/o/1914915.html

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

相关文章

最后纪元Last Epoch可以通过什么搬砖 游戏搬砖教程

来喽来喽&#xff0c;最后纪元&#xff0c;一款《最后纪元》是一款以获得战利品为基础的暗黑风格动作RPG游戏&#xff0c;玩家将从2281年的毁灭时代追溯到由女神Eterra创造的世界&#xff0c;通过多个时代与黑暗的命运对抗&#xff0c;找到拯救世界的方式。游戏有五种职业&…

AI赛道成功的“小”AI平台,都在做什么?

在深入了解30多家跨界拓展AI赛道业务的企业后&#xff0c;我们发现大家对目前的AI市场存在一定程度的误解&#xff1a;即认为在AI领域想要分一杯羹&#xff0c;只需要搞几个API&#xff0c;把大语言模型、绘画、视频、数字人等功能都放上去&#xff0c;可能就有机会占一席之地了…

20240711 每日AI必读资讯

&#x1f3a8;Runway Gen-3 Alpha 详细使用教程以及提示词指南大全 - 7月9日&#xff0c;著名生成式AI平台Runway在官网公布了&#xff0c;最新发布的文生视频模型Gen-3 Alpha的文本提示教程。 - 从技术层面来说&#xff0c;输入的文本提示会被转换成“向量”&#xff0c;这些…

数据开源 | Magic Data大模型高质量十万轮对话数据集

能够自然的与人类进行聊天交谈&#xff0c;是现今的大语言模型 (LLM) 区别于传统语言模型的重要能力之一&#xff0c;近日OpenAI推出的GPT-4o给我们展示了这样的可能性。 对话于人类来说是与生俱来的&#xff0c;但构建具备对话能力的大模型是一项不小的挑战&#xff0c;收集高…

Python实现的深度学习技术在水文水质领域应用

当前&#xff0c;深度学习作为人工智能的热门技术发展迅速&#xff0c;以其强大的非线性和不确定性处理能力在图像识别、语音识别、自然语言处理等领域取得了显著的成效。它是一种端到端的处理方法&#xff0c;在训练算法的指导下&#xff0c;深层神经网络自发地从原始数据中进…

JAVASE进阶day08(Map双列集合)

HashMap 1.HashMap基本使用 package com.lu.day08.map;import java.util.HashMap; import java.util.Map; import java.util.Set;public class MapDome {public static void main(String[] args) {HashMap<String , String> map new HashMap<>();//添加后者修改-…

Data Guard高级玩法:failover备库后,通过闪回恢复DG备库

作者介绍&#xff1a;老苏&#xff0c;10余年DBA工作运维经验&#xff0c;擅长Oracle、MySQL、PG、Mongodb数据库运维&#xff08;如安装迁移&#xff0c;性能优化、故障应急处理等&#xff09; 公众号&#xff1a;老苏畅谈运维 欢迎关注本人公众号&#xff0c;更多精彩与您分享…

动手学深度学习(Pytorch版)代码实践 -循环神经网络-57长短期记忆网络(LSTM)

57长短期记忆网络&#xff08;LSTM&#xff09; 1.LSTM原理 LSTM是专为解决标准RNN的长时依赖问题而设计的。标准RNN在训练过程中&#xff0c;随着时间步的增加&#xff0c;梯度可能会消失或爆炸&#xff0c;导致模型难以学习和记忆长时间间隔的信息。LSTM通过引入一组称为门…

rk3588s 定制版 tc358775 调试 lvds 屏幕 (第一部分)

硬件: 3588s 没有 lvds 接口 , 所以使用的 东芝的 tc358774 (mipi ---> lvds芯片), 这个芯片是参考 3399 的 官方设计得来的,3399 的官方demo 板上应该是 使用到了 这颗芯片 参考资料: 1 网上的 GM8775C 转换芯片。 2 瑞芯微的 3588s 的资料 总体的逻辑: 1 3588s…

25届近5年中国民航大学自动化考研院校分析

中国民航大学 目录 一、学校学院专业简介 二、考试科目指定教材 三、近5年考研分数情况 四、近5年招生录取情况 五、最新一年分数段图表 六、初试大纲复试大纲 七、学费&奖学金&就业方向 一、学校学院专业简介 二、考试科目指定教材 1、考试科目介绍 2、指定教…

centos系统查找mysql的配置文件位置

执行命令查找mysql的安装目录&#xff1a; which mysql cd进入mysql的安装目录 cd /usr/bin 查找配置文件位置 ./mysql --help | grep "my.cnf" 定位配置文件 cd /etc 查找命令还可以用find命令 find / -name "my.cnf"

Docker 部署 ShardingSphere-Proxy 数据库中间件

文章目录 Github官网文档ShardingSphere-Proxymysql-connector-java 驱动下载conf 配置global.yamldatabase-sharding.yamldockerdocker-compose.yml Apache ShardingSphere 是一款分布式的数据库生态系统&#xff0c; 可以将任意数据库转换为分布式数据库&#xff0c;并通过数…

绿盟培训入侵排查

一、webshell 排查 1、文件特征 2、windows 3、linux 4、内存马 二、web 日志排查 1、日志排查 2、中间件报错排查 三、服务器失陷处置

Linux常用选项和指令

目录 Linux指令使用注意 用户创建与删除 ls指令 ls指令介绍 ls常见选项 ls选项组合使用 pwd指令 Linux文件系统结构 多叉树结构文件系统介绍 多叉树结构文件系统的特点 cd指令 绝对路径 相对路径 cd指令介绍 家户目录 最近访问的目录 touch指令 ​编辑mkdir指…

【HarmonyOS NEXT】鸿蒙 代码混淆

代码混淆简介 针对工程源码的混淆可以降低工程被破解攻击的风险&#xff0c;缩短代码的类与成员的名称&#xff0c;减小应用的大小。 DevEco Studio提供代码混淆的能力并默认开启&#xff0c;API 10及以上版本的Stage模型、编译模式为release时自动进行代码混淆。 使用约束 …

【中项第三版】系统集成项目管理工程师 | 第 10 章 启动过程组

前言 第10章对应的内容选择题和案例分析都会进行考查&#xff0c;这一章节属于10大管理的内容&#xff0c;学习要以教材为准。本章上午题分值预计在2分。 目录 10.1 制定项目章程 10.1.1 主要输入 10.1.2 主要输出 10.2 识别干系人 10.2.1 主要输入 10.2.2 主要工具与技…

解决:WPS,在一个表格中,按多次换行,无法换到下一页

现象&#xff1a;在一个表格里面&#xff0c;多次按下回车&#xff0c;始终无法到下一页 解决方法&#xff1a;右击—>表格属性—>选择行—>勾选 允许跨页断行 效果演示 对比展示

vulnhub-NOOB-1

确认靶机 扫描靶机发现ftp Anonymous 的A大小写都可以 查看文件 解密 登录网页 点击about us会下载一个压缩包 使用工具提取 steghide info 目标文件 //查看隐藏信息 steghide extract -sf 目标文件 //提取隐藏的文件 steghide embed -cf 隐藏信息的文件 -ef…

【AI大模型新型智算中心技术体系深度分析 2024】

文末有福利&#xff01; ChatGPT 系 列 大 模 型 的 发 布&#xff0c; 不 仅 引 爆 全 球 科 技 圈&#xff0c; 更 加 夯 实 了 人 工 智 能&#xff08;Artificial Intelligence, AI&#xff09;在未来改变人类生产生活方式、引发社会文明和竞争力代际跃迁的战略性地位。当…

CephFS文件系统存储服务

目录 1.创建 CephFS 文件系统 MDS 接口 服务端操作 1.1 在管理节点创建 mds 服务 1.2 创建存储池&#xff0c;启用 ceph 文件系统 1.3 查看mds状态&#xff0c;一个up&#xff0c;其余两个待命&#xff0c;目前的工作的是node02上的mds服务 1.4 创建用户 客户端操作 1.5…