C++ string类的初步了解

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

一.   为什么学习string类

1.C语言中的字符串

2.string类

二.   string类的常用接口说明

1.构造

 2.容量

size和length

 capacity

clear

empty

reserve

resize

3.元素访问 

operator[]

at

front、back

4.迭代器

​编辑begin、end

rbegin、rend 

cbegin、cend、crbegin、crend

5.增添、删除、修改

 operator+=

append 

push_back

assign

insert

replace

swap

pop_back 

 6.字符串操作

 c_str

find 

rfind

substr 

7.string类非成员函数

​operator+

relational operators 

operator>>

getline


一.   为什么学习string类

1.C语言中的字符串

C语言中字符串是以'\0'结尾的一些字符的集合为了操作方便C标准库中提供了一些str系列的库函数但是这些库函数与字符串是分离开的不太符合OOP的思想而且底层空间需要用户自己管理稍不留神可能还会越界访问。


2.string类

string类介绍

1. 字符串是表示字符序列的类

2. 标准的字符串类提供了对此类对象的支持其接口类似于标准字符容器的接口但添加了专门用于操作单字节字符字符串的设计特性。

3. string类是使用char(即作为它的字符类型使用它的默认char_traits和分配器类型(关于模板的更多信息请参阅basic_string)。

4. string类是basic_string模板类的一个实例它使用char来实例化basic_string模板类并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。

5. 注意这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列这个类的所有成员(如长度或大小)以及它的迭代器将仍然按照字节(而不是实际编码的字符)来操作。

总结

1. string是表示字符串的字符串类

2. 该类的接口与常规容器的接口基本相同再添加了一些专门用来操作string的常规操作。

3. string在底层实际是basic_string模板类的别名typedef basic_string<char, char_traits, allocator>string;

4. 不能操作多字节或者变长字符的序列。



二.   string类的常用接口说明

标红的是常用的

1.构造

在string类的成员函数中最开始讲的是constructor (构造) 、destructor (析构) 以及operator (赋值)

我们主要来看一下构造

 在C++98中给与了7种方式我们可以对照着后面给的注释分别来看一下

首先第一种不用多说构造一个空的string类即一个空的字符串

第二种的参数是str即另外一个string类 功能实质上就是一个拷贝构造

第三种的参数就变成了三个分别为str、pos、以及len其中pos指的是拷贝开始的位置类似于数组的下标同样是从0开始而len指的是所拷贝的长度而后面的npos则是缺省值

通过索引我们可以知道npos的值默认为无符号的-1即2^31-1由于我们在创建string类时长度不可能这么大所以我们可以当做在不写参数len时默认为将后面所有拷贝进新的string类

第四种所传的参数则是一个类似于c语言中的字符串

第五种在第四种的基础上增加了一个参数n意为将字符串的前n个字符进行拷贝

第六种的两个参数n与c指的是构造一个size为n的string类并初始化为字符c

第七种需要先掌握迭代器暂时先放一放

我们可以来实践一下

void Test1()
{
	string s1;
	cin >> s1;
	string s2(s1);
	string s3(s1, 2, 3);
	string s4("abcdef");
	string s5("abcdef", 3);
	string s6(5, 'a');
	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;
	cout << "s3:" << s3 << endl;
	cout << "s4:" << s4 << endl;
	cout << "s5:" << s5 << endl;
	cout << "s6:" << s6 << endl;
}

我们也可以通过监视来看一下string类中的成员变量的情况

当然在allocator[6]的位置也是存有'\0'的

再往后的destructor(析构)没啥好说的就固定的一种方式

而operator=的使用方式与拷贝构造类似这里也就不多做说明


 2.容量

后面的Iterators迭代器我们先放一放先来讲一下Capacity容量

size和length

都是用来返回字符串的有效长度即成员变量size的那么这两个接口有什么不同呢没什么不同那么为什么会存在两个同样方式的接口呢这是因为不只是string其他容器同样也有大小而就像树一样它的大小不能使用length长度来表示只能使用size因此为了与其他容器保持一致string的接口就新增了size而以前用于表示大小的length当然不能舍弃

 capacity

返回容量大小字符串总长度即成员变量capacity

 

 

clear

说的也很清楚清空string而清空的是字符串中的有效部分

void Test2()
{
	string s1("abcdef");
	s1.clear();
}

clear前

clear后

 

empty

即判断字符串有效部分是否为空空返回1非空返回0

void Test2()
{
	string s1("abcdef");
	cout << s1.empty() << endl;
	s1.clear();
	cout << s1.empty() << endl;
}

reserve

通过解释我们可以知道该接口是将对象的capacity变为n当capactiy小于n时直接扩增到n或者更大当大于时其实是一个未定义的行为会根据编译器进行优化而同时不管怎么优化都不能对字符串的有效部分进行改变。

void Test2()
{
	string s1("abcdef");
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	s1.reserve(14);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	s1.reserve(18);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}

很显然我所使用的vs2022是没有对大于的情况进行优化的 

而在s1.reserve(18)中实际将capacity扩增到了31(加上'\0'为32)这是为什么呢我们先来探究一下扩增的规律涉及到后面的增添数据可以之后返回来看

void Test3()
{
	string s1;
	size_t sz = s1.size();
	for (int i = 0; i < 1000; i++)
	{
		s1 += 'a';
		if (sz != s1.capacity())
		{
			sz = s1.capacity();
			cout << sz << ' ';
		}
	}
	cout << endl;
}

 在上面的代码中我们将size在1000范围内所能扩增到的capacity的大小打印了出来

当然capacity是不包含'\0'的因此我们可以将sz+1打印出来作为真正的大小

 

可以看到除了第一次扩增了2倍以外后面的扩增都大概遵循1.5倍的关系

而在reserve进行扩增时也会从n向上找一个接近的值进行扩增这也就是为什么我们上面会扩增到31

resize

简而言之首先将字符串的长度size扩增到n若是n<=capacity就往原本字符串的末尾位置到位置n之间存放字符c若是没有该参数默认存放‘\0’若是n>capacity则先扩容在存放。

而要注意的是当n<size时size依旧会改变为n

void Test2()
{
	string s1("abcdef");
	cout << s1.size() << ' ' << s1.capacity() << endl;
	s1.resize(5);
	cout << s1.size() << ' ' << s1.capacity() << endl;
	s1.resize(12);
	cout << s1.size() << ' ' << s1.capacity() << endl;
	s1.resize(18);
	cout << s1.size() << ' ' << s1.capacity() << endl;
}

 


3.元素访问 

operator[]

 实质上就是下标访问操作符的重载用法上也是类似

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	while (s1[i] != '\0')
	{
		cout << s1[i++] << ' ';
	}
	cout << endl;
}

