络腮胡菲菲 发表于 2025-4-6 00:53:59

第五讲(上) | string类的利用

部分参考了“爱扑bug的熊”大佬的视频,大家可以去看看: https://www.bilibili.com/video/BV1sX4y1w7zx/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=dc7a926badd09458939fa8246d82a9e3
C风格字符串(以\0结尾的字符数组)太过复杂,难于把握,不太适合大步伐的开发,所以C++STL中定义了一种string类,在头文件< string >中。
注:为C-Style字符串提供的库函数在头文件< cstring >中。
一、string和C风格字符串的对比



[*]char* 是一个指针,string是一个类。string封装了 char* ,管理这个字符串,是一个 char* 型的容器。
[*]string封装了很多实用的成员方法。
[*]不消思量内存释放和越界。string管理 char* 所分配的内存,每一次string的赋值/复制,取值都由string类负责维护,不消担心复制越界和取值越界等。
二、string类的本质

是一个动态的char数组大概是char数组的次序表。
三、string常用的API(注意只解说最常用的接口)

API 全称 Application Programming Interface (应用编程接口),STL提供给用户利用的接口,一样平常表现为公共成员函数和运算符重载。大部分的API只要看到它的函数声明就会用了。
string这里会解说的细致一些,后面的容器不会解说这么细致,由于很多地方都是相似的
https://i-blog.csdnimg.cn/direct/f5e04db09f1e4fb38929f19bbf781363.png
在利用string类时,必须包含头文件< string >以及using namespace std;
Member constants(成员常数)

npos

npos(全称non position)是编程中常见的特殊常量,主要用于表现无效位置或未找到的标识。
定义:std::string::npos是std::string类的静态常量成员,类型为size_t,表现字符串中不存在的索引位置。其值通常被定义为size_t类型的最大值(即-1的补码表现)。
Member functions

https://i-blog.csdnimg.cn/direct/c44ed1a3c34a4154a346e4c293d95afd.png
构造的这个API底层是个数组,string st2("hello world");将字符串拷贝给st2,底层是用模板写的,实例化出的次序表:
//class string
//{
//private:
//        char* _str;
//        size_t _size;
//        size_t _capacity;
//};
在底层上,不是把字符串指针给_str,而是_str在堆上开空间,把字符串拷贝过去。这么做的缘故原由:可能还要对字符串进行修改,对字符串有更好的管理。
#include <iostream>
#include <string>
using namespace std;

// 类模板
template<typename T>
class Stack {
public:
        Stack(size_t capacity = 4)
        {
                _array = new T;
                _capacity = capacity;
                _top = 0;
        }
        void Push(const T& data);
private:
        T* _array;
        size_t _top;
        size_t _capacity;
};
template<class T>
void Stack<T>::Push(const T& data)
{
        // 扩容
        _array = data;
        ++_top;
}

