C++ string类的初步了解

目录

一.   为什么学习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 

 

 相较于operator,append使用的方式更多一些,大多数的使用方式其实与开始学的构造类似,可以推断出来,而第二种方式简单来说就是将参数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

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

finish_speech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值