当然也可以改变一下循环

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	for(i=0;i<=s1.size();i++)
	{
		cout << s1[i] << ' ';
	}
	cout << endl;
}

当然由于该函数是传引用返回我们也可以对其进行修改 

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	while (s1[i] != '\0')
	{
		s1[i] = s1[i] - 'a' + '1';
		cout << s1[i++] << ' ';
	}
	cout << endl;
}

而我们可以看到还有第二种方式即当对象被const修饰时返回类型也就变为const char&这时就只能完成访问而无法做到改变

 

而为了越界operator[] 采用的方式是断言

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	for(i=0;i<20;i++)
	{
		cout << s1[i] << ' ';
	}
	cout << endl;
}

 

at

用法其实和operator[]一样

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	for(i=0;i<s1.size(); i++)
	{
		cout << s1.at(i) << ' ';
	}
	cout << endl;
}

 

 而不同的点在于at检查越界的方式是抛异常

void Test4()
{
	string s1("abcdef");
	size_t i = 0;
	for(i=0;i<20; i++)
	{
		cout << s1.at(i) << ' ';
	}
	cout << endl;
}

 

 

front、back

 一个是返回第一个字符一个是返回最后一个字符同样都可以访问对没有const修饰的对象都可以进行改变都无法对空的string对象进行使用而它们与begin和end的不同就放在后面的迭代器里讲吧

void Test4()
{
	string s1("abcdef");
	cout << s1.front() << " ";
	s1.front() = 'A';
	cout << s1.back() << endl;
	s1.back() = 'F';
	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << ' ';
	}
	cout << endl;
}

 


4.迭代器

begin、end

 简单来说begin是返回的字符串第一个字符的迭代器end是返回的字符串最后一个字符的下一个的迭代器。

我们可以使用它们来完成遍历

void Test5()
{
	string s1("abcdef");
	string::iterator it = s1.begin();
	for (it; it != s1.end(); it++)
	{
		cout << *it << ' ';
	}
	cout << endl;
}

同样若是对象没有使用const进行修饰我们也可以进行修改

void Test5()
{
	string s1("abcdef");
	string::iterator it = s1.begin();
	for (it; it != s1.end(); it++)
	{
		cout << *it << ' ';
		*it = *it - 'a' + '1';
	}
	cout << endl;
	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << ' ';
	}
	cout << endl;
}

 

 

这里再插一点除了operator[]、at以及迭代器我们也可以使用范围for来进行遍历

我们之前已经学过范围for来遍历数组而在string类中也可以范围for

void Test5()
{
	string s1("abcdef");
	for (auto& e:s1)
	{
		cout << e << ' ';
		e = e - 'a' + '1';
	}
	cout << endl;
	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << ' ';
	}
	cout << endl;
}

 

rbegin、rend 

 

 与begin、rend类似不同的是rbegin返回的是最后一个字符的反向迭代器而rend返回的是第一个字符前一个的反向迭代器