void test_string1()
{
        // 默认构造函数string();
        // 初始化空的string类对象st1,即空字符串
        string st1;

        // 都是调用含参构造函数string(const char* s);但是语法意义不一样
        // 用C格式字符串初始化string类对象st2
        string st2("hello world");
        // 类似单参数构造的隐式类型转换:
        // "hello world"构造一个string类临时对象,临时对象拷贝初始化st3,编译器会优化为直接构造
        string st3 = "hello world";

        // 引用的不是常量字符串,而是临时对象,临时对象具有常性,加const引用。
        const string& st4 = "hello world";

        // 栈中存储string类型的数据
        Stack<string> st;
        string st5("中国");
        st.Push(st5);
        // 减少代码量
        st.Push("中国");

        // 拷贝构造函数string(const string& str);
        string st6(st5);

        // 含参构造函数string(size_t n, char c); n个字符c初始化
        // 由10个字符'#'构成的字符串,开了11个char类型空间,最后存储了'\0'
        string st7(10, '#');

        // string类中有流插入、流提取运算符重载,所以可以直接输入输出string类对象
        cout << st1 << endl;
        cout << st2 << endl;
        cout << st3 << endl;
        cout << st4 << endl;
        cout << st5 << endl;
        cout << st6 << endl;
        cout << st7 << endl;

        cin >> st1;
        cout << st1 << endl;
}
int main()
{
        test_string1();
        return 0;
}
https://i-blog.csdnimg.cn/direct/a3b40a448e1d4135a3b052e84ad56ed6.png
#include <iostream>
#include <string>
using namespace std;
void test_string2()
{
        string s1("hello world");
        cout << s1 << endl;

        // string(const string& str, size_t pos, size_t len = npos);
        // 取字符串的一部分,从下标pos位置开始长度为len的字符串
        // len大于剩下字符串的长度或不传实参直接用缺省值,最后都是截取到字符串结束的位置
        string s2(s1, 4);
        cout << s2 << endl;
        string s3(s1, 5, 3);
        cout << s3 << endl;
        string s4(s1, 2, 30);
        cout << s4 << endl;

        // string(const char* s, size_t n);
        string s5("hello world", 3);
        cout << s5 << endl;

        //调用赋值运算符重载
        s1 = s5;
        cout << s1 << endl;
        s1 = "啦啦啦啦";
        cout << s1 << endl;
        s1 = 'z';
        cout << s1 << endl;
}
int main()
{
        test_string2();
        return 0;
}
https://i-blog.csdnimg.cn/direct/0a8f171aa90a4a289681fdbc5b8a1a22.png
test_string3():
size()与strlen()一样都是返回字符串的长度,不包括’\0’:
https://i-blog.csdnimg.cn/direct/a6fdde3197474ae78f8f98230fa712b8.png
https://i-blog.csdnimg.cn/direct/1a1b9a6543b44a7b832c316ccba1918e.png
遍历三种方法中,底层的是下标+[]和迭代器,上层的是范围for
遍历2
迭代器——像指针一样的东西(可能是指针,也可能不是指针),遍历(遍历读、遍历写都可以)访问容器
迭代器是通用的访问容器的方式
全部的迭代器都是在对应的类域内里定义的类型。string的迭代器string::iterator,iterator是在类域内里定义的类型,经过typedef的
左闭右开的区间:
begin()返回的是开始位置的迭代器/指针
end()返回的是最后一个数据的下一个位置的指针——包管遍历完全部有效的数据
https://i-blog.csdnimg.cn/direct/0ca3a5b8e8eb453e89cab5eea563bf66.png
任何容器都有对应的迭代器,迭代器在各自的类域内里:
https://i-blog.csdnimg.cn/direct/35770f09b9dd46ecb0b30773045ef949.png
#include <iostream>
#include <string>
#include <vector>
#include <list>
using namespace std;
//class string
//{
//public:
//   char& operator[](size_t pos)
//   {
//               assert(pos < _size);
//       return _str;
//   }
//private:
//        char* _str;
//        size_t _size;
//        size_t _capaicty;
//};

