C++——string类
寂柒 2024-10-11 15:35:01 阅读 62
1.初识string
string属于C++标准库,而不属于STL,STL也属于C++标准库
string是管理字符的顺序表,用来管理字符数组
string是模板,只是库直接给它typedef了,直接实例化了
string是动态开辟的字符数组,指向的空间在堆上,可以动态增长
string的接口设计的非常的繁杂
自定义类型实现流插入和流提取要重载,但这里在库里已经实现了
<code>void TestString1()
{
string s1;//构造无参的
cin >> s1;
cout << s1 << endl;
}
2.string的构造函数
2.1无参数构造函数
<code>void TestString1()
{
char arr[10];
//C语言的问题是,没办法很好地按需去申请空间
string s1;//构造无参的
cin >> s1;
cout << s1 << endl;
}
2.2字符串构造
<code>void TestString1()
{
string s2("hello");
}
2.3拷贝构造函数
<code>void TestString1()
{
string s1;//构造无参的
string s2("hello");
string s3(s2);
cout << s3 << endl;
}
2.3.1补充
void TestString2()
{
string s1("hello");
string s2="hello";code>
//为什么支持这种写法?
//单参数的构造函数支持隐式类型转换
//这里是构造加拷贝构造,然后优化
}
2.4substring(3)
<code>void TestString6()
{
string s1("hello world");
string s2(s1);
cout << s2 << endl;
string s3(s1, 6, 3);
cout << s3 << endl;//wor
string s4(s1, 6, 5);
cout << s4 << endl;//world
}
void TestString6()
{
string s5("hello worldxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyy");
string s6(s5, 6);
//想把第6个位置及其后面的字符全部取到,怎么做?难道一个个地数吗?
//此时就可以使用缺省参数npos
cout << s6 << endl;
string s7(s5, 6,s5.size()-6);//使用size也可以
cout << s7 << endl;
//注意:strlen不可以对自定义类型使用
//strlen(s5._str);//这里不可行,_str是私有变量
//而且STL有很多版本,你无法得知对象中的私有变量名称
}
npos是里面的静态成员变量,给的值是-1,但npos的类型是size_t(unsigned integer:无符号整型)
在底层存储的是补码,-1的补码是全1,类型提升、转换,转换成无符号整型就是整型的最大值:42亿9千万左右
1G是 2^30 4G就是 2^32 byte ,也就是42亿9千万左右
一个string对象不会有4G那么大,所以npos完全够用
32位下new 1G还可以,2G就new不出来了,开不了那么大的连续空间,内存不够
如果len太长,字符串太短,此时也不会越界去访问,有多少取多少,直到字符串结束
2.5from sequence(5)
使用前n个去初始化
<code>void TestString8()
{
string s1("hello world",5);
cout << s1 << endl;//hello
string s2("hello world", 9);
cout << s2 << endl;//hello wor
}
2.6fill(6)
使用n个c去初始化
<code>void TestString7()
{
string s1(10,'a');
cout << s1 << endl;//aaaaaaaaaa
string s2(3,'x');
cout << s2 << endl;//xxx
}
2.7range(7)
<code>void TestString9()
{
//使用迭代器区间去初始化
string s1("hello world");
string s2(s1.begin(), s1.end());
cout << s2 << endl;//hello world
string s3(++s1.begin(), --s1.end());//第一个和最后一个不要
cout << s3 << endl;//ello worl
}
2.8赋值运算符重载
<code>void TestString10()
{
string s1("hello");
cout << s1 << endl;
string s2("world");
cout << s2 << endl;
//一、
s1 = s2;
cout << s1 << endl;
//二、
s1 = "hello world";
cout << s1 << endl;
//三、
s1 = 'x';
cout << s1 << endl;
}
3.string的非成员函数重载(全局函数)
3.1 +
<code>void TestString1()
{
string s1;//构造无参的
string s2("hello");
string ret1 = s1 + s2;//实现字符串的连接
cout << ret1 << endl;
string ret2 = s1 + "world";
cout << ret2 << endl;
//C语言实现需要strcat
//缺点:1.可读性差
//2.需要考虑扩容问题
//3.需要找到'\0',如果字符串很长,找'\0'也需要很长的时间
string ret3 = s1 + s1;//这里是拷贝而不是往s1后面添加字符串
cout << ret3 << endl;
}
void TestString4()
{
string s2("hello");
string s3 = s2 + 'a';
string s4 = s2 + "abcd";
//能不使用+就不要使用,因为+是传值返回
}
3.2 relational operators
3.3 swap
3.4operator>>
scanf和cin都有一个特点,如果输入多个值,默认使用空格或者换行来作为间隔分割
<code>void test()
{
//scanf 不能处理内置类型
string s1;
scanf("%s", s1.c_str());
//问题一:返回值是const
//问题二:没有开空间
}
3.5 getline
获取一行,遇到空格不结束,遇到换行才结束
还可以自己定义结束符号
#include<string>
using namespace std;
void test()
{
//想以某个指定的字符结束
string s1;
getline(cin, s1, '!');
cout << s1;
}
4.string的元素访问
4.1 []
4.2at
at和[]的功能是一样的,只是说它不是运算符重载,它就是一个普通的函数
<code>void TestString3()
{
string s1;
s1.resize(10, '#');
s1[0]++;//使用起来更加形象
cout << s1 << endl;
s1.at(0)++;//使用起来比较别扭
cout << s1 << endl;
}
5.遍历数组
遍历数组的方式有三种:
5.1 []
//遍历数组
void TestString2()//访问
{
//一、
for (size_t i = 0; i < s1.size(); i++)
{
//读
cout << s1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < s1.size(); i++)
{
//写
s1[i]++;
}
cout << s1;
}
5.2迭代器
迭代器分为两大类:有无const、正向与反向,组合起来就是4种
5.2.1正向迭代器
//正向迭代器
void TestString3()
{
string s1("hello");
//二、迭代器
//迭代器是遍历数据结构的一种方式,可以认为它类似一个指针
string::iterator it = s1.begin();//一般在类里面定义或是typedef
//迭代器定义在类域里面,所以要指定类域
//it是对象
//begin()是成员函数,一般begin就是返回开始位置的迭代器,返回开始位置的指针
//迭代器何时终止?它不等于end就截止了,end是最后一个数据的下一个位置,o是最后一个数据
//C++中对字符串是有要求的,为了满足兼容C语言等需求,除了存储有效字符,还要存储'\0'
while (it != s1.end())
{
cout << *it << " ";//只读
++it;
}
cout << endl;
//_size是5,与strlen相同,不算'\0','\0'在这里面是一个标识字符,标识结束
//所以最后一个有效字符是o,end就指向'\0'
}
5.2.2补充
<code>void TestString3()
{
string s1("hello");
string::iterator it = s1.begin();
//while (it != s1.end())
while (it < s1.end())//这里可以这样使用,但不建议
{
cout << *it << " ";
++it;
}
cout << endl;
//实际上迭代器才是主流的遍历方式,它是通用的,可以遍历所有容器
//同时屏蔽了底层的实现细节,也体现了面向对象的封装
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::iterator lit = lt.begin();
while (lit != lt.end())//所以推荐使用!=,因为它是通用的
//while (lit < lt.end())//物理空间大概率不连续,是一个个的小块内存,end未必一定比begin大
{
cout << *lit << " ";
lit++;//底层也是cur=cur->next
}
}
5.2.3反向迭代器
<code>//反向迭代器
void TestString4()
{
string s1("hello");
//倒着遍历
//string::reverse_iterator rit = s1.rbegin();
auto rit = s1.rbegin();//因为rbegin的返回值就是反向迭代器,右边有一个对象,会自动推导左边对象的类型
while (rit != s1.rend())
{
cout << *rit << " ";
rit++;//这里++就是反着走的
}
//这样也可以实现,但是非常别扭
string::iterator rit = s1.end();
rit--;
while (rit != s1.begin())
{
cout << *rit << " ";//只读
--rit;
}
cout << endl;
}
5.2.4有无const
<code>void func(const string& s)
//不推荐传值传参
// 要进行拷贝,并且这里要去调用拷贝构造,同时拷贝构造要使用深拷贝,否则会出现多次析构的问题
{
//string::iterator it = s.begin();//这样写就不支持了
string::const_iterator it = s.begin();
while (it != s.end())
{
//const迭代器不支持写
//*it = 'a';
//读
cout << *it << " ";
it++;
}
cout << endl;
//string::const_reverse_iterator rit = s.rbegin();
auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
}
void TestString6()
{
string s1("hello");
func(s1);
}
5.3范围for
void TestString5()
{//三、
string s1("hello");
//自动判断结束、解引用、++
//原理:编译时编译器替换成迭代器
//读
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
//范围for的底层和第二种遍历方式是完全类似的,把*it赋值给ch
//看似代码很短,实际是交给编译器去生成代码了
//范围for不支持倒着遍历
for (auto ch : s1)
{
ch++;//s1不会修改
}
cout <<s1<< endl;
//写
for (auto& ch : s1)//ch就是*it的别名,这样就可以修改s1
{
ch++;
}
cout << s1 << endl;
}
6.Capacity
6.1size
size用来看字符串的有效字符是多少个,不算‘\0’,'\0'是标识字符,标识结束
void TestString11()
{
string s1("hello");
cout << s1.size() << endl;
//不算'\0',打印的是5
}
底层在数组上存储的时候要存储‘\0’
为什么要存储‘\0’?明明是个可以动态增长的数组,不要‘\0’也是可以的。
因为要兼容C语言,有些场景下必须要调用C的接口,Linux是用C写的,Linux的接口也是使用C写的,比如有个地方需要传字符串,此时传string是不认识的
void TestString14()
{
string filename;
cin >> filename;
//FILE* fout = fopen(filename, "r");//这里不能编译通过
//怎么打开文件?
//首先,第一个要传一个常量字符串,C语言规定常量字符串要给'\0'
//c_str,即返回C形式的字符串,会返回底层指向字符数组的指针
FILE* fout = fopen(filename.c_str(), "r");
//C语言对字符串、字符数组的规定是结尾要有'\0'
//如果结尾没有'\0',就算提供了c_str,C语言的那些接口也不能很好地兼容,因为不知道字符串的结尾
//C语言字符串为什么要加'\0',因为'\0'是标识字符,就知道它结束了,那就可以拿到名字的长度了
//这样C++的string就可以和C语言的接口进行完美的配合
}
6.2length
与size作用相同,string产生的比STL要早,C语言最开始给字符串取名字叫strlen,length(长度)明显比size(大小)更为形象,早期的时候字符个数设计的就叫做length,在STL出现后,就开始使用size了,因为size具有通用性,而length不具有,同时其它的数据结构只有size
6.3capacity
计算数组的容量大小,一般情况下不包含'\0'
6.4clear
会清掉数据,但不会释放空间
void TestString12()
{
string s1("hello world");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.clear();
cout << endl;
//如何判断是否释放了空间?
//看capacity,capacity变小就说明释放了
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//二者都是15,证明没有释放空间
cout << endl;
//STL并未进行严格的规定,但一般来说,clear是不释放空间的
//clear一般都是把之前的数据清掉,然后继续插入一些数据
//空间比较廉价,没有必要把空间释放掉
//最终空间肯定会被释放的,有析构函数
s1 += "jxyxtla";
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
}
6.5empty
用来判空
6.6max_size
设计初衷是想知道string最大有多长,最长能到多少
但实际上,1.不同平台下实现不同
2.它非常无用,因为获取的信息也未必准确,比如假设现在内存已经不足了,已经创建了很多对象,开了很多的空间,它就不准确了。这个值是写死的而不是实时变化的。
void TestString13()
{
string s1("hello world");
cout << s1.max_size() << endl;
//内存很足时,可以开21亿(32位)
}
6.7reserve
6.7.1引入
<code>void TestString1()
{
//string的扩容
//不断插入数据,看capacity的变化,来观察string的扩容
string s;
size_t old = s.capacity();
cout << "初始是" << old << endl;
for (size_t i = 0; i < 100; i++)
{
s.push_back('a');
if (s.capacity() != old)
{
cout << "扩容为" << s.capacity() << endl;
old = s.capacity();
}
}
}
6.7.2应用
// reserve 保留
void TestString2()
{
//reserve可以提前开空间
string s;
s.reserve(100);//要100的空间,肯定会去开100,但同时也可能会比100还大
size_t old = s.capacity();
cout << "初始是" << old << endl;
}
6.7.3补充
<code>void TestString2()
{
//那么reserve会不会缩容?
s.reserve(10);
//一般是不会缩容,但STL没有严格规定,这只是一般惯例的实现,不同的平台实现都可能不同
//核心原因还是缩空间这件事不符合我们目前的设计,空间是足够大的
cout << "缩容为" << s.capacity() << endl;
}
6.8resize
6.8.1大于15
<code>void TestString3()
{
string s("hello world");
cout << s << endl;
cout << "初始长度为" << s.size() << endl;
cout << "初始容量为" << s.capacity() << endl;
s.resize(20, 'a');//此时就会扩容
cout << s << endl;
cout << "改变size后长度为" << s.size() << endl;
cout << "改变size后容量为" << s.capacity() << endl;
}
6.8.2大于11小于15
void TestString3()
{
string s("hello world");
cout << s << endl;
cout << "初始长度为" << s.size() << endl;
cout << "初始容量为" << s.capacity() << endl;
//s.resize(13);
s.resize(13,'a');//打印hello worldaa
}
6.8.3小于11
<code>void TestString3()
{
string s("hello world");
cout << s << endl;
cout << "初始长度为" << s.size() << endl;
cout << "初始容量为" << s.capacity() << endl;
s.resize(5);//此时就会删除数据,只保留前5个字符
cout << s << endl;
cout << "改变size后长度为" << s.size() << endl;
cout << "改变size后容量为" << s.capacity() << endl;
}
6.8.4使用场景
void TestString3()
{
//使用场景
//比如要开一个字符串,要开10个空间,且每个字符都是#
string s1;
cout << s1 << endl;
cout << "初始长度为" << s1.size() << endl;
cout << "初始容量为" << s1.capacity() << endl;
s1.resize(10, '#');//此时的作用就是开空间+初始化
cout << s1 << endl;
cout << "使用size后为" << s1.size() << endl;
cout << "使用size后为" << s1.capacity() << endl;
//核心特点就是要把size改变为所需的值
}
6.9shrink_to_fit
缩容的接口,缩容以后还要插入数据,代价太大,尽量不要使用
7.Modifiers
7.1push_back
<code>void TestString4()
{
string s;
s.push_back('a');//push_back是插入一个字符
s.append("hello world");//插入字符串要使用append
string s1("hello");
s.append(s1);
}
7.2append
7.3+=
<code>void TestString4()
{
string s;
string s1("hello");
s += s1;
s += "helloc";
s += 'x';
}
7.4assign
assign有赋值的意思
<code>void TestString5()
{
string str;
//string str("hello world");
//如果已经有一些数据,会把这些数据覆盖掉
string base = "The quick brown fox jumps over a lazy dog.";
str.assign(base);//把base assign给str
std::cout << str << endl;
str.assign(base,5,10);//从第5个位置开始,取10个字符
std::cout << str << endl;
}
7.5insert
在指定位置前,进行插入数据的操作
<code>void TestString6()
{
//之前的数据结构,像顺序表,都是插入一个值
//string的特点就是可以插入一个值,也可以插入多个值
//因为它的多个值非常好表达,像之前的只能使用数组来表达
//而它可以使用字符串来表达,有常量字符串这个概念
string s("hello");
cout << s << endl;
//头插一个字符只能这样写
s.insert(0, 1, 'b');//一、
s.insert(s.begin(), 'a');//二、
cout << s << endl;
//头插需要挪动数据,尽量少使用头插
}
7.6erase
<code>void TestString7()
{
string s("hello world");
cout << s << endl;
s.erase(7);//删除第7个位置及其以后的数据
cout << s << endl;
}
7.7replace 替换
<code>void TestString8()
{
string s("hello world");
cout << s << endl;
string s1("hello");
s.replace(5, 5, s1);
cout << s << endl;
s.replace(5, 1, "123");
cout << s << endl;
s.replace(5, 3, "456");
//平替覆盖时效率尚可,但凡多一个少一个都要挪动数据
cout << s << endl;
}
注意:insert、erase和replace尽量不要使用,因为它们都涉及到挪动数据,效率不高
它们的接口设计都是复杂繁多,需要使用时查看文档即可
7.8补充
void TestString8()
{
//题目:将空格替换为%10
string s2("askjd n nasdnkla lk sknkl nlk");
cout << s2 << endl;
//以空间换时间的方式
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "%10";
}
}
cout << s3 << endl;
}
那如果就是要s2改变怎么呢?
7.8.1赋值
void TestString8()
{
//一、赋值 开空间,拷贝数据过去
s2 = s3;
//s2.assign(s3);
cout << s2 << endl;
}
7.8.2 string的Modifiers::swap
void TestString8()
{
//二、string自己提供的swap
//本质是交换二者的成员变量 _str、_size、_capacity
printf("s2:%p\n", s2.c_str());
printf("s3:%p\n", s3.c_str());
s2.swap(s3);
cout << s2 << endl;
printf("s2:%p\n", s2.c_str());
printf("s3:%p\n", s3.c_str());
//证明
}
7.8.3全局的swap模板
<code>void TestString8()
{
//三、全局的swap可以交换string
swap(s2, s3);
cout << s2 << endl;
//3次深拷贝,要去调用一次构造、两次赋值,代价太大
}
7.8.4string的非成员函数重载的swap
实际上,上面的代码不会去实例化全局的swap模板
有可以直接去调用的函数,就不用实例化模板了
<code>void TestString8()
{
swap(s2, s3);
cout << s2 << endl;
}
7.9pop_back
8.String operations
8.1 c_str
兼容C语言,有些接口是C的,想把数组传过去就使用c_str
8.2 data
与c_str功能类似
8.3 copy
8.4 substr
8.5 find
<code>void TestString9()
{
//假设有一个文件,要求读取这个文件的后缀,怎么做?
//文件后缀有个特点,以.为结尾
string s1("test.cpp");
size_t i = s1.find('.');
string s2 = s1.substr(i);//.cpp
cout << s2 << endl;
}
8.5.1应用
void TestString10()
{
string s1("https://legacy.cplusplus.com/reference/string/string/substr/");
size_t i = s1.find(':');
if(i!=string::npos)
cout << "协议为:" << s1.substr(0, i) << endl;
i+=3;
size_t i1 = s1.find('/',i);
if (i1 != string::npos)
cout << "域名为:" << s1.substr(i, i1-i) << endl;
i = i1 + 1;
//cout << "资源名为:" << s1.substr(i, s1.size()-1-i) << endl;
cout << "资源名为:" << s1.substr(i) << endl;
}
8.6 rfind
void TestString9()
{
string s3("test.cpp.tar.zip");
//如果文件名是这样的怎么办?
size_t i1 = s3.rfind('.');//倒着找
string s4 = s3.substr(i1);//.zip
cout << s4 << endl;
}
8.7 find_first_of
void TestString11()
{
string s1("sajsj ksjald ksljad ljaks");
size_t i = s1.find_first_of("abc");
//找到第一个出现的该字符串中的任意一个字符
while (i!=string::npos)
{
s1[i] = '*';
i = s1.find_first_of("abc", i + 1);
}
cout << s1 << endl;
}
8.7.1补充
void TestString11()
{
string s1("sajsj ksjald ksljad ljaks");
size_t i = s1.find_first_of("abc");
//如果在s1中要遍历m次,在"abc"要遍历n次,则O(n)=m*n
//如何减少时间复杂度?
int count[256] = { 0 };
count[97] = 1;
count[98] = 1;
count[99] = 1;
for (int i = 0; i < s1.size(); i++)
{
if (count[s1[i]] == 1)
s1[i] = '*';
}
cout << s1 << endl;
//直接在s1中遍历,这样O(n)就是n了
}
8.8 find_last_of
void TestString11()
{
string s1("sajsj ksjald ksljad ljaks");
size_t i = s1.find_last_of("abc");
//同理,但是是倒着找的
while (i != string::npos)
{
s1[i] = '*';
i = s1.find_last_of("abc", i + 1);
}
cout << s1 << endl;
}
8.9 find_first_not_of
void TestString11()
{
string s1("sajsj ksjald ksljad ljaks");
size_t i = s1.find_first_not_of("abc");//找不是"abc"的字符
//这里的字符串给的越长,花费的时间就越长,效率就越低
while (i != string::npos)
{
s1[i] = '*';
i = s1.find_first_not_of("abc", i + 1);
}
cout << s1 << endl;
}
8.10 find_last_not_of
同理
9.练习
387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
//索引就是下标
class Solution {
public:
int firstUniqChar(string s) {
int Array[26]={0};//计数排序可以很好地解决问题
for(auto ch:s)
{
Array[ch-'a']++;//相对映射
}
for(size_t i=0;i<s.size();i++)
{
if(Array[s[i]-'a']==1)
{
return i;
}
}
return -1;
}
};
917. 仅仅反转字母 - 力扣(LeetCode)
//string中的[]对越界的检查是通过断言来检查,[]检查非常严格,一旦越界就会报错
//所以要遵循逻辑:先检查,再访问
class Solution
{
public:
bool IsLetter(char ch)
{
if(ch>='a'&&ch<='z')
return true;
if(ch>='A'&&ch<='Z')
return true;
return false;
}
string reverseOnlyLetters(string s)
{
int begin=0;
int end=s.size()-1;
while(begin<end)
{
while(begin<end&&!IsLetter(s[begin]))
begin++;
while(begin<end&&!IsLetter(s[end]))
end--;
swap(s[begin],s[end]);
begin++;
end--;
}
return s;
}
};
415. 字符串相加 - 力扣(LeetCode)
class Solution
{
public:
string addStrings(string num1, string num2)
{
int x1 = stoi(num1);
int x2 = stoi(num2);
return to_string(x1 + x2);
}
};
//这样是不可行的,给定的测试值稍大就会超出范围
#include<string>
class Solution
{
public:
string addStrings(string num1, string num2)
{
string s;
int end1=num1.size()-1;
int end2=num2.size()-1;
int next=0;//进位数
while(end1>=0||end2>=0)
{
int x1=end1>=0?num1[end1]-'0':0;
int x2=end2>=0?num2[end2]-'0':0;
int ret=x1+x2+next;
next=ret/10;
ret=ret%10;
s.push_back('0'+ret);//使用尾插效率更高,O(n)=n
//使用insert头插也可以是实现,但效率更低,O(n)只有n^2
end1--;
end2--;
}
if(next==1)
{
s.push_back('1');
}
reverse(s.begin(),s.end());//最后逆置就可以了
return s;
}
};
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。