【C++】--- string的使用
string的使用
- 1.构造函数的使用
- 2. string类对象的访问及遍历操作
- 2.1operator[]实现遍历
- 2.2 普通迭代器正序遍历
- 2.3 auto/范围for
- 2.3.1 auto
- 2.3.2 范围for
- 2.4 普通迭代器逆序遍历
- 2.5 const迭代器
- 3.string类对象的容量操作
- 3.1 size和length
- 3.2 max_siz
- 3.3 capacity
- 3.4 clear
- 3.5 resize
- 4.string类对象的修改操作
- 4.1 push_back
- 4.2 append
- 4.3 operator+=
- 4.4 不同平台下string的扩容机制
- 4.5 find和replace 替换指定字符
- 5. 剩下的部分接口
- 5.1 c_str
- 5.2 refind 和 substr 取文件名的后缀
string是basic_string
的模板char类型实现,相比于其他类型的模板实现,string是最常用的。
1.构造函数的使用
#include<iostream>
using namespace std;
int main()
{
string s1; //string(); 无参构造,构造空字符串
string s2("hello world"); //string(const char* s) 用C-string来构造string类对象
string s3 = s2;//string(const string&s) 拷贝构造函数
string s4(5, 'c');//string(size_t n, char c) string类对象中包含n个字符c
cout << s1<< endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
return 0;
}
2. string类对象的访问及遍历操作
2.1operator[]实现遍历
int main()
{
string s1("hello world");
for (int i = 0; i < s1.size(); i++)//size()返回容器中元素的个数
{
cout << s1[i] << " ";
}
return 0;
}
s1[i]
这里是一个函数重载,原型是s1.operator[].(i)
。operator[]的实现大致如下:
char& operator[] (size_t pos)
{
assert(pos < _size);
return _str[pos];
}
char&
引用返回允许我们修改返回字符。
库函数中还实现了const版本的operator[]函数,const的string对象使用operator[]就会调用const版本的重载,当然这个版本不允许修改返回字符。
2.2 普通迭代器正序遍历
int main()
{
string s1("hello world");
string::iterator it1 = s1.begin();
while (it1 != s1.end())//最后一个数据的下一个位置 '\0'
{
cout << *it1 << " ";
it1++;
}
return 0;
}
- 迭代器的使用很像指针,有些容器的迭代器底层实现是指针,有些不是。
- 迭代器可以实现对字符串对象的读写。
- 迭代器是一种通用的方式,operator[]只适用于元素地址连续的容器。
- 迭代器是通过运算符重载的方式来实现的。
2.3 auto/范围for
2.3.1 auto
auto是C++11给出一个"语法糖",会自动推导类型。
int main()
{
int i = 0;
auto z = i;
auto x = 1.1;
auto p = &i;
return 0;
}
int main()
{
int& r1 = i;
auto r2 = r1; //int
auto& r3 = r1; //int& r1的引用
}
auto无法推导出引用,r1是i的引用,但是r2并不是r1的引用,这里的quto r2 = r1
等价于int r2 = r1
,可以使用auto&
来解决问题。
另外 auto 需要被初始化,auto r4;
这样无法推导类型,是错误的。下面将体现auto的使用价值。
int main()
{
//auto的价值1:方便
auto it1 = list1.begin();
std::map<std::string, std::string> dict;
//std::map<std::string, std::string>::iterator;
auto dit = dict.begin();
}
这样就可以替代写起来很长的类型,可以简化代码。
2.3.2 范围for
C++范围for也是C++11的一个语法糖,用来遍历容器,底层是迭代器 当底层支持迭代器的时候,才可以使用范围for。
如何使用范围for遍历一个string对象呢?
int main()
{
string s1("hello world");
//自动取容器的数据赋值给左边的值,自动判定结束,自动++;
for (char ch : s1)
{
cout << ch << " ";
}
return 0;
}
一般使用范围for的时候一般搭配auto
使用,上面的例子可以写为for(auto ch: s1)
string s1("hello world");
for (char ch : s1)
{
cout << ch << " ";
}
cout << endl;
for (char ch : s1)
{
ch++;
}
for (char ch : s1)
{
cout << ch << " ";
}
cout << endl;
由于范围for是把容器拷贝给auto对象,所以范围for不修改原容器。
可以使用引用来修改容器。
for (char& ch : s1)
{
ch++;
}
范围for搭配引用的使用范围:
- 需要修改容器
- 类型占内存很大。
- 当类型很大同时又不想修改的时候可以考虑
const auto&
有个例外,数组也支持范围for,可以认为是编译器的特殊处理
int main()
{
int a[] = { 1,2,3,4,5,6 };
for (auto ae : a)
{
cout << ae << " ";
}
return 0;
}
从C++20开始支持auto做参数,C++11开始就支持auto做返回值了。
auto Func(auto x)
{ ..}
但是auto做返回值,会导致程序不够清晰明了。
auto func3()
{
auto y = func4();
return y;
}
auto func2()
{
auto x = func3();
return x;
}
int main()
{
auto ret1 = func();
}
ret1的类型是什么呢?auto做返回值要谨慎使用。
2.4 普通迭代器逆序遍历
如何倒叙遍历一个字符串对象呢?迭代器分为正向迭代器iterator
和反向迭代器reverse_iterator
,反向迭代器可以实现倒叙遍历。
int main()
{
string s1("hello world");
auto rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit; //++ 不是 --
}
return 0;
}
2.5 const迭代器
const对象无法使用普通的迭代器,const迭代器也无法修改原容器。
与普通迭代器相同,const迭代器也分为正向迭代器const_iterator
和反向迭代器const_reverse_iterator
int main()
{
string s1("hello world!");
const string s2(s1);
//const对象无法使用普通的迭代器
//auto it = s2.begin();
string::const_iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
++it;
//*it1 += 1; 无法修改
}
cout << endl;
string::const_reverse_iterator it1 = s2.rbegin();
while (it1 != s2.rend())
{
cout << *it1 << " ";
++it1;
//*it1 += 1; //const迭代器无法修改
}
return 0;
}
3.string类对象的容量操作
3.1 size和length
获取string对象中的有效字符的个数
int main()
{
string s1("hello world");
cout << s1.size() << endl;
cout << s1.length() << endl
return 0;
}
size和length的功能相同,为什么要都实现出来呢,着不得不提出发展历史的原理,string的涉及比STL要早,string起初设置的是length,但是STL的都命名为size,string类也就补充了一个size,size的命名方式比lengt通用,所以推荐使用size。
3.2 max_siz
表示容器可最大申请的个数,理想化的,没有意义,在不同的环境下不同。
int main()
{
string s1("hello world");
cout << s1.max_size() << endl;
return 0;
}
在X86环境下,可以达到21亿多,但是实际是达不到的。
3.3 capacity
可以存储的有效字符的个数,底层跟顺序表的设计类似,可实现自动扩容。
int main()
{
string s1("hello world");
cout << s1.capacity() << endl;
return 0;
}
这里实际存储的容量是16,有效字符的后一个位置存\0
,和C语言保持一致。
3.4 clear
清除数据,一般不清除空间数据,只修改size,然后首位置改为\0
,具体的实现更为复杂。
int main()
{
string s1("hello world1111");
s1.clear();
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
这里还有三个接口没有讲
3.5 resize
resize会有三种情况
- resize < size
- size < resize < capacity
- resize > capacity
第一种是一个删除的行为,第二三种就是插入的行为,第三种还涉及到扩容。
int main() //resize
{
string s1("hello world");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(10); //删除数据
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(14); //插入
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl; //调试后发现插入的是'\0',最后面一个被认为是终结符。
s1.resize(20,'x'); //扩容并插入
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
如果没有设置插入的字符,默认就是\0
,并且只有最后一个\0
会被认定为是终结符。
4.string类对象的修改操作
这一部分也是string设计比较乱的地方
4.1 push_back
在字符串后尾插字符c
int main()
{
string s1("hello");
//尾插字符
s1.push_back(',');
s1.push_back('w');
cout << s1 << endl;
return 0;
}
4.2 append
在字符串后追加一个字符串。
int main()
{
string s1("hello");
s1.push_back(',');
s1.push_back('w');
s1.append("orld");
cout << s1 << endl;
return 0;
}
append接口有多个重载函数,可以实现很多方式的字符串尾插。
甚至可以插入一段迭代器区间
int main()
{
string s1("hello");
s1.push_back(',');
s1.push_back('w');
s1.append("orld");
string s2("hello linux");
s1.append(s2.begin()+5, s2.end());
cout << s1 << endl;
return 0;
}
4.3 operator+=
+=运算符重载可以平替上面两个接口的部分功能,是很常用的。
int main()
{
string s3("hello");
s3 += ',';
s3 += "world";
cout << s3 << endl;
return 0;
}
4.4 不同平台下string的扩容机制
写一段代码来体现一下vs2022的string的扩容机制。
int main()
{
string s1;
size_t old = s1.capacity();
cout << "capacity:" << old << endl;
for (size_t i = 0; i < 500; i++)
{
s1 += 'x';
if (s1.capacity() != old)
{
cout << "capacity:" << s1.capacity() << endl;;
old = s1.capacity();
}
}
return 0;
首次2倍,后续都是1.5倍来扩容的。
Linux下的扩容方式为2倍扩容。
int main()
{
string s3("1111111");
string s4("11111111111333333333333333333333333333");
cout << sizeof(s3) << endl;
cout << sizeof(s4) << endl;
return 0;
}
vs的第一次为什么是2倍扩容呢?string的成员变量有一个char buffer[16]
,当字符的有效个数不大于15的时候,字符串实际存储在栈上,当字符串大于15时,才会到堆上开辟空间,避免了短小字符串在堆上频繁申请增加系统开销。
为了避免频繁扩容,在预先知道大约需要多少空间的时候,我们可以调用reserve
接口,提前申请好容量。
int main()
{
string s5;
s5.reserve(200);
cout <<s5.capacity()<< endl;
return 0;
}
reserve在vs2022下的实现开出了一个比200大的空间,也是符合使用需求的。
4.5 find和replace 替换指定字符
int main()
{
string s1("hell world ");
cout << s1 << endl;
size_t i = s1.find(' ');
while (i != string::npos)
{
s1.replace(i, 1, "%%");
i = s1.find(' ',i+2);
}
cout << s1 << endl;
return 0;
find没有找到指定字符的时候就是返回npos
find函数由于替换的时候需要循环挪动数据,所以效率很低。
如何提效呢?这里有一个空间换时间的方法。
int main()
{
string s1("hell world ");
cout << s1 << endl;
string s2;
for (auto ch : s1)
{
if (ch != ' ')
s2 += ch;
else
s2 += "%%";
}
s1 = s2;
//s1.swap(s2);
cout << s1 << endl;
return 0;
5. 剩下的部分接口
5.1 c_str
返回c格式的字符串
int main()
{
string s1("hello world");
cout << s1 << endl;
cout << s1.c_str() << endl;
return 0;
}
C++已经实现了流插入运算符的函数重载,可以打印出C格式的字符串了,那为什么还有提供这个接口呢?有些软件不会提供C++的接口。因为C++兼容C,c_str()
可以保证C和C++混合编程。
5.2 refind 和 substr 取文件名的后缀
refind: 从字符串的pos位置向前查找,返回该字符在字符串种的位置。
substr:在str中从pos位置开始,截取n个字符,然后将其返回。
int main()
{
string s1("test.cpp.zip");
size_t pos = s1.rfind('.');
if(pos != string::npos)
{
string sub = s1.substr(pos);
cout << sub << endl;
}
return 0;
}