//string的遍历修改——3种方式
void test_string3()
{
        string s1("hello world");
        // string类里重载了二元运算符[],string类像数组一样去使用
        // 底层是运算符重载,上面有
        s1++;
        cout << s1 << endl;

        s1 = 'h';
        cout << s1 << endl;

        // 遍历1
        // 下标+[]
        for (size_t i = 0; i < s1.size(); i++)
        {
                s1++;
                cout << s1 << " ";
        }
        cout << endl;
        cout << s1.size() << endl;// 11
        // 运算符重载参数pos越界了会进行断言,底层相当于加了断言,上面


        // 遍历2
        // 迭代器——像指针一样的东西(可能是指针,也可能不是指针),遍历(遍历读、遍历写都可以)访问容器
        // 迭代器是通用的访问容器的方式,能跟算法一起使用(下面有逆置的算法)
        // string的迭代器string::iterator,iterator是在类域里面定义的类型,经过typedef的
        string::iterator it = s1.begin();//begin()返回的是开始位置的迭代器/指针,左闭右开的区间
        while (it != s1.end())// end是最后一个数据的下一个位置——保证遍历完所有有效的数据
        {
                (*it)--;
                ++it;
        }
        cout << endl;
        it = s1.begin();
        while (it != s1.end())
        {
                cout << *it << " ";
                ++it;
        }
        cout << endl;


        //任何容器都有对应的迭代器,迭代器在各自的类域里面
        vector<int> v;
        v.push_back(1);
        v.push_back(2);
        v.push_back(3);
        v.push_back(4);

        list<int> lt;
        lt.push_back(1);
        lt.push_back(2);
        lt.push_back(3);
        lt.push_back(4);

        vector<int>::iterator vit = v.begin();
        while (vit != v.end())
        {
                cout << *vit << " ";
                ++vit;
        }
        cout << endl;

        list<int>::iterator lit = lt.begin();
        while (lit != lt.end())
        {
                cout << *lit << " ";
                ++lit;
        }
        cout << endl;
}
int main()
{
        test_string3();
        return 0;
}
https://i-blog.csdnimg.cn/direct/a474433efd714214be130ebfee5a5dc3.png
谨慎auto做返回值、做参数。
算法库里就有逆置的函数——包含头文件< algorithm >
逆置函数模板,表现了泛型编程:
https://i-blog.csdnimg.cn/direct/fa084394e20f4baa94ec9661f5155851.png
https://i-blog.csdnimg.cn/direct/99ba16d68d244c538eb59fcaefaa2c83.png
https://i-blog.csdnimg.cn/direct/868a36de77dd4e76bea00261674fcf45.png
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <map>
#include <algorithm>//algorithm——算法
using namespace std;
// 谨慎auto做返回值、做参数
auto func1(auto x)
{
        cout << x << endl;
        return x;
}
auto func2(auto x)
{
        return func1(x);
}
auto func3(auto x)
{
        return func2(x);
}
void test_string4()
{
        /* 3个容器,对容器里的数据逆置 */
        // 算法库里就有逆置的函数——包算法的头文件<algorithm>
        string s1("hello world");
        vector<int> v;
        v.push_back(1);
        v.push_back(2);
        v.push_back(3);
        v.push_back(4);

        list<int> lt;
        lt.push_back(10);
        lt.push_back(20);
        lt.push_back(30);
        lt.push_back(40);

        reverse(v.begin(), v.end());
        reverse(lt.begin(), lt.end());
        reverse(s1.begin(), s1.end());


        /* 遍历3
           范围for (C++11) */
           // 自动取容器数据依次赋值给对象,自动判断结束
           // auto可以替换成容器里的数据类型,但是一般都是写auto,容器是什么数据类型让它自动推
           // 遍历读(没问题)
           // 遍历写的问题在于遍历修改后的输出结果还是不变,原因是自动取容器数据是依次拷贝给对象,对象只是个拷贝,
   //改变e只是改变了拷贝,并没有修改容器里的数据,所以要用auto引用,e就是容器数据的别名

           //for (auto e : s1)// e(element)就是个变量/对象名,随便取就行
        for (auto& e : s1)
        {
                e--;
        }
        // for (auto e : s1)
        for (char e : s1)
        {
                cout << e << " ";
        }
        cout << endl;

        //for (int x : v)
        for (auto x : v)
        {
                cout << x << " ";
        }
        cout << endl;

        //for (int e : lt)
        for (auto e : lt)
        {
                cout << e << " ";
        }
        cout << endl;

        // 范围for原理:编译时替换成迭代器,会把下面的一段代码替换成string迭代器的代码
        for (auto& e : s1)
        {
                e--;
        }

        //遍历数组也可以用范围for,这里范围for是替换成访问数组的指针
        //指向数组的指针是一种天然的迭代器
        int a[] = { 1, 2, 3, 4 };
        for (auto e : a)
        {
                cout << e << " ";
        }
        cout << endl;


        /* auto的介绍 */
        int i = 0;
        int j = i;
        // 自动通过右边初始化值推导k的类型
        auto k = i;

        // auto第一个价值:替代长类型
        // list<int>::iterator lit = lt.begin();
        auto lit = lt.begin();
        while (lit != lt.end())
        {
                cout << *lit << " ";
                ++lit;
        }
        cout << endl;
        // vector<int>::iterator vit = v.begin();
        auto vit = v.begin();
        while (vit != v.end())
        {
                cout << *vit << " ";
                ++vit;
        }
        cout << endl;

        // map示例。在项目中不让展开命名空间里的所有成员。迭代器代码冗长,但是代码可读性强,
        // 用auto就简便很多了,但是auto可读性不强,需要知道右边返回值类型
        std::map<std::string, std::string> dict;
        // std::map<std::string, std::string>::iterator dit = dict.begin();
        auto dit = dict.begin();

        // auto声明指针,两种写法等价,但是auto和auto*有区别。
        auto p1 = &i;//右边可以传非指针。传指针,推出来是int*,不传指针auto p1 = i;,推出来是int
        auto* p2 = &i;//右边必须传指针,不传指针就会报错。这里指定了必须是指针

        auto& ref = i;// 推导auto是int类型,ref是i的别名

        // auto第二个价值:auto可以做参数,也可以做返回值。
        // 但是建议谨慎使用,代码量大的话,查找函数的返回值类型会很麻烦,代码的可读性减弱
        auto ret = func1(i);
        cout << ret << endl;

        //auto xx = func3(11);// 这里的代码量不算大,但是在几十个函数中查找该函数的返回值类型会很麻烦,代码的可读性减弱
}
int main()
{
        test_string4();
        return 0;
}
https://i-blog.csdnimg.cn/direct/cb9cc4ad72ac4483aaf37c8865e135ba.png
可以把string想象成内部有一个指针、有size、capacity,就跟次序表的结构一样,其实就是一个字符次序表,只是它跟字符次序表的区别是string最后会多一 个’\0’。
字符串末尾’\0’是个标识字符,不算有效的字符
Iterators——迭代器

