一、string类的基本介绍
1.1 c语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列
的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户
自己管理,稍不留神可能还会越界访问
这是c语言中使用的字符串,实际上c语言没有字符串的概念,它是一个字符数组
1.2 c++string类的介绍
先去看官方文档的介绍吧!
它本质上是由 typedef而来 ,是由类模版basic_string<char>实例化而来,用来记录一些字符。
既然是一个类,那么它就有成员变量、成员函数,操作符重载和六大默认构造 接下来,将要从这几个角度思考问题。
当然,在使用string类时,记得加上头文件<string>
二、string的默认函数
2.1 构造函数(constructor)
我们先看文档,从文档中查找函数
光看文档感觉好吓人呀!没事,先只看常用的,其他的不会以后再查表,这里会介绍最常用的,并且对其简化
一个一个来:
这个就是普通的默认构造函数,直接调用即可
string(): 构造空的string类对象,即空字符串
这里就是最简单的拷贝构造,参数就是一个const string类
string(const string&s)
这是C语言的构造方式,为了兼容C语言
string(const char* s)
string(const char* s,size_t n)
size_t n这个参数就是来限定,字符个数的。
string (const basic_string& str, size_type pos, size_type len = npos);
这个函数,也是拷贝构造,是个重载,pos表示从下面为pos的值开始读取,
len表示读取的长度,后面的缺省值(npos)很重要
我们转到它的定义去看
简单分析 size_t是无符号的数,-1的补码为 11111111111111111111111111111111,所以对于无符号数来说他的值为INT_MAX就是int类型最大值。
那么该函数,表示的就是,从pos位置copy这个字符串 n个字符。若没有n这个值,那么就会从pos位置copy所有值。
这里传的是引用啊!不是常量,a2 a3 a4 一个是从下标为6的位置分别度 1、2,npos个。 要注意pos指的是下标位置,比如说,第一个字符的pos值是0,pos为6时,此时为第七个字符
string(size_t n, char c) string类对象中包含n个字符c
2.2 析构函数
直接查表:
可以注意的是:表中没有关于析构的实现,所以,我们不用注意这一点,毕竟在销毁时,它会自动调用,所以不必在意
2.3 赋值运算符重载
先看看文档
这么三种函数,一个是赋值引用,一个是C语言的字符串(字符串的首地址),一个是字符,可以举例看看
值的注意的是,赋值运算符是对已经存在的对象进行复制,如果:
string a1=a2;//这是是拷贝构造,而不是赋值
三、string的操作符重载
3.1 流插入符与流输出符
<< >>使用这两个操作符,可以对string进行io流操作这个很简单
3.2 判断大小类操作符
在文档中就是这么多,哈哈!!!
大家认真观察就可以知道,无非就是三种:
1. 引用(自己的变量) 与 引用相比
2. 常量 与 引用 相比
3. 引用 与 常量 相比
在此之前,还有介绍字符串相比的规则
1. 相等与不相等:两个字符串完全一样,字符数量,与字符一 一相等,不相等就是它的反向逻辑
“abcdef” 与 "abcdef"相等 “aaaaaa”与“bbbbbb”不相等
2.大于 :第一个字符串大于第二个字符串:两个字符串中,第一个不相等的字符,左边的字符大于右边字符的Ascll值,还一种情况是,左右两边的字符串Ascll值一直相等,但是,左边字符串更长
"abcdef"大于"abcdea" f的Ascll值大于a
“ abcde”大于" abcd" 左边字符串比右边长
其实介绍完这两个,其他的就不用介绍了
3. 小于:不就是 大于等于取反吗?
4.大于等于:不就是大于和等于或一下
5.小于等于:就是大于取反
这样就里顺了逻辑了。
不过还有一个问题,为什么没有常量与常量相比呢?认真想一想,常量与常量相比,还需要调用函数吗?我们自己都能看出来,对吧!
这里就借用文档的例子吧!
#include <iostream>
#include <vector>
int main ()
{
std::string foo = "alpha";
std::string bar = "beta";
if (foo==bar) std::cout << "foo and bar are equal\n";
if (foo!=bar) std::cout << "foo and bar are not equal\n";
if (foo< bar) std::cout << "foo is less than bar\n";
if (foo> bar) std::cout << "foo is greater than bar\n";
if (foo<=bar) std::cout << "foo is less than or equal to bar\n";
if (foo>=bar) std::cout << "foo is greater than or equal to bar\n";
return 0;
}
这个好理解。
3.3 +=运算符
这个很简单,但是用的是非常多的,而且效率更高,对于尾接一个字符串
直接举例:
3.4 []运算符
它有两种,一种是常量另一种是普通变量
出现[]重载的原因是:要兼容c语言,可以把当成字符数组来看待,毕竟它的底层其实就是一个动态字符数组。
当然,这里也会有使用时越界问题:
对于c++而言,它会自己报错,所以越界了不用担心该就是了!
四、string的容量成员函数
4.0 容量与长度的介绍
string的底层是一个动态数组,那么这个动态数组里头,有size成员和capacity成员
当size==capacity时,就会进行扩容,一般的编译器是1.5倍扩容,所以一般来说容量都会是对齐。当然这里的容量不包含‘\0’,实际容量大1.所以容量与长度不是同一个概念,容量比长度大。
4.1 length和size函数
从底层实现来看,他们的效率都是一样,只是名字不同而已,length是为了兼容c语言而设计的,他们的功能:返回字符串的长度,注意就是不包括 '\0' 实际的长度要多一个。
这里STL库为了统一,一般使用size
4.2 max_size
这个函数是返回字符串的最大长度
由于已知的系统或库实现限制,这是字符串可以达到的最大潜在长度,但不能保证对象能够达到该长度:在达到该长度之前,它仍然可能在任何时候无法分配存储。
这个返回值,是不确定的!因为系统是不同的
我们还是举例子:
在X86的情况下:开出的最长字符串时2的31次方减1
这是计算器算的数
这是在X64的环境:开出的最长字符串时2的61次方减1
这是计算器算出的数,一模一样
4.3 capacity
返回字符串的容量,没有什么可以讲的。
4.4 resize
调整字符串容量
如果n是小于字符串的长度,那么size会缩小到n个字符,但是容量是不变的
如果n是大于字符串的长度,如果指定了字符,那么就会使用这个指定的字符,去扩展整个字符串,直到容量大于等于n,如果没有指定的话就会使用'\0'来扩展,所以说resize的作用,在于缩小字符串size与扩大字符串的容量,他只会扩大容量不会缩小容量,它是会改变字符串的内容的,慎用!!
举例:
int main()
{
string a("hello");
cout << a << endl;
cout <<"容量为"<< a.capacity() << endl;
a.resize(1);
cout << a << endl;
cout <<"容量为"<< a.capacity() << endl;
a.resize(10);
cout << a << endl;
cout << "容量为"<< a.capacity() << endl;
a.resize(40, 'a');
cout << a << endl;
cout << "容量为" << a.capacity() << endl;
a.resize(1);
cout << a << endl;
cout << "容量为" << a.capacity() << endl;
return 0;
}
这里就验证了,我们的说法了,resize最好是用于需要改变字符串内容的场景
4.5 reserve
它的功能更像是弥补resize的不足,OK
Requests that the string capacity be adapted to a planned change in size to a length of up to n characters.
要求字符串容量适应计划中的大小变化,长度最多为n个字符。
就是这里可以调整容量到n,如需扩容就会自动扩容,但是对于缩容是由编译器定义的,有些会缩容,有些不会。
In all other cases, it is taken as a non-binding (不具约束力)request to shrink the string capacity: the container implementation is free to optimize otherwise and leave the string with a capacity greater than n.
也就是缩容是不确定的!
This function has no effect on the string length and cannot alter its content.
无法改变字符串的内容!!
认真观察,其实它的容量是对齐的,而且在vs里它不会缩容,只会扩容,但是它的扩容效率比一般扩容更高它是一次性全部扩完,而一般的扩容是一次一次扩容的。不过差不了多少,1.5倍扩 容也很快。
但是啊!在Linux环境下,它是会缩容的!会缩到刚好容纳的下size。
4.6 clear以及empty
学过数据结构的一般都熟悉这两个接口:
clear()只是将string中有效字符清空,不改变底层空间大小
empty判空,判断string是否为空
这个就不举例子了,太简单了
4.7 简单介绍string类的扩容底层
string类一般有四个成员函数,
char buff[16];
char *a;
int capacity;
int size;
看看正好对应了这个类的大小
对于后三个成员,他们的作用就是扩容,存字符,当然buff数组的作用是当字符串的长度没有超过15时,此时使用buff数组来记录字符串,当超过15时,编译器就使用动态开辟堆里的空间存储字符
不同编译器下实现也就不同,但是大体的逻辑是不变的!
五、string的遍历方法
5.1 迭代器
迭代器类似于指针,指针只是指向一个变量,但是迭代器是指向一个自定义类型,他们都是用来访问内容的,下面就可以使用迭代器去遍历string类
直接看文档,一共八个迭代器
没关系一个一个来
先看begin和end
他们是一个获取首位置,以获取尾位置,对于end来说它获取的位置为有效位的下一个
end: Returns an iterator pointing to the past-the-end character of the string.
看代码如何遍历:
int main()
{
string a("hello world");
string::iterator it = a.begin();
for (; it != a.end(); ++it)
{
cout << *it <<" " ;
}
return 0;
}
再看 rbegin() 以及 rend()
rbegin()是返回最后一个有效位置,而rend是返回第一个有效位置之前的位置。注意他们是反向迭代器,所以移动的方向也会反向。
那么看代码吧!!
int main()
{
string a("hello world");
string::reverse_iterator it = a.rbegin();
for (; it != a.rend(); ++it)
{
cout << *it <<" " ;
}
return 0;
}
再看 cbegin() cend(),他们是常量迭代器,不能修改字符串的内容。当然前面的非常量迭代器是可以修改string的。我们这里长话短说
string ::const_iterator it=a.cbegin();
同理crend() 以及 crbegin()的道理是一样的,他们只是反向的常量迭代器
string ::const_reverse_iterator it=a.crbegin();
5.2 auto和范围for
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组auto也不能只定义不初始化
易错案例1:
#include<iostream>
using namespace std;
int func1()
{
return 10;
}
// 不能做参数
void func2(auto a)
{}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
return 3;
}
易错案例2:
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
auto e;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
易错案例3:
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
//error C3535 : 无法推导“auto * ”的类型(依据“int”)
auto*n=x;
易错案例4:
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto cc = 3, dd = 4.0;
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 };
这样就把易错点归纳完了。
接下来看范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。int main() { string arr = "abcdefg"; //c++98的遍历 for (int i = 0; i < arr.size(); ++i) { arr[i]++; } for (int i = 0; i < arr.size(); ++i) { cout<<arr[i]; } cout << endl; //c++11 范围for遍历 for (auto& ele : arr) { ele += 1; } for (auto ele : arr) { cout << ele; } return 0; }
注意点:
没有取引用的范围for是一个临时拷贝,不改变string的内容,但是加上了&就可以去改变string
auto 是自动识别类型会再编译是自动转化为对应的类型,但是使用多了容易造成可读性差
使用[]采用c语言也可以遍历字符串
六、string类修改对象成员函数
6.1 append()
作用:在字符串后面追加一个字符串
int main()
{
string a("haha");
a.append("haha");
cout << a << endl;//追加一个常量字符串
string b("hello world");
a.append(b, 6, 5);
cout << a << endl;//从下标为6的位置开始追加5个字符
string c;
c.append(b, 0);
cout << c << endl;//从下标为0的位置开始追加,npos(很大的数)个字符
string d;
d.append(b);
cout << d<< endl;//追加一个字符串
string e;
e.append("hahahaha", 4);
cout << e<< endl;//追加一个常量字符串的前4个
string f;
f.append(5, 'a');
cout << f << endl;//追加5个'a'字符
return 0;
}
这里其实和构造是很像的。不过+=也可以完成这个操作.设计了很多了接口,但是用的多就是参数为拷贝构造的那个
6.2 push_back()尾插
这个就很简单了,但是是用的很多的一个函数
就是在尾部插入一个字符即可。
6.3 insert pos位置插入
还是看代码吧!
int main()
{
string a("*****");
string b("aaaaaa");
a.insert(0,"haha");//在0位置插入一个常量字符串
a.insert(3, a);//在3位置插入字符串
a.insert(2,"a");//在2位置插入字符串 "a"
char ch='a';
a.insert(a.begin(), 'c');//头插一个字符
a.insert(0, 1, ch);//头插一个字符
return 0;
}
其实这里设计的很乱,确实很乱!
我们标记几个重点:
用的最多的还是 a.insert(0,b)和a.insert(begin(),'a');
这里的insert写的是头插
在使用insert时,一定得注意,insert的使用时间复杂度较高为n,使用时谨慎
6.4 erase pos位置删除
Erases part of the string, reducing its length:
int main()
{
string a("hello world");
//头删
a.erase(0, 1);
cout << a << endl;
a.erase(a.begin());
cout << a << endl;
//尾删
a.erase(a.size() - 1, 1);
cout << a << endl;
a.erase(--a.end());
cout << a << endl;
//删除pos位置后的所有字符
string s("hello world");
s.erase(5);
cout << s << endl;
//把所有字符全部删除完
string b("hahaha");
b.erase(b.begin(), b.end());
cout << b << endl;
return 0;
}
6.5 replace 替代函数
Replaces the portion of the string that begins at character pos and spans len characters (or the part of the string in the range between [i1,i2)) by new contents:
将字符串中从字符pos开始并跨越len字符的部分(或字符串中[i1,i2之间的部分)替换为新内容:
这个函数就是两个区间的替换
更多例子就不举例了,前面已经举例很多例子了,可以举一反三了!!!
6.6 swap
这个函数非常简单,就是把两个类的成员函数进行交换,以达到内容交换
6.7 pop_back()
这个函数同样简单,就是对string进行尾删。
6.8 find系列函数
find()
Searches the string for the first occurrence of the sequence specified by its arguments.
在字符串中搜索其参数指定的序列的第一个匹配项。
匹配项可以是字符串,也可以是字符
这样就完成了匹配,注意如果没有找到就返回无符号数-1,也就是npos,所有的find类的返回值都是这样,找到了返回下标,没有找到就返回无符号数-1.
rfind()
它的参数类型与find一模一样,只是find是从左边寻找,而rfind()是从右边开始寻找的,既可以找一个字符又可以找一个字符串。
find_first_of
什么意思呢?
它这个名字取得不太好,意思是:从pos位置,(从左往右,左闭右开),开始找,所有存在于
另一个字符串或者是字符的内容,找到第一个就返回其位置,没有找到就返回-1
看例子:
这里咱们就把所有有关"ld"的部分全部转化为'*',这是很有意思的,所实话,咱们的敏感字屏蔽就是这么来的,可以举例子让大家开心一下
find_last_of
这个函数与find_first_of几乎是一致的,就是查找方向是反的,怎么说,它是从后往前查找的
我们用这个来举例子:来看看英语的敏感字屏蔽。
这样,就可以做到屏蔽脏话
find_first_not_of和find_last_not_of
也同样道理,加上not就是不找指定的字符串呗,这个也好说
6.9 substr
它的功能就是返回一个从pos位置开始len长度的子串,len不写就是就是从pos位置开始的最长子串,这个我们用的多
这个很简单,也好用,就是复杂度为O(n);
七、 string类的小练习
7.1 第一例:
在一个字符串中把所有的空格替换成"%%"
这个思路还是很简单:直接find()查找空格,然后使用replace()替换即可
改进思路:牺牲空间以换时间
int main()
{
string a("hello cogou hello haha");
string b;
int i = 0;
while (a[i] != '\0')
{
if (a[i] == ' ')
{
b += "%%"; i++;
}
else
b += a[i++];
}
cout << b << endl;
return 0;
}
7.2 第二例:
c_str的应用
这个作为补充知识的,c_str是为了兼容C语言而创建的,为了以后方便调用数据库,它是返回字符串的首地址的,如果没有这个函数,其实对于string而言是没有这个概念的,当然从底层来讲,他当然是一个从堆里开空间的动态数组
举一个文件操作吧!
int main()
{
string a;
cin >> a;
FILE* fout = fopen(a.c_str(), "r");
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
fclose(fout);
return 0;
}
以后需要连接数据库时就要用到它。
7.3 第三例:
给一个完整的文件路径的字符串,要求文件与路径分离
补充:linux的文件分隔符为/ 而windows的文件路径分隔符为\
思路不难,使用find_last_of 查找最后一个分隔符,在使用substr获取子串即可
void fun(const string& x)
{
size_t pos = x.find_last_of("\\/");
string a = x.substr(0, pos);
string b = x.substr(pos + 1);
cout << a << endl;
cout << b << endl;
}
int main()
{
string a("C:\\Users\\Default\\AppData\\Roaming\\Microsoft\\Spelling");
string b("C:/Users/Default/AppData/Roaming/Microsoft/Spelling");
fun(a);
fun(b);
return 0;
}
7.4 第四例:
getline的补充知识
delim是用于读取字符串时,修改的结束字符
cin在读取时是不会读取空格的,但是如果一个字符串了就是有空格,而且需要读取输入,此时可以使用getline来解决问题。
字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)
看这个题目吧!
计算字符串最后一个单词的长度,单词以空格隔开,字符串长度小于5000。(注:字符串末尾不以空格为结尾)
输入描述:
输入一行,代表要计算的字符串,非空,长度小于5000。
输出描述:
输出一个整数,表示输入字符串最后一个单词的长度
示例1
输入:
hello nowcoder
输出:8
说明:最后一个单词为nowcoder,长度为8
#include <iostream>
using namespace std;
int main() {
string a;
getline(cin,a,'\n');
size_t pos=a.rfind(' ');
cout<<a.size()-1-pos;
return 0;
}
7.5 第五例:
给你一个字符串 s
,根据下述规则反转字符串:
所有非英文字母保留在原有位置。
所有英文字母(小写或大写)位置反转。
返回反转后的 s
经典双指针问题:
class Solution {
public:
inline bool panduan(char a)
{
return (a>='a'&&a<='z')||(a>='A'&&a<='Z');
}
string reverseOnlyLetters(string s) {
int left=0;
int right=s.size()-1;
while(left<right)
{
while(left<right&&!panduan(s[left]))
{
left++;
}
while(left<right&&!panduan(s[right]))
{
right--;
}
swap(s[left++],s[right--]);
}
return s;
}
};
7.6 第六例:
387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
这个题目在过去的哈希表题目讲解中写过,但是又出现了
直接上代码
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char,int>mp;
for(char ch:s)
{
mp[ch]++;
}
for(int i=0;i<s.size();i++)
{
if(mp[s[i]]==1)
return i;
}
return -1;
}
};
7.7 第七例:
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s
,如果它是 回文串 ,返回 true
;否则,返回 false
。
分析:
还是一个简单的题目,可以先把它转化,在使用双指针
class Solution {
public:
inline bool panduan(char x)
{
return (x>='a'&&x<='z')||(x>='A'&&x<='Z')||(x>='0'&&x<='9');
}
bool isPalindrome(string s) {
string a;
for(auto &ele:s)
{
if(panduan(ele))
{
if((ele>='A'&&ele<='Z'))
{
a+=ele+32;
}
else
a+=ele;
}
}
int left=0;int right=a.size()-1;
while(left<=right)
{
if(a[left++]!=a[right--])
return false;
}
return true;
}
};
7.8 第八例:
给定两个字符串形式的非负整数 num1
和num2
,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger
), 也不能直接将输入的字符串转换为整数形式。
思路:采用相加的运算法则,倒过来计算字符串,最后逆置
class Solution {
public:
string addStrings(string num1, string num2) {
string sum;
int addr=0;
int i=num1.size()-1;int j=num2.size()-1;
while(i>=0||j>=0)
{
int num=0;
if(i>=0)
{
num+=num1[i]-'0';--i;
}
if(j>=0)
{
num+=num2[j]-'0';--j;
}
num+=addr;
addr=num/10;
num%=10;
sum+=num+'0';
}
if(addr)
sum+='1';
reverse(sum.begin(),sum.end());
return sum;
}
};
这里补充知识:
1.reverse它是一个全局的函数,逆置容器的底层是迭代器
只要把迭代器的范围标注,就会逆置范围内的内容。
2.字符转化为数字与数字转化为字符都可以查文档
下面是字符变各种类型的数字
<string> - C++ Reference (cplusplus.com)
这里是数字变成字符
7.9 第九例:
给定一个字符串 s
和一个整数 k
,从字符串开头算起,每计数至 2k
个字符,就反转这 2k
字符中的前 k
个字符。
如果剩余字符少于 k
个,则将剩余字符全部反转。
如果剩余字符小于 2k
但大于或等于 k
个,则反转前 k
个字符,其余字符保持原样。
这个思路比较简单:就是使用reverse函数,注意区间问题即可
class Solution {
public:
string reverseStr(string s, int k) {
int i=0;//开始位置
int j=s.size()-1;//终止位置
while(1)
{
//运行位置
if(i+2*k-1<=j)
{
reverse(s.begin()+i,s.begin()+i+k);
i+=2*k;
}
else if(i+k-1<=j)
{
reverse(s.begin()+i,s.begin()+i+k);
break;
}
else
{
if(i+k-1>j&&i<=j)
reverse(s.begin()+i,s.end());
break;
}
}
return s;
}
};
7.10 第十例:
557. 反转字符串中的单词 III - 力扣(LeetCode)
给定一个字符串 s
,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
思路:还是如此find()找到空格,逆置单词
class Solution {
public:
string reverseWords(string s) {
int pos=s.find(' ',0);
int init=0;
while(pos!=string::npos)
{
reverse(s.begin()+init,s.begin()+pos);
init=pos+1;
pos=s.find(' ',init);
}
if(s[init]!='\0')
reverse(s.begin()+init,s.end());
return s;
}
};