C++ string类介绍 - string的迭代器 - 范围for_c++string迭代器-程序员宅基地

技术标签: C++  c++  java  开发语言  

目录

string类

string类的构造函数

 string类对象的访问及遍历操作

 operator [] 

 关于 string 当中的容量的函数

 迭代器

 范围for


string类

 在C语言当中 ,也有字符串,它是以 " \0 " 结尾 的 一些字符的集合,在C的标准库当中还有一些 用于操作 str 类型的库函数,但是,这些函数的功能不是很全面,而且这些操作函数和 str 类型是分开的,不符合 OPP 思想,如果用户对 这些操作函数使用不当的话,可能会有访问越界,或者是 实现不了 想要效果的这些问题。

所以在C++ 当中,对这些 进行了优化,包装,改进,做出了 string类。

string类就是 管理的 字符数组,里面包含了 增删查改 和 一些 功能算法。

string类的文档:

https://cplusplus.com/reference/string/string/?kw=stringicon-default.png?t=N7T8https://cplusplus.com/reference/string/string/?kw=string

 我们可以在官方文档当中使用 ctrl + F 来搜索当前浏览器当中的内容。

 string类属于C++ 标准库当中的 内容,所以是包含在 std 命名空间当中的,用的时候,要么展开命名空间,要么使用 std::string 的方式访问这个string的内容:

string s1;
std::string s2;

string name("张三");
name = "张飞";

string类的构造函数

string();
创建一个空的string对象,也就是创建一个空的字符串
string (const char* s);
以 C 当中 str 的方式 创建这个 string对象,最后以 " \0 " 结尾。
string (size_t n, char c);
string类当中有 n 个 c 这个字符。
string (const string& str);
拷贝构造函数
string (const string& str, size_t pos, size_t len = npos);
拷贝构造函数,从某一位置开始(pos),拷贝 len 个字符。
string (const char* s, size_t n);
构造一个string 类 ,在 s 常量字符串 的前n 个字符中拷贝
int main()
{
	string s1();
	string s2("张三");
	string s3("hello world");
	string s4(10 , '$');
	string s5(s2);

	string s6(s3, 2, 5);

	return 0;
}

上述的这个拷贝构造函数当中 string (const string& str, size_t pos, size_t len = npos);   npos的值是-1,但是npos 是一个 静态的常量,无符号数,它给的初始值是 -1 ,也是 size_t  ,是无符号数,那么-1 就是最大的数。

那么在官方文档中也有说明:

 表示,如果 给的 len 大于 字符串长度,或者是 此时的 len = npos ;那么都取到字符串的结尾。

 string类对象的访问及遍历操作

当然,在string类当中,还实现了很多的重载符函数,比如 = <   <=  >=  == 等等操作符,基本实现的功能都差不多,但是在Stringe类当中有一个 很厉害的重载符函数(operator[] )。

 operator [] 

 operator[] 在C当中 " [] " 这个操作符相当于是 解引用操作,只有在连续的空间当中才能使用这个 " [] " 这个操作符,比如在栈上开辟的数组和 堆上动态开辟的空间。

那么在自定义类型string 类当中,我们也可以使用 " [] " 来访问这个字符串数组。

使用 下标 + []  的方式来访问string自定义类型。

	string s3("hello world");
	// 直接打s3 当中的内容
	cout << s3 << endl;

	// 下标 + []
	for (int i = 0; i < s3.size(); i++)
	{
		cout << s3[i];
	}
	cout << endl;

	return 0;

输出:

 在string 类当中,底层是使用一个 在堆上开辟的数组来存储 字符串的,那么既然是字符串,就是以 " \0 " 结尾的,我们上述的 循环能不能访问到 "\0"呢?

我们先来看看 size()函数计算出的大小是多少:

	cout << s3.size() << endl;

输出:
 

 我们发现输出的是 11 ,而上述的 "hello world" 的字符串个数就是11 个 ,不加 "\0" 。

因为 "\0" 不是有效字符,他是表示字符串结束的特殊字符。

当我们故意打印出 "\0" 实际上是可以访问到的,但是有些编译器不会显示这个 "\0" ,但是实际上是会访问到的。

那么我们同样可以像使用 " [] " 修改数组一样,对 string 当中的字符串进行修改,因为这个 " operator [] "  函数返回的是 当前 传入的 pos 位置的引用:

 当我们传入的是 普通对象的时候,就是 POS 位置的引用,如果传入的这个对象都是 const 的,那么这个函数的返回值就是  const char&  ,常量引用。

	string s3("hello world");
	char s1[] = "hello world";

	s1[1]++;  // 等价于  *(s1 + 1);
	s3[1]++;  // 等价于 s3.operator[](1);

 上述代码的反汇编如下:

 我们发现,在s3 这个对象当中使用 s3[1]++; 这样的实现,在底层其实就是 调用的 operator[] 这个重载运算符函数。

 关于 string 当中的容量的函数