四种迭代器:正向迭代器iterator、const正向迭代器const_iterator、反向迭代器reversr_iterator、const反向迭代器const_reverse_iterator。
普通迭代器:正/反向迭代器。普通迭代器都是可读可修改字符串的。
const迭代器:const正/反向迭代器。const迭代器只读字符串,不能修改字符串。
正/反向迭代器开口方向不同:正向迭代器开口方向向右,++就会向右边走,–就会向左边走;反向迭代器开口方向向左,++就会向左边走,–就会向右边走。
rbegin()在最后一个数据的位置(实际底层实现会有差别,在反向迭代器的底层实现中会解说),迭代器都是左闭右开的,所以rend()在第一个数据的前一个位置。
权限可以平移、缩小,但是不能放大。普通对象、const对象都可以调用const成员函数。像string类中,普通成员函数、const成员函数都存在,就会调用更匹配的。但是const的迭代器不能给普通迭代器,权限不能放大;(普通迭代器能给const迭代器,权限能缩小)
it不是常量,*it是常量。it是常量的话怎么能++呢。
注意是const_iterator,不是const iterator,由于const iterator是修饰迭代器(指针)本身不能被修改,但是我们是让迭代器(指针)指向内容不能被修改,而迭代器本身是能被修改的,类比const T* ptr。
普通对象调用普通成员函数begin()/rbegin(),返回的是普通迭代器;const对象调用const成员函数begin()/rbegin(),返回的是const迭代器。
begin()、 end()、rbegin()、rend()每个函数都有两个版本,也就是普通成员函数、const成员函数都在,对象调用时就会调用更匹配的;而在前面加c的意思是确定就是const版本,实际不太常用。
https://i-blog.csdnimg.cn/direct/059cd6f493e74316b9fe0c19f08ac91a.png
#include <iostream>
#include <string>
using namespace std;
void func(const string& s)
{
        //string::iterator it = s.begin();// error
        string::const_iterator it = s.begin();
        while (it != s.end())
        {
                //(*it)++;// error
                cout << *it << " ";
                ++it;
        }
        cout << endl;
}
void test_string1()
{
        // 正/反向迭代器开口方向不同
        string s1("hello world");
        string::iterator it = s1.begin();
        while (it != s1.end())
        {
                (*it)++;
                cout << *it << " ";
                ++it;
        }
        cout << endl;

        // rbegin()在最后一个数据的位置(实际底层实现会有差别,在反向迭代器的底层实现中会讲解)
        // 迭代器都是左闭右开的,所以rend()在第一个数据的前一个位置
        string::reverse_iterator rit = s1.rbegin();
        while (rit != s1.rend())
        {
                (*rit)--;
                cout << *rit << " ";
                ++rit;
        }
        cout << endl;
       
        //像string类中,普通、const的成员函数都存在,就会调用更匹配的
        func(s1);
}
https://i-blog.csdnimg.cn/direct/54da96f64cf044838417ca00f332bca2.png
Capacity——容量