void Test5()
{
	string s1("abcdef");
	string::reverse_iterator it = s1.rbegin();
	for (it; it != s1.rend(); it++)
	{
		cout << *it << ' ';
		*it = *it - 'a' + '1';
	}
	cout << endl;
	for (it = s1.rbegin(); it != s1.rend(); it++)
	{
		cout << *it << ' ';
	}
	cout << endl;
}

而我们或许会觉得使用operator[]进行遍历就足够了迭代器没有什么必要。

的确在string中operator[]的确更方便但 这并不是在所有容器中通用的而迭代器是通用的

cbegin、cend、crbegin、crend

就是把begin、end、rbegin、rend中const的方式给单独摘出来了没啥其他不同


5.增添、删除、修改

 operator+= 

实际上就是向后增添字符串三种方式分别是增添string对象、 增添C形式字符串、增添字符

void Test6()
{
	string s1("abc");
	string s2("de");
	s1 += s2;
	cout << s1 << endl;
	s1 += "fg";
	cout << s1 << endl;
	s1 += 'h';
	cout << s1 << endl;
}

 

append 

 

 相较于operatorappend使用的方式更多一些大多数的使用方式其实与开始学的构造类似可以推断出来而第二种方式简单来说就是将参数str从下标为subpos的位置的长度为sublen的字符串增添到后面其实参数就是在pos和len的基础上加了一个sub

void Test6()
{
	string s1("abc"),s2("abc"),s3("abc"),s4("abc"),s5("abc");
	string s6("def");
	s1.append(s6);
	s2.append(s6, 0, 2);
	s3.append("defg");
	s4.append("defg",2);
	s5.append(3, 'e');
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
}

 

 

push_back

相较而言push_back就比较简单了

void Test6()
{
	string s1("abc");
	s1.push_back('a');
	cout << s1 << endl;
}

 

assign

大概就是重新分配字符串类似于赋值操作符只是用法多一些

第一种直接赋值第二种从下标subpos开始赋值sublen个字符第三种C类型字符串第四种C类型字符串前n个第五种n个字符c

void Test7()
{
	string s1("abcdef");
	string s2, s3, s4, s5, s6;
	s2.assign(s1);
	s3.assign(s1, 2, 3);
	s4.assign("abcedf");
	s5.assign("abcedf",3);
	s6.assign(3,'a');
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	cout << s6 << endl;
}

 

insert 

就是插入也很容易理解

除去迭代器相关的从上到下一次为pos位置插入str、pos位置插入str的subpos往后sublen长度、pos位置插入C类型字符串、pos位置插入C类型字符串前n项、pos位置插入n个字符‘c’

这些方法和前面的众多接口都是相似的后面类似的就不举例子了

replace

大部分都关联到迭代器就先不说了

swap

交换字符串就一种方法

此外swap还有非成员函数的重载

 

pop_back 

删除尾部字符


 6.字符串操作

 c_str

说白了就是将string对象转换为C类型的字符串返回首元素地址而要注意的是不能改变

void Test8()
{
	string s1("abcdef");
	cout << s1.c_str() << endl;
}

 

find 

查找查找string对象、C类型字符串、字符

 找到即返回第一个元素的位置找不到返回npos 

void Test8()
{
	string s1("abcdef");
	string s2("bc");
	cout << s1.find(s2) << endl;
	cout << s1.find("ef") << endl;
	cout << s1.find("mn") << endl;
	cout << s1.find("ef", 5) << endl;
	cout << s1.find('f') << endl;
}

 

rfind

功能类似逆序查找

void Test8()
{
	string s1("abcdef");
	string s2("bc");
	cout << s1.rfind(s2,4) << endl;
	cout << s1.rfind("ef") << endl;
	cout << s1.rfind("mn") << endl;
	cout << s1.rfind("ef", 4) << endl;
	cout << s1.rfind('f') << endl;
}

 

我们可以看到第4个不同于find需要所有字符串在0-pos的范围内rfind只需要所查找的第一个字符在0-pos内即可找到

substr 

从pos位置截断len长度的字符串作为string对象返回

void Test9()
{
	string s1("abcdef");
	string s2(s1.substr(2, 2));
	cout << s2 << endl;
}

 


7.string类非成员函数


operator+

string对象能和C类型字符串相加

void Test9()
{
	string s1("abc");
	string s2("def");
	cout << s1+s2 << endl;
	cout << s1 + "def" << endl;
	cout << 'a' + s2 << endl;
}

 

relational operators 

比较运算符重载和我们之前学的C类型字符串之间的比较一样。

operator>>

 

经典的流提取与流插入操作符的重载我们在前面也或多或少的用到了

getline  

 在流插入中与scanf有同样的问题可以以空格为间隔符这样就无法输入带有空格的字符串

void Test9()
{
	string s1;
	cin >> s1;
	cout << s1 << endl;
}

 

而getline就能解决这个问题

void Test9()
{
	string s2;
	getline(cin, s2);
	cout << s2 << endl;
}

 

end

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++