函数名称 功能说明
size(重点) 返回字符串有效字符长度
length 返回字符串有效字符长度
capacity 返回空间总大小
empty (重点) 检测字符串释放为空串,是返回true,否则返回false
clear (重点) 清空有效字符
reserve (重点) 为字符串预留空间**
resize (重点) 将有效字符的个数改成n个,多出的空间用字符c填充

 max_size() 计算这个字符串最大可以达到多大的空间:

 但是,max_size ( )计算的值在不同的编译器下的结果是不同的,比如上述结果是在 VS2022 环境下所 输出的结果。

但是如果实在 VS2013 环境下输出如下:

 所以 max_size ()函数在实际当中没有多大是使用意义。

capacity()返回这个空间的容量:

	string s3("hello world");

	cout << s3.capacity() << endl; // 15

 当然,在数据结构当中我们知道,比如在栈当中,当这个栈满的时候,我们插入元素,那么就会就扩容,而一般我们是扩到原来大小的两倍,但是这个扩容大小在不同的版本的C++之下,有所不同。

在 VS2019 下:

reserve  保留(直接开辟空间)

我们可以使用这个函数,对这个对象直接开辟 n 个大小的空间,如下例子:

int main()
{
	string s1;
	s1.reserve(100);
	size_t sz = s1.capacity();
	cout << "capacity = " << sz << endl;


	cout << "making s grow:" << sz << endl;
	for (int i = 0; i < 100; i++)
	{
		s1.push_back('c');
		if (sz != s1.capacity())
		{
			sz = s1.capacity();
			cout << "capacity change: " << sz << endl;
		}
	}

	return 0;
}

 输出:

 我们发现,在后续插入 'c' 这个字符的时候,没有进行扩容的操作,因为在插入 'c' 字符的之前,就与开辟了100 个空间。提前开辟开空间。

注意的是,我们使用的reserve是给string的对象预留空间,而如果我们在后面给定的大小小于本来的大小,那么对于reserve()会不会缩容操作,他在底层是有自己的判断的:

 比如上述的代码,我们使用 clear()函数清除数组当中存储的内容,然后我们使用 reserve 缩容操作就可以实现。例如上述代码,我们在扩容之后再使用 clear()清除当中的内容,然后在进行缩容:

······
·····
······
	s1.clear();
	s1.reserve(10);
	cout << "clear_capacity: " << s1.capacity() << endl;


输出:

 我们发现,从之前的 111 大小,缩容到了 15。

 而,如果我们不使用 clear()直接进行缩容,那么就不行:

	s1.clear();
	cout << "clear_capacity: " << s1.capacity() << endl;

 输出:(如下,还是 111 大小)
 

而且,我们上述在使用reserve 与开辟空间的时候,传入的是 100 ,但是我们发现,在reserve 当中开辟的是 111 个大小的空间,这里可能是 VS 下的内存对齐等等的有关,但是只能是 大于等于 100,不会小于 100。

 resize 开空间 + 填值初始化

 resize 也是开空间,只不过在开空间的时候,如果有新开辟的空间,那么就会在新开辟的空间当中,进行填值,初始化。如果没有在函数当中给出初始化的值,那么就默认是用 空字符('\0')来初始化。如果有给定值,使用给定值初始化:

 上述例子,string类当中的 size(有效字符数)发生了改变。

 指定初始化的值:

 如果resize()当中给定大小比数组的大小要小,那么他会直接删除掉之后多余的数据。但是,和之前的 reserve 一样,不一定会进行缩容,其中还是有底层判断。

其实,上述当中的缩容,大多数情况下是不会缩的,string的实现还是比较保守的;为什么不会轻易进行缩容呢?因为缩容是要付出代价的。此处的缩容不是把后面不需要的空间的权限还给操作系统,我们之前在使用C语言当中的 malloc 等等动态开辟的空间一样,开辟的空间是一块连续的空间,不能像上述一样把这个空间分两段,前一段需要,后一段不需要就把权限返回给操作系统。

实际当中实现的缩容是,开辟一块新的空间,这个空间的大小就是缩容之后的空间大小;然后把元空间当中的数据拷贝下来,来释放原空间。

 如果我们想要主动缩容,那么我们可以使用下面 在C++11 当中实现的接口:
 

 官方文档:

string::shrink_to_fit - C++ Reference (cplusplus.com)

 上述的 shrink_to_fit  也是缩容不一定会缩到 指定大小,可能会考虑一下内存对齐问题,可能会大一点。