size()、length()都表现返回的字符个数,不包含标识字符’\0’。
推荐用size(),string类早于STL,所以在文档中并没有归并到容器里,某种水平上string类算标准库一部分,但是从归类上,string类和容器是一类的。早期设计的时间,由于只思量string,所以是length(),后面从STL的角度又加了size()。
size()更通用一些,length()更针对一些。length()对字符串没有题目,对链表也勉强,但是对树就行不通了。
全部容器里都有size接口,表现数据个数。
max_size能给到的最大长度,不管字符串是长是短,给到的最大长度都是一样的。
capacity返回容量的大小,表现能存储的有效字符个数。例如"hello world",capacity是15,表明能存储的有效字符个数是15,但是实际空间大小是16,会包含标识字符’\0’。
clear清理数据接口,相当于把size()清理成0,但是不会清理空间capacity()。
shrink_to_fit缩容接口,哀求capacity()变革到跟size()一样。真要缩容的话就用这个接口,不要用reserve缩容。假设一共20个字节的空间,size是10,缩容做不到把后10个字节的空间释放,由于系统提供内存释放的接口是不答应分段释放的,只能重新释放整个20个字节大小的空间,所以一样平常都是异地缩容,就是再申请10个字节的空间,拷贝数据,释放原20个字节空间。很少利用这个接口,了解一下。
string、vector底层是数组结构的,肯定是做缩容的,clear()数据不缩容。
void test_string2()
{
        // size()、length()都表示返回的字符个数,不包含标识字符'\0'
        // 推荐用size()
        string s1("hello world");
        cout << s1.size() << endl;
        cout << s1.length() << endl;
        // max_size能给到的最大长度,不管字符串是长是短,给到的最大长度都是一样的
        cout << s1.max_size() << endl;
        string s2;
        cout << s2.max_size() << endl;
        // capacity返回容量的大小,表示能存储的有效字符个数。例如"hello world",capacity是15,表明能存储的有效字符个数是15,但是实际空间大小是16,会包含标识字符'\0'
        cout << s1.capacity() << endl;
        cout << s2.capacity() << endl;
        // empty()判空
        if (!s1.empty())
        {
                cout << s1 << endl;
        }
        // clear()清理数据,相当于把size()清理成0,但是不会清理空间capacity()
        s1.clear();
        cout << s1.size() << endl;
        cout << s1.capacity() << endl;
}
https://i-blog.csdnimg.cn/direct/eaf0b5e6f3e34730954831e60a26ea4c.png
在了解resize、reserve之前,先了解一下push_back接口(尾插一个字符),观察string是怎样扩容的?
void test_string3()
{
        string s;
        size_t old = s.capacity();
        cout << old << endl;
        for (size_t i = 0; i < 200; ++i)
        {
                s.push_back('x');
                if (old != s.capacity())//old == s.capacity()没扩容,!=就是扩容了
                {
                        //扩容
                        cout << s.capacity() << endl;
                        old = s.capacity();
                }
        }
}
https://i-blog.csdnimg.cn/direct/43e392c8c92849f08457a931cf59992f.png
观察string的扩容规则。C++标准库只是个规范,不同平台下STL怎样实现取决于平台本身。vs下除了第一次2倍扩容,剩下都是1.5倍扩容。Linux下g++编译器是标准的2倍扩容。这些扩容一样平常都是异地扩容,异地扩容就会有消耗,效率不高。C++里new了空间之后,空间不敷用就要本身异地扩容了(后面会解说底层实现)
reserve和resize

reserve: https://legacy.cplusplus.com/reference/string/string/reserve/
reserve(保留)与reverse(逆置)区分。
reserve本质是一个扩容接口。文档里给出了2种环境,若n比capacity大,capacity肯定会扩容到n个字符的长度或更长(更长是由于思量了内存对齐,取决于平台);若比n小,不愿定会缩容。文档中给出non-binding,也就是没有束缚力的,是否缩小取决于平台,就不要用reserve缩容了。所以reserve主要是用于扩容。
reserve不会改变字符串长度(size)和数据内容的。例如,capacity是100,size是50,reserve是30,就算缩容也不会将容量缩到30,而是缩到50,size和数据不会丢。
reserve主要用于确定知道需要的空间大小(capacity),提前用reserve直接开好空间,这样可以镌汰异地扩容的次数,提高效率。
void test_string3()
{
        string s;
        s.reserve(200);//知道需要的空间就提前开好
        size_t old = s.capacity();
        cout << old << endl;
        for (size_t i = 0; i < 200; ++i)
        {
                s.push_back('x');
                if (old != s.capacity())//old == s.capacity()没扩容,!=就是扩容了
                {
                        //扩容
                        cout << s.capacity() << endl;
                        old = s.capacity();
                }
        }
}
有了reserve就不存在多次异地扩容了。扩容结果,取决于平台。vs下capacity结果可能会比200大,Linux的g++下capacity是200。
https://i-blog.csdnimg.cn/direct/729b84921d3343a3a4a2a5b389fdb85e.png
void test_string3()
{
        string s1("xxxxxxxxxxxxxxxxxxx");
        s1.reserve(100);
        cout << s1.size() << endl;
        cout << s1.capacity() << endl;

        //缩容
        s1.reserve(10);
        cout << s1.size() << endl;
        cout << s1.capacity() << endl;
}
对于reserve是否缩容是取决于平台的。vs下是绝对不缩容的。Linux的g++下缩容了。所以照旧不要用reserve缩容了。
https://i-blog.csdnimg.cn/direct/7ef82c312df046eca19f500de5626d6f.png
size:https://legacy.cplusplus.com/reference/string/string/resize/
resize是改变size的接口(本质是插入、删除数据,也可能会改变capacity),有3种环境,如果size是18,capacity是31:

[*]size < n < capacity:插入数据。若n = 25,size要达到25只能插入数据。
[*]n > capacity:扩容+插入数据。
[*]n < size:删除数据。
resize有两个版本的接口:
https://i-blog.csdnimg.cn/direct/5433f8713dd64de590bba094ac67f8b7.png
https://i-blog.csdnimg.cn/direct/fc567793bfb443d588874e86760747eb.png
https://i-blog.csdnimg.cn/direct/03bd6c54a4b74c61abe082fb6ec23c81.png
void test_string4()
{
        string s("xxxxxxxxxxxx");
        cout << s.size() << endl;
        cout << s.capacity() << endl;

        s.resize(15);
        cout << s.size() << endl;
        cout << s.capacity() << endl;

        s.resize(20, 'y');
        cout << s << endl;
        cout << s.size() << endl;
        cout << s.capacity() << endl;

        s.resize(5);
        cout << s << endl;
        cout << s.size() << endl;
        cout << s.capacity() << endl;
}
https://i-blog.csdnimg.cn/direct/ef0a1eaa62cd4e1e9155b81a6c41358a.png
Element access——元素存取

operator[] 接口是获取pos位置的字符。operator[]接口有两个版本。普通对象调用普通版本可以对pos位置的字符进行修改、读;const对象调用const版本只能对pos位置的字符进行读。
at接口也有两个版本,at和operator[]功能一样,只不过operator[]是以运算符重载的形式呈现,at是以函数的形式。
operator[]和at区别:对于越界的检查方式不一样,operator[]底层有断言,断言检查越界(断言会直接中断步伐,是一种更激进的检查方式,断言不能捕获,但是断言在release下会被忽略,只能在debug下有作用);at会抛异常,需要本身捕获,捕获后步伐还能运行。
void test_string4()
{
        s = 'm';
        cout << s << endl;
        //s;//断言检查越界
       
        s.at(1) = 'b';
        cout << s << endl;
        //抛异常检查越界
        s.at(10);
}
int main()
{
        try {
                test_string4();
        }
        // 捕获at抛出的异常
        catch (const exception& e)
        {
                cout << e.what() << endl;
        }
        return 0;
}
https://i-blog.csdnimg.cn/direct/b398eb115b584085bdb3d7d304c76c18.png
back/front返回结束/开始的字符,也能修改(普通版本:可读可修改)(const版本:只能读),用的少,由于可以直接用operator[]或at访问。
Modifiers——修改

最常用operator+=,其次就是insert、erase。此中的swap接口在底层实现中解说。
尾部追加数据:

