• [C++](9)string类的使用:构造|赋值|遍历|容量|修改|字符串|迭代器


    STL简介

    STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

    STL六大组件img

    网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。

    简单了解string类

    string出现较早,本不是STL容器,但是它和STL容器有很多相似的操作,因此我们把它和其他STL容器放到一起学习。


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

    C++中,我们有了string类,操作更加方便,快捷。


    string实际上是模板实例化后typedef出来的:

    string类的文档介绍👉string - C++ Reference (cplusplus.com)

    string
    typedef basic_string<char> string;
    
    • 1
    • 2

    可见其底层的类模板名叫basic_string,指定模板参数char实例化的类就是string类

    类似的还有:

    u16string
    typedef basic_string<char16_t> u16string;
    
    • 1
    • 2
    u32string
    typedef basic_string<char32_t> u32string;
    
    • 1
    • 2
    wstring
    typedef basic_string<wchar_t> wstring;
    
    • 1
    • 2

    由于char是8位字符类型,最多只能表示256种字符,许多外文字符集所含的字符数目超过256个,char型无法表示,所以出现了其他这些字符类型。

    为了适应不同的字符类型,于是就有了basic_string这个类模板


    string类的一个简单使用:

    int main()
    {
    	string s = "hello";
    	s += " world!";
    	cout << s << endl;
    	return 0;
    }
    //结果:hello world!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    string类常见接口

    构造

    构造函数说明
    string()string的默认构造,空字符串
    string(const char* s)用C-string构造string类对象
    string(const string& s)string的拷贝构造
    string(const char* s, size_t n)用C-string的前n个字符构造
    string(size_t n, char c)string类对象中包含n个字符c
    string(const string& str, size_t pos, size_t len = npos)用str中pos位置开始的len个字符构造(len默认为npos)
    string(InputIterator first, InputIterator last)迭代器区间初始化

    最常用的:①②③

    //默认构造
    string s1;
    //带参构造
    string s2("hello world!");
    string s3 = "hello world";
    //拷贝构造
    string s4(s2);
    string s5 = s3;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    不那么常用的:④⑤

    string s6("ABCDEF", 3);
    cout << s6 << endl; //结果:ABC
    string s7(10, 'x');
    cout << s7 << endl; //结果:xxxxxxxxxx
    
    • 1
    • 2
    • 3
    • 4

    string s8(s2, 3, 5);
    cout << s8 << endl; //结果:lo wo
    
    string s9(s2, 3, 100);
    cout << s9 << endl; //结果:lo world! //len的值大于要拷贝的字符串长度,则直接拷贝到字符串末尾
    string s10(s2, 3);
    cout << s10 << endl; //结果:lo world!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    👆:第三个参数len默认为nposnpos是string类的一个静态成员变量,其原型为static const size_t npos = -1;-1的补码全为1,转换为无符号整型就是size_t的最大值,意味着如果不传第三个参数,就直接拷贝到str字符串的末尾。

    ⑦涉及迭代器,留到后面讲。

    赋值

    string& operator= (const string& str);
    string& operator= (const char* s);
    string& operator= (char c);
    
    • 1
    • 2
    • 3

    支持string,C-string,char类型赋值

    string s1("hello world");
    string s2("xxx");
    s1 = s2;
    s1 = "yyy";
    s1 = 'z';
    
    • 1
    • 2
    • 3
    • 4
    • 5

    遍历

    1. 下标+[]

    此法使用最多

    string类重载了[],我们可以把它当成数组来访问:

          char& operator[] (size_t pos);
    const char& operator[] (size_t pos) const;
    
    • 1
    • 2

    👆:注意这里是引用返回,而不是传值返回,目的不是减少拷贝,而是为了支持修改返回对象;第二个有const修饰的,他与第一个构成函数重载,这样做可以保证如果传入的是const修饰的string,那么返回的引用也具有const修饰。

    例:

    void test1()
    {
        string s1("hello world!");
        for (int i = 0; i < s1.size(); i++) //size()返回该字符串字符个数
        {
            cout << s1[i] << " ";
        }
        cout << endl; //结果:h e l l o   w o r l d !
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    👆:string类里面还有个length成员函数,它的作用和size一样。它是为早期的string设计的,现在为了和其他STL容器统一,更多使用size


    也可以通过at成员函数来访问

          char& at (size_t pos);
    const char& at (size_t pos) const;
    
    • 1
    • 2

    例:

    void test4()
    {
    	string s1("hello world!");
    	for (int i = 0; i < s1.size(); i++)
    	{
    		cout << s1.at(i) << " ";
    	}
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    at与[]的区别在于,at的越界访问会抛异常,[]的越界访问通过断言来检查。

    2. 迭代器(重要)

    迭代器遍历虽然麻烦一点,但它是我们遍历STL容器的利器,比如list这种就用不了[],但依然可以用迭代器。

    • 现阶段可以认为迭代器是一个类似指针的东西。具体是不是指针,还要看底层实现。
    • 迭代器类型iterator是一个类所特有的,使用时要指定类域
    • 成员函数begin()返回开始位置的迭代器,end()返回末尾的下一个位置的迭代器。

    例:

    void test2()
    {
        string s1("hello world!");
        string::iterator it = s1.begin();
        while (it != s1.end())
        {
            cout << *it << " ";
            ++it;
        }
        cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    👆:while的判断条件it != s1.end()也可以写成it < s1.end(),因为这里的迭代器可以当成指针,而且string的存储空间是连续的。


    反向迭代器

    下标的反向遍历很简单,迭代器的反向遍历需要用到反向迭代器。

    • 反向迭代器的类型:reverse_iterator
    • rbegin()返回一个反向迭代器,该迭代器指向字符串最后一个字符。rend()返回的反向迭代器指向第一个字符之前的理论元素。

    例:

    void test5()
    {
    	string s1("hello world!");
    	string::reverse_iterator rit = s1.rbegin();
    	while (rit != s1.rend())
    	{
    		cout << *rit << " ";
    		++rit; //此处不能写成--rit
    	}
    	cout << endl; //结果:! d l r o w   o l l e h
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    👆:正向迭代器和反向迭代器一个是正着走,一个是倒着走,但都是用的++


    const迭代器

    对于const修饰的对象,以上两种迭代器都不能用,因为它们既可以读也可以写。

    const迭代器的类型为const_iterator或者const_reverse_iterator

    const string s2("hello world!");
    string::const_iterator it = s2.begin(); //begin传入const对象的指针返回的就是const迭代器
    
    • 1
    • 2

    3. 范围for

    string类型的对象是可迭代的,可以使用范围for。

    例:

    void test3()
    {
    	string s1("hello world!");
    	for (auto ch : s1)
    	{
    		cout << ch << " ";
    	}
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    容量操作

    函数说明函数原型
    size返回字符串长度size_t size() const;
    length返回字符串长度size_t length() const;
    max_size返回字符串可以达到的最大长度size_t max_size() const;
    capacity返回已分配的存储空间大小size_t capacity() const;
    reserve请求更改容量void reserve(size_t n = 0);
    resize将字符串的长度调整至nvoid resize(size_t n);
    void resize(size_t n, char c);
    clear请掉所有数据,容量不变。void clear();
    empty判断是否为空bool empty() const;

    ①②中的①更常用一些,在上文 string类常见接口/遍历/1.下标+[] 使用过。


    max_size

    例:

    string s("hello world!");
    cout << s.max_size();
    //结果:2147483647
    
    • 1
    • 2
    • 3

    capacity

    我们可以通过以下程序看看编译器如何扩容:

    void testCapacity()
    {
    	string s;
    	size_t sz = s.capacity();
    	cout << "capacity changed: " << sz << endl;
    	for (int i = 0; i < 100; i++)
    	{
    		s.push_back('c'); //尾插
    		if (sz != s.capacity())
    		{
    			sz = s.capacity();
    			cout << "capacity changed: " << sz << endl;
    		}
    	}
    }
    //vs2022下的结果:
    //capacity changed: 15
    //capacity changed: 31
    //capacity changed: 47
    //capacity changed: 70
    //capacity changed: 105
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看出在vs2022的环境下,除了第一次,后面都是1.5倍扩容。


    reserve

    为了避免多次扩容造成的开销,我们可以提前使用reserve开好容量。

    在VS下如果要改的容量小于当前容量,则此函数无效,即reserve能扩不能缩

    例:

    void testReserve()
    {
    	string s;
    	s.reserve(1000);
    	cout << s.capacity() << endl;
    }
    //结果:1007
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    resize

    它改变的是字符串实实在在的大小,不仅要开空间,还要对新开的空间初始化。

    默认对新开的空间初始化为'\0',也可以自己传入要初始化的参数。

    在VS下如果要改的字符串大小小于当前大小,则舍弃后面的,只保留前n个字符,capacity保持不变。

    例:

    void testResize()
    {
    	string s1("hello world!");
    	s1.resize(1000); //通过监视窗口可以看到后面都是'\0'
    	string s2("hello world!");
    	s2.resize(1000, 'x'); //后面都是'x'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ⑦⑧比较常用,使用也非常简单,这里不多演示

    修改操作

    函数说明
    push_back在字符串后面加一个字符
    append在字符串后面加一个字符串
    operator+=在字符串后面加一个字符串
    insert插入到字符串中
    erase从字符串中删除字符
    assign为字符串指定新值,替换其当前内容。
    replace替换字符串的一部分
    swap交换两个字符串

    ①只能添加单个字符,②可以添加字符串,使用方式类似构造函数


    operator+=是最常用的,可读性最好

    函数原型:

    string& operator+= (const string& str);	 //string (1)
    string& operator+= (const char* s); //c-string (2)
    string& operator+= (char c); //character (3)
    
    • 1
    • 2
    • 3

    例:

    void test()
    {
    	string s1("a");
    	string s2("efg");
    	s1 += 'b';
    	s1 += "cd";
    	s1 += s2;
    	cout << s1 << endl;
    }
    //结果:abcdefg
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    insert

    函数原型:

    string& insert (size_t pos, const string& str); //string (1)
    string& insert (size_t pos, const string& str, size_t subpos, size_t sublen); //substring (2)
    string& insert (size_t pos, const char* s); //c-string (3)
    string& insert (size_t pos, const char* s, size_t n); //buffer (4)
    string& insert (size_t pos, size_t n, char c); //fill (5)
       void insert (iterator p, size_t n, char c);
    iterator insert (iterator p, char c); //single character (6)	
    template <class InputIterator> //range (7)	
       void insert (iterator p, InputIterator first, InputIterator last);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    👆:它的函数重载太多了,但也不必刻意地去记,有需要的时候可以查文档,这里挑选常见的进行讲解。

    插入单个字符可以用(5)或(6)

    例:

    void testInsert()
    {
    	string s("hello world!");
    	s.insert(0, 1, 'x'); //(5)头插一个'x'
    	cout << s << endl; //xhello world!
    	s.insert(s.begin(), 'y'); //(6)头插'y'
    	cout << s << endl; //yxhello world!
    
    	s.insert(3, 1, 'x'); //在下标为3的位置插入一个'x'
    	cout << s << endl; //yxhxello world!
    	s.insert(s.begin() + 3, 'y'); //迭代器当成指针来用
    	cout << s << endl; //yxhyxello world!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    插入一个字符串可以用(1)或(3)

    例:

    void testInsert()
    {
    	string s1("hello world!");
    	string s2("yes ");
    	s1.insert(0, "ok "); //头插一个C-string
    	cout << s1 << endl; //ok hello world!
    	s1.insert(0, s2); //头插s2
    	cout << s1 << endl; //yes ok hello world!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    erase

    函数原型:

    string& erase (size_t pos = 0, size_t len = npos); //sequence (1)
    iterator erase (iterator p); //character (2)
    iterator erase (iterator first, iterator last); //range (3)
    
    • 1
    • 2
    • 3

    例:

    void testErase()
    {
    	string s("hello world!");
    	s.erase(s.begin()); //删除一个字符
    	cout << s << endl; //ello world!
    	s.erase(3, 2); //删除下标3开始的2个字符
    	cout << s << endl; //ellworld!
    	s.erase(3); //删除下标3开始的所有字符
    	cout << s << endl; //ell
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ⑥⑦用得较少,此处省略。。。


    swap

    函数原型:

    void swap (string& str);
    
    • 1

    这个swap是成员函数,另外库里还有一个swap是函数模板。

    例:

    void testSwap()
    {
    	string s1("hello world!");
    	string s2("goodbye world!");
    	s1.swap(s2);
    	swap(s1, s2);
    	cout << s1 << " " << s2 << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么这两个swap有什么区别呢?

    • 成员函数swap效率更高,它本质上是指针的交换。
    • 函数模板实例化的swap效率较低,它是深拷贝交换,会用到拷贝构造和赋值。

    字符串操作

    函数说明
    c_str返回指向这个字符串的指针(C-string)
    substr返回子字符串
    findrfind在字符串中查找内容

    c_str

    函数原型:

    const char* c_str() const;
    
    • 1

    例:

    void testCstr()
    {
    	string s("hello world!");
    	const char* sp = s.c_str(); //转换为了C风格的字符串
    	cout << sp << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    substr

    函数原型:

    string substr (size_t pos = 0, size_t len = npos) const;
    
    • 1

    返回从pos位置开始的长度为n的字符串


    find

    函数原型:

    size_t find (const string& str, size_t pos = 0) const; //string (1)
    size_t find (const char* s, size_t pos = 0) const; //c-string (2)
    size_t find (const char* s, size_t pos, size_t n) const; //buffer (3)
    size_t find (char c, size_t pos = 0) const; //character (4)
    
    • 1
    • 2
    • 3
    • 4
    • 返回字符串第一次匹配的首字符的位置
    • 如果找不到匹配项,则返回string::npos

    比如我们给一个文件名的字符串,要取出该文件名的后缀:

    void testFind()
    {
    	string file("string.cpp");
    	size_t pos = file.find('.');
    	if (pos != string::npos) //找到
    	{
    		string suffix = file.substr(pos, file.size() - pos);
    		cout << file << "后缀:" << suffix << endl;
    	}
    	else //未找到
    	{
    		cout << "没有后缀" << endl;
    	}
    }
    //结果:string.cpp后缀:.cpp
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    rfind和find的区别:find是从前往后找,rfind是从后往前找。

    如果把上面的文件名换成string.c.tar.zip,用find找的就是第一个.,最后打印结果为.c.tar.zip。只想取出最后一个后缀可以用rfind

    void testRfind()
    {
    	string file("string.c.tar.zip");
    	size_t pos = file.rfind('.');
    	if (pos != string::npos)
    	{
    		string suffix = file.substr(pos, file.size() - pos);
    		cout << file << "后缀:" << suffix << endl;
    	}
    	else
    	{
    		cout << "没有后缀" << endl;
    	}
    }
    //结果:string.c.tar.zip后缀:.zip
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    利用上面的字符操作函数,如何将一个网站的协议,域名,uri分别取出呢?

    void test()
    {
    	string url1("https://blog.csdn.net/CegghnnoR/article/details/125471285");
    	string& url = url1;
    	string protocol; //协议
    	size_t pos1 = url.find(':');
    	if (pos1 != string::npos)
    	{
    		protocol = url.substr(0, pos1);
    		cout << "protocol:" << protocol << endl;
    	}
    	else
    	{
    		cout << "非法url" << endl;
    	}
    
    	string domain; //域名
    	size_t pos2 = url.find('/', pos1 + 3); //指定从pos1+3位置开始找
    	if (pos1 != string::npos)
    	{
    		domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
    		cout << "domain:" << domain << endl;
    	}
    	else
    	{
    		cout << "非法url" << endl;
    	}
    
    	string uri = url.substr(pos2 + 1);
    	cout << "uri:" << uri << endl;
    }
    //结果:
    //protocol:https
    //domain:blog.csdn.net
    //uri:CegghnnoR/article/details/125471285
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    string类非成员函数

    关系运算符(string)

    string支持比较,可以string和string比较,也可以string和C-string比较。

    void testRelational()
    {
    	string s1("hello world!");
    	string s2("string");
    	s1 < s2;
    	s1 < "hello";
    	"hello" < s1;
    
    	s1 < string("hello"); //匿名对象
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    👆:有人吐槽,支持string和C-string比较是多此一举,C-string也可以构造成匿名对象参与比较。


    operator+ (string)

    +的重载,不建议多用,因为它是传值返回,增加了拷贝。


    operator>> (string)operator<< (string)

    已经用了很多次了,不多说。


    getline (string)

    • 获取流中的一行到str中
    istream& getline (istream& is, string& str, char delim);
    istream& getline (istream& is, string& str);
    
    • 1
    • 2

    C语言中要通过键盘把带空格的字符串输入到字符数组中要用到gets,因为scanf是以空格和换行作为分隔符,gets只以换行为分隔符。

    类似于gets,C++中可以使用getline。

    void testGetline()
    {
    	string s;
    	getline(cin, s); //输入hello world!
    	cout << s << endl; //打印hello world!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用getline和rfind可以轻松解决👉:字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)

    参考代码:

    #include <iostream>
    using namespace std;
    int main()
    {
        string s;
        getline(cin, s);
        size_t pos = s.rfind(' ');
        cout << s.size() - pos - 1 << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    shell脚本之数组
    jsp+ssm+maven美容院管理系统--idea mysql
    【存储RAID】存储RAID常见模式及其特点
    【无标题】
    Node Sass version 9.0.0 is incompatible with ^4.0.0.
    微信支付证V3
    liunx CentOs7安装MQTT服务器(mosquitto)
    PHP基础学习第六篇(初识CSS)
    ssm+vue的高校疫情防控管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。
    SpringCloud中Gateway提示OPTIONS请求跨域问题
  • 原文地址:https://blog.csdn.net/CegghnnoR/article/details/125507077