但是还是不建议进行缩容操作,因为基本没有这个需求,而且缩容有代价。

注意:

  • 上述的 size( )和 length()这两个函数在底层的实现逻辑是一样的,出现 size( )的目的就是与其他的容器的实现保持一致。
  • clear()这个函数只是将 string类中底层的 数组当中的有效字符清空,并不会改变这个数组的 空间大小。
  • .resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大
  • 小,如果是将元素个数减少,底层空间总大小不变。
  • eserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小

 迭代器

我们可以使用begin和 end  来获取这个string 类当中的字符串的 首位字符串的迭代器,和 最后一个有效字符的后一位置的字符的迭代器。

begin+ end begin获取一个有效字符的迭代器 + end获取最后一个有效字符下一个位置的迭
代器
rbegin + rend rbegin获取最后一个有效字符的迭代器 + rend获取第一个有效字符前一个位置的迭
代器
	string s3("hello world");
	string::iterator it = s3.begin();
	while (it != s3.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

输出:

像上述使用 迭代器访问的方式,我们可以把这里的 it 迭代器理解为一个指针,但是迭代器不完全是指针。

而像上述当中的 begin( )和 end(),两个函数返回的是对应位置的迭代器,向上述例子当中可以理解为指针,但是这个函数不是都返回的指针。

 像上述当中的 *it 就是这个it位置的字符。

++it , 就是让 it 这个迭代器往下走,当 it == "\0" 就结束循环。

iterator是一个像指针一样的类型,可能是指针,可能是封装的自定义类型。

 *it 可以理解是指针的解引用,那么就可以使用 解引用来修改字符:
 

	cout << s3 << endl;
	string::iterator it = s3.begin();
	while (it != s3.end())
	{
		(*it)--;

		++it;
	}
	cout << endl;

	it = s3.begin();
	while (it != s3.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

输出:

不仅仅是在 string类 当中支持迭代器,任何容器都是支持迭代器的,而且用法都类似。 

	vector<int>v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator vit = v.begin();
	while (vit != v.end())
	{
		cout << *vit << endl;
		++vit;
	}

 迭代器的好处就在于,我们之前访问数组这些连续空间当中访问的时候,就可以使用 "[]" 的方式来访问这个其中的数据,但是 "[]" 这样方式访问的前提是 ,这空间是连续的,如果是向链表,树,这样的结构就不能使用 "[]" 的方式访问,但是像上述的这些类型,有些可以实现迭代器,那么就可以使用迭代器的方式来访问其中的数据了。

迭代器提供了一种统一的方式去访问 简单的数组,复杂一点 链表,更复杂的 树,哈希表这些结构。

迭代器的用法就是 和算法来一起使用的 ,我们在库当中查看一些算法的时候,发现很多的函数都实现的所有的类型,比如如下的 reverse()逆置函数,他支持的是所有的类型,在这里就是使用的是迭代器来进行传参,而用模板来实现各种迭代器的匹配问题:

 利用函数模板,来实现不同类型的迭代器的传参,实现函数的重载,这样不管是什么类型的迭代器传进来,都可以使用这个算法。

那么如上述的过程,算法就可以通过容器去修改容器当中的数据,使用迭代器和模板之后,我们就可以不用在去关系实现的数据是什么类型的了,只需要传入这个类型的迭代器即可。 

对于上述的当中的 begin()和 end()函数是顺序访问的取其中的迭代器的函数,那么如果我们想要逆向访问的话,也是可以的。

使用 rbegin()和 rend()函数都是可以进行操作的,所对应获取的位置的迭代器如下所示:
 

上两个函数对应的迭代器就是 reverse_iterator 迭代器,所以我们在使用迭代器的时候,应该使用的迭代器应该是 reverse_iterator。

例子:

	string s3("hello world");

	string::reverse_iterator vit = s3.rbegin();
	while (vit != s3.rend())
	{
		cout << *vit << " ";
		vit++;
	}
	cout << endl;

 输出:
 

 对于上述当中的迭代器,在写起来的时候已经有些麻烦了,我们可以使用auto来自动推导这个迭代器的类型:

auto vit = s3.rbegin();

 一些问题

 我们在实现函数的传参的时候,比如现在我想把 一个 string类的对象传入到函数当中去,那么这里就会取调用这个string类的拷贝构造函数,去创建一个空间,这里是一个深拷贝,这样不仅仅会浪费空间,而且会有损效率,我们在这里的优化解决方案是传入这个 string类对象的 引用,这样就不会发生深拷贝了。如果我们不想修改这个对象,那么我们还会用 const 修饰这个形参

但是如果我们在传入引用之后,函数中使用了这个对象的迭代器,不会报错;但是如果用const 修饰之后就会报错!!!

 如这个例子:

void func(const string& s)
{
	string::reverse_iterator vit = s.rbegin();
	while (vit != s.rend())
	{
		cout << *vit << " ";	
		vit++;
	}
	cout << endl;
}

int main()
{
	string s3("hello world");

	func(s3);

	return 0;
} 

 报错:

 我们发现,报的错是模板的错,而且报的错很复杂,因为模板的实现很复杂。

 这里其实发生的权限的放大,而且对于const 对象的传参,有对应的const 迭代器来使用:

 在官方文档当中,有 const char& operator[] (size_t pos_ const; 这个const的成员函数当我们函数中传入的是 const 的对象的时候,在其中使用的迭代器应该是 const 的 迭代器:

void func(const string& s)
{
	string::const_reverse_iterator vit = s.rbegin();
	while (vit != s.rend())
	{
		cout << *vit << " ";	
		vit++;
	}
	cout << endl;
}

 输出:

对于 普通的迭代器,可以读写,但是对于const 的迭代器,就只能进行读的操作。其中的写功能也就是能不能对string类当中给定字符串中的字符进行修改。

 也就是说 如果 it 是我们定义的迭代器的话,如果不能进行 写的操作,那么是 *it 不能改变,而 it 是可以修改的。

 针对上述的 const 的问题,我们使用 auto就非常的好用,他同样会自动推导这个 const 的迭代器

void func(const string& s)
{
	/*string::const_reverse_iterator vit = s.rbegin();*/
	auto vit = s.rbegin();
	while (vit != s.rend())
	{
		cout << *vit << " ";	
		vit++;
	}
	cout << endl;
}

 输出:

 范围for

 在string类当中的访问,其实最方便的就是使用 返回for 的方式来访问这个 string类当中的字符串。

范围for是在C++11  支持的更简洁的新的遍历方式。

 语法:

for(变量的类型 变量名(s1) : 需要迭代的变量名(s2))
{
    // 其中就可以使用新创建的 s1 这个变量来迭代 s2 
}
	string s3("hello world");
	for (auto str : s3)
	{
		cout << str;
	}

输出:

 在此处我们使用了 auto 来自动的推出这个这个str 的类型,然后他会自动的 迭代,自动判断结束。

如上述例子,就是依次从 s3 当中取数据,然后赋值给 str ,通过这样的方式来进行迭代。

 所以像上述的 方式,如果我们直接修改 str 的内容,s3 当中的字符串是不会改变的

	string s3("hello world");
	for (auto str : s3)
	{
		str++;
	}
	cout << s3 << endl;

输出:

 如果我们使用类型是 这个目标变量的引用就可以修改了:

	string s3("hello world");
	for (auto& str : s3)
	{
		str++;
	}
	cout << s3 << endl;

输出:

 这时候,每一次传入的就是依次传入 这个 字符串当中字符的引用,所以我们就可以进行修改

范围for 其实 在底层的实现就是用的迭代器来实现的,他其实就是使用类似于我们上述在迭代器当中实现的遍历一样来实现,而上述的依次拷贝其实就是把 *it 拷贝给了 str,从而使实现自动迭代。

 我们查看反汇编也能看到一些我们之前在 迭代器当中的一些影子:

 在 返回for 当中也 调用了 begin()和 end()函数来判断 开始和结束。

也就是说,我们使用得很香的 范围for 其实就是使用的 迭代器 来实现的,没有迭代器就没有 范围for,那么有些类型是不支持  迭代器的,那么它就不支持 范围for,例如:

在 Stack 当中就不支持 范围 for:

范围for 也是有局限性的,因为范围for 只能正向的遍历,不能像之前的迭代器一样,还有返乡遍历的 迭代器,范围for只是一个傻瓜式用 正向遍历的 迭代器做成的。

问题:范围for很智能,如果for调用的对象是普通对象,调用就是普通的迭代器;如果范围for调用的是const的迭代器,那么调用就是const 的迭代器,如果当前类没有实现const 的迭代器,那么就会报错。(权限的放大) 

 建议:因为范围for是非常单纯的把其中的迭代器取出,赋值给某一个中间变量,然后在进行for当中操作,如果我们取出的迭代器是普通内置类型的迭代器,那么赋值的影响不大,如果是自定义类型的赋值,那么多了之后代价就很大了。所以,关于范围for当中的 中间变量的类型,可以考虑用引用:

vector<string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
v.push_back("4444");

for(auto& ch : v)
{
    cout << ch << endl;
}

如上例子当中,范围for中的 ch 中间变量可以用引用,因为从v当中取出的是string的自定义类型。

但是因为是引用,所以,建议不要再 范围for当中对ch 进行修改。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chihiro1122/article/details/130789273

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签