[*] push_back追加一个字符。
[*] append内里有一组函数,但是设计的有点冗余,最常用的就是C++98中的第三个string& append(const char* s)再加第一个,即可以追加常量字符串或string类对象。也可以追加迭代区间的值,也可以取迭代区间的一部分(迭代区间都是左闭右开)。
[*] 追加string类对象时会在原string类对象中的’\0’后再追加新的类对象吗?也就是说会有两个标识字符’\0’吗?注意不会有两个标识字符’\0’,在追加后的字符串后面才会有’\0’,且只有这一个。
[*] 前面的了解一下,string类里追加数据,operator+= 反而是更好用的接口,可以追加一个常量字符串/一个string类对象/一个字符。
void test_string5()
{
        /*尾部追加数据*/
        /*operator+=*/
        string s1("xxxxxx");
        cout << (s1 += "mmmm") << endl;
        string s2("hello world");
        s1 += s2;
        cout << s1 << endl;
        cout << (s1 += 'k') << endl;
        //append尾部插入多个字符,里面有一组函数,但是设计的有点冗余,最常用的就是C++98中的第三个string& append(const char* s);再加第一个
        //即可以追加常量字符串或string类对象
        s1.append("opq");
        cout << s1 << endl;
        s1.append(s2);
        cout << s1 << endl;
        s1.append(s2, 0, 3);
        cout << s1 << endl;
        s1.append("hello", 2);
        cout << s1 << endl;
        s1.append(3, 'v');
        cout << s1 << endl;
        //也可以追加迭代区间的值,也可以取迭代区间的一部分(迭代区间都是左闭右开)
        s1.append(s2.begin(), s2.end() - 6);
        cout << s1 << endl;
        s1.append(++s1.begin(), --s1.end());
        cout << s1 << endl;

        /*尾删*/
        //pop_back()
        s1.pop_back();
        cout << s1 << endl;
}
https://i-blog.csdnimg.cn/direct/8fa2f5588e50403b972e3b593fb7f61c.png
不支持提供头插和头删的接口,由于像string、vector这种次序数组结构,头插和头删的效率太低了。
不过可以通过insert、erase间接实现头插和头删。
由于string出现的比较早,没有参考的地方,接口设计的函数就会冗余
insert最常用的就是在pos位置的字符之前插入一个字符、一个字符串、一个string类对象,insert要谨慎利用,是一个时间复杂度为O(n)的接口。谨慎利用insert,由于底层要挪动数据,再插入,效率不高。
erase删除某个迭代器位置、从pos位置删除len个字符(若字符串太短,有多少删除多少;若给的是缺省值(npos是42亿9000万)删除到结尾)、删除迭代器区间。谨慎利用erase,由于底层要挪动数据覆盖删除。
assign赋值,很少用
void test_string6()
{
        //不过可以通过insert、erase间接实现头插和头删
        string s1("hello world");
        string s2 = "stuvwxyz";
        s1.insert(5, s2);
        cout << s1 << endl;
        s1.insert(3, s2, 0, 3);
        cout << s1 << endl;
        s2.insert(0, "opqr");
        cout << s2 << endl;
        s2.insert(0, "lmnxxx", 3);
        cout << s2 << endl;
        s2.insert(2, 3, 'x');
        cout << s2 << endl;
        string s3("abcdefg");
        s3.insert(++s3.begin(), 3, 'y');
        cout << s3 << endl;
        s3.insert(s3.end(), 'h');
        cout << s3 << endl;
        string s4("I love C++");
        s3.insert(s3.begin(), s4.begin(), s4.end());
        cout << s3 << endl;

        //erase删除某个迭代器位置、从pos位置删除len个字符(若字符串太短,有多少删除多少;若给的是缺省值(npos是42亿9000万)删除到结尾)、删除迭代器区间
        string s5("hello world");
        s5.erase(2, 5);
        cout << s5 << endl;
        s5.erase(5);
        cout << s5 << endl;
        s5.erase(s5.begin());
        cout << s5 << endl;
        s5.erase(++s5.begin(), --s5.end());//左闭右开,删除字符的个数 == 区间相减
        cout << s5 << endl;

        //assign赋值,很少用
}
https://i-blog.csdnimg.cn/direct/f409a9911dd748d18c2cb3830cc642c0.png
谨慎利用replace,由于底层要挪动数据。replace更换,实践当中用处不大。靠前的少数字符更换成多字符后会往后挪动数据,效率不高。同理,多更换成少的也效率不高,数据会往前挪动。只有同比例更换效率才高。
void test_string7()
{
        //谨慎使用replace,因为底层要挪动数据
        string s1("hello world");
        cout << s1 << endl;
        s1.replace(5, 1, "%%%%%");
        cout << s1 << endl;
        //一个字符串中的空格替换成两个%%,下面写法效率极低
        string s2("hello world hello bit");
        for (size_t i = 0; i < s2.size(); ++i)
        {
                if (s2 == ' ')
                {
                        s2.replace(i, 1, "%%");
                }
        }
        cout << s2 << endl;
        //新思路
        //用reverse减少扩容
        string s3("hello world hello bit");
        string s4;
        s4.reserve(s3.size());
        for (auto ch : s3)
        {
                if (ch != ' ')
                {
                        s4 += ch;
                }
                else {
                        s4 += "%%";
                }
        }
        s3 = s4;
        cout << s3 << endl;
}
https://i-blog.csdnimg.cn/direct/9f441044302f4f4dac68467db371e1c3.png
String operations——string操作

c_str返回底层c风格的字符串,string底层就是一个字符串,c_str本质就是兼容C语言的接口。
windows文件系统不区分大小写,string s1(“Test.cpp”);中的Test.cpp任意一个字符大小写都行,打开的都是同一个文件。
data跟c_str功能差不多,就是跟size、lenth的逻辑一样,用c_str用的多。
get_allocator很少用。
substr比copy好用,substr取pos位置的len个字符去构建一个string对象返回。
find相干接口:
find默认从0(缺省值)位置依次往后去查找一个字符/一个字符串/一个string类对象,当然也可以本身给一个值,从该位置查找。
npos:整型的最大值,42亿9000万。
find()+substr取子串的共同.后缀比较多的,找真正的后缀就倒着找rfind()
url网址,任何一个网站都有网址,网址是一个字符串,剖析出网址的三个部分协议https、域名legacy…com、资源reference…
左闭右开(右在最后一个数据的下一个位置),区间一减就是中间字符个数。左闭右闭,例如区间一减,少一个数
find_first_of 从前往后找给的字符串匹配参数里的字符串中的任意一个字符,找到就返回,匹配的就屏蔽为’#’
find_last_of 从后往前找…
find_frist_not_of 从前往后找给的字符串不匹配参数里的字符串中的任意一个字符,找到就返回。相当于在给的字符串中只保参数中的字符
void test_string8()
{
        string s1("Test.cpp");
        //FILE* fout = fopen(s1.c_str(), "r");
        //char ch = fgetc(fout);
        //while (ch != EOF)
        //{
        //        cout << ch;
        //        ch = fgetc(fout);
        //}

        // find()+substr取子串的配合。后缀比较多的,找真正的后缀就倒着找rfind()
        string ret;
        string s2("Test.cpp.tar.zip");
        size_t pos = s2.rfind('.');
        if (pos != string::npos)
        {
                ret = s2.substr(pos);
        }
        cout << ret << endl;

        //url网址,任何一个网站都有网址,网址是一个字符串,解析出网址的三个部分协议https、域名legacy....com、资源reference....
        string url("https://legacy.cplusplus.com/reference/string/string/find/");
        size_t pos1 = url.find(':');
        if (pos1 != string::npos)
        {
                string sub1 = url.substr(0, pos1);
                cout << sub1 << endl;
        }
        size_t pos2 = url.find('/', pos1 + 3);
        if (pos2 != string::npos)
        {
                string sub2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));
                cout << sub2 << endl;

                string sub3 = url.substr(pos2 + 1);
                cout << sub3 << endl;
        }
        //find_first_of 从前往后找给的字符串匹配参数里的字符串中的任意一个字符,找到就返回,匹配的就屏蔽为'#'
        //find_last_of从后往前找...
        //find_frist_not_of 从前往后找给的字符串不匹配参数里的字符串中的任意一个字符,找到就返回。相当于在给的字符串中只保参数中的字符
        std::string str("Please, replace the vowels in this sentence by asterisks.");
        std::size_t found = str.find_first_not_of("abcd");
        while (found != std::string::npos)
        {
                str = '#';
                found = str.find_first_not_of("abcd", found + 1);
        }
        std::cout << str << '\n';
}
https://i-blog.csdnimg.cn/direct/d4fc22aa234b4820902cc71b392c3332.png
与string类相干的函数重载玉成局的

relational operators 中的每个运算符重载的后两个版本设计得有点冗余,由于支持单参数隐式类型转换,所以只用第一个版本也是可以的。为什么会重载玉成局的呢?左操作数可能是非当前重载类的类型。
https://i-blog.csdnimg.cn/direct/db5fa308a8c04b7baa018758e06943f3.png
operator+:
https://i-blog.csdnimg.cn/direct/901302ebfb9d447b9624175ab65a67a9.png
流提取>>与scanf:流提取不会有越界的题目,底层会动态扩容。C中scanf可以输入字符串到字符数组里,但是要提前盘算好空间,malloc()出来的也要提前盘算好,C++就相对来说更机动。
但是流提取有缺陷,例如下面的例子,输入hello world时,只会输出hello。scanf和cin都有一个规定,空格或换行时会做多项之间的分割。
https://i-blog.csdnimg.cn/direct/a8a3d71fc42345c28937695219df53ec.png
也就是说若这个字符串中就包含空格,那就不能用流提取了,怎么办理这种题目呢?——getline,获取一行
https://i-blog.csdnimg.cn/direct/6c755dc2dcb346eb9ff6dc992743bddd.png
第二个版本默认是以换行作为结束符。第一个版本可以自定义输入结束符,即可以换行继续输入,调试可以看到换行字符。
https://i-blog.csdnimg.cn/direct/817f4c008c4b4cadb74468588c67bde9.png
https://i-blog.csdnimg.cn/direct/51fac09e20ac450a9797d50c30e2fc70.png
四、字符串与整型/浮点数的互相转换

https://i-blog.csdnimg.cn/direct/4fc253439cb847a9bccd67dc7d9201d0.png
stoi常用
https://i-blog.csdnimg.cn/direct/3d4573ac049748868fa24f2dd0523d9e.png
浮点数跟转换后的字符串可能会不一样,浮点数不能精确存储。(C语言:数据在内存中的存储)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 第五讲(上) | string类的利用