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

打印 上一主题 下一主题

主题 1780|帖子 1780|积分 5340

部分参考了“爱扑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这里会解说的细致一些,后面的容器不会解说这么细致,由于很多地方都是相似的

在利用string类时,必须包含头文件< string >以及using namespace std;
Member constants(成员常数)

npos

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


构造的这个API底层是个数组,string st2("hello world");将字符串拷贝给st2,底层是用模板写的,实例化出的次序表:
  1. //class string
  2. //{
  3. //private:
  4. //        char* _str;
  5. //        size_t _size;
  6. //        size_t _capacity;
  7. //};
复制代码
在底层上,不是把字符串指针给_str,而是_str在堆上开空间,把字符串拷贝过去。这么做的缘故原由:可能还要对字符串进行修改,对字符串有更好的管理。
  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. // 类模板
  5. template<typename T>
  6. class Stack {
  7. public:
  8.         Stack(size_t capacity = 4)
  9.         {
  10.                 _array = new T[capacity];
  11.                 _capacity = capacity;
  12.                 _top = 0;
  13.         }
  14.         void Push(const T& data);
  15. private:
  16.         T* _array;
  17.         size_t _top;
  18.         size_t _capacity;
  19. };
  20. template<class T>
  21. void Stack<T>::Push(const T& data)
  22. {
  23.         // 扩容
  24.         _array[_top] = data;
  25.         ++_top;
  26. }
  27. void test_string1()
  28. {
  29.         // 默认构造函数string();
  30.         // 初始化空的string类对象st1,即空字符串
  31.         string st1;
  32.         // 都是调用含参构造函数string(const char* s);  但是语法意义不一样
  33.         // 用C格式字符串初始化string类对象st2
  34.         string st2("hello world");
  35.         // 类似单参数构造的隐式类型转换:
  36.         // "hello world"构造一个string类临时对象,临时对象拷贝初始化st3,编译器会优化为直接构造
  37.         string st3 = "hello world";
  38.         // 引用的不是常量字符串,而是临时对象,临时对象具有常性,加const引用。
  39.         const string& st4 = "hello world";
  40.         // 栈中存储string类型的数据
  41.         Stack<string> st;
  42.         string st5("中国");
  43.         st.Push(st5);
  44.         // 减少代码量
  45.         st.Push("中国");
  46.         // 拷贝构造函数string(const string& str);
  47.         string st6(st5);
  48.         // 含参构造函数string(size_t n, char c); n个字符c初始化
  49.         // 由10个字符'#'构成的字符串,开了11个char类型空间,最后存储了'\0'
  50.         string st7(10, '#');
  51.         // string类中有流插入、流提取运算符重载,所以可以直接输入输出string类对象
  52.         cout << st1 << endl;
  53.         cout << st2 << endl;
  54.         cout << st3 << endl;
  55.         cout << st4 << endl;
  56.         cout << st5 << endl;
  57.         cout << st6 << endl;
  58.         cout << st7 << endl;
  59.         cin >> st1;
  60.         cout << st1 << endl;
  61. }
  62. int main()
  63. {
  64.         test_string1();
  65.         return 0;
  66. }
复制代码

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. void test_string2()
  5. {
  6.         string s1("hello world");
  7.         cout << s1 << endl;
  8.         // string(const string& str, size_t pos, size_t len = npos);
  9.         // 取字符串的一部分,从下标pos位置开始长度为len的字符串
  10.         // len大于剩下字符串的长度或不传实参直接用缺省值,最后都是截取到字符串结束的位置
  11.         string s2(s1, 4);
  12.         cout << s2 << endl;
  13.         string s3(s1, 5, 3);
  14.         cout << s3 << endl;
  15.         string s4(s1, 2, 30);
  16.         cout << s4 << endl;
  17.         // string(const char* s, size_t n);
  18.         string s5("hello world", 3);
  19.         cout << s5 << endl;
  20.         //调用赋值运算符重载
  21.         s1 = s5;
  22.         cout << s1 << endl;
  23.         s1 = "啦啦啦啦";
  24.         cout << s1 << endl;
  25.         s1 = 'z';
  26.         cout << s1 << endl;
  27. }
  28. int main()
  29. {
  30.         test_string2();
  31.         return 0;
  32. }
复制代码

test_string3():
size()与strlen()一样都是返回字符串的长度,不包括’\0’:


遍历三种方法中,底层的是下标+[]和迭代器,上层的是范围for
遍历2
迭代器——像指针一样的东西(可能是指针,也可能不是指针),遍历(遍历读、遍历写都可以)访问容器
迭代器是通用的访问容器的方式
全部的迭代器都是在对应的类域内里定义的类型。string的迭代器string::iterator,iterator是在类域内里定义的类型,经过typedef的
左闭右开的区间:
begin()返回的是开始位置的迭代器/指针
end()返回的是最后一个数据的下一个位置的指针——包管遍历完全部有效的数据

任何容器都有对应的迭代器,迭代器在各自的类域内里:

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <list>
  5. using namespace std;
  6. //class string
  7. //{
  8. //public:
  9. //   char& operator[](size_t pos)
  10. //   {
  11. //                 assert(pos < _size);
  12. //       return _str[pos];
  13. //   }
  14. //private:
  15. //        char* _str;
  16. //        size_t _size;
  17. //        size_t _capaicty;
  18. //};
  19. //string的遍历修改——3种方式
  20. void test_string3()
  21. {
  22.         string s1("hello world");
  23.         // string类里重载了二元运算符[],string类像数组一样去使用
  24.         // 底层是运算符重载,上面有
  25.         s1[0]++;
  26.         cout << s1 << endl;
  27.         s1[0] = 'h';
  28.         cout << s1 << endl;
  29.         // 遍历1
  30.         // 下标+[]
  31.         for (size_t i = 0; i < s1.size(); i++)
  32.         {
  33.                 s1[i]++;
  34.                 cout << s1[i] << " ";
  35.         }
  36.         cout << endl;
  37.         cout << s1.size() << endl;// 11
  38.         // 运算符重载参数pos越界了会进行断言,底层相当于加了断言,上面
  39.         // 遍历2
  40.         // 迭代器——像指针一样的东西(可能是指针,也可能不是指针),遍历(遍历读、遍历写都可以)访问容器
  41.         // 迭代器是通用的访问容器的方式,能跟算法一起使用(下面有逆置的算法)
  42.         // string的迭代器string::iterator,iterator是在类域里面定义的类型,经过typedef的
  43.         string::iterator it = s1.begin();//begin()返回的是开始位置的迭代器/指针,左闭右开的区间
  44.         while (it != s1.end())// end是最后一个数据的下一个位置——保证遍历完所有有效的数据
  45.         {
  46.                 (*it)--;
  47.                 ++it;
  48.         }
  49.         cout << endl;
  50.         it = s1.begin();
  51.         while (it != s1.end())
  52.         {
  53.                 cout << *it << " ";
  54.                 ++it;
  55.         }
  56.         cout << endl;
  57.         //任何容器都有对应的迭代器,迭代器在各自的类域里面
  58.         vector<int> v;
  59.         v.push_back(1);
  60.         v.push_back(2);
  61.         v.push_back(3);
  62.         v.push_back(4);
  63.         list<int> lt;
  64.         lt.push_back(1);
  65.         lt.push_back(2);
  66.         lt.push_back(3);
  67.         lt.push_back(4);
  68.         vector<int>::iterator vit = v.begin();
  69.         while (vit != v.end())
  70.         {
  71.                 cout << *vit << " ";
  72.                 ++vit;
  73.         }
  74.         cout << endl;
  75.         list<int>::iterator lit = lt.begin();
  76.         while (lit != lt.end())
  77.         {
  78.                 cout << *lit << " ";
  79.                 ++lit;
  80.         }
  81.         cout << endl;
  82. }
  83. int main()
  84. {
  85.         test_string3();
  86.         return 0;
  87. }
复制代码


谨慎auto做返回值、做参数。
算法库里就有逆置的函数——包含头文件< algorithm >
逆置函数模板,表现了泛型编程:



  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <list>
  5. #include <map>
  6. #include <algorithm>//algorithm——算法
  7. using namespace std;
  8. // 谨慎auto做返回值、做参数
  9. auto func1(auto x)
  10. {
  11.         cout << x << endl;
  12.         return x;
  13. }
  14. auto func2(auto x)
  15. {
  16.         return func1(x);
  17. }
  18. auto func3(auto x)
  19. {
  20.         return func2(x);
  21. }
  22. void test_string4()
  23. {
  24.         /* 3个容器,对容器里的数据逆置 */
  25.         // 算法库里就有逆置的函数——包算法的头文件<algorithm>
  26.         string s1("hello world");
  27.         vector<int> v;
  28.         v.push_back(1);
  29.         v.push_back(2);
  30.         v.push_back(3);
  31.         v.push_back(4);
  32.         list<int> lt;
  33.         lt.push_back(10);
  34.         lt.push_back(20);
  35.         lt.push_back(30);
  36.         lt.push_back(40);
  37.         reverse(v.begin(), v.end());
  38.         reverse(lt.begin(), lt.end());
  39.         reverse(s1.begin(), s1.end());
  40.         /* 遍历3
  41.            范围for (C++11) */
  42.            // 自动取容器数据依次赋值给对象,自动判断结束
  43.            // auto可以替换成容器里的数据类型,但是一般都是写auto,容器是什么数据类型让它自动推
  44.            // 遍历读(没问题)
  45.            // 遍历写的问题在于遍历修改后的输出结果还是不变,原因是自动取容器数据是依次拷贝给对象,对象只是个拷贝,
  46.    //改变e只是改变了拷贝,并没有修改容器里的数据,所以要用auto引用,e就是容器数据的别名
  47.            //for (auto e : s1)// e(element)就是个变量/对象名,随便取就行
  48.         for (auto& e : s1)
  49.         {
  50.                 e--;
  51.         }
  52.         // for (auto e : s1)
  53.         for (char e : s1)
  54.         {
  55.                 cout << e << " ";
  56.         }
  57.         cout << endl;
  58.         //for (int x : v)
  59.         for (auto x : v)
  60.         {
  61.                 cout << x << " ";
  62.         }
  63.         cout << endl;
  64.         //for (int e : lt)
  65.         for (auto e : lt)
  66.         {
  67.                 cout << e << " ";
  68.         }
  69.         cout << endl;
  70.         // 范围for原理:编译时替换成迭代器,会把下面的一段代码替换成string迭代器的代码
  71.         for (auto& e : s1)
  72.         {
  73.                 e--;
  74.         }
  75.         //遍历数组也可以用范围for,这里范围for是替换成访问数组的指针
  76.         //指向数组的指针是一种天然的迭代器
  77.         int a[] = { 1, 2, 3, 4 };
  78.         for (auto e : a)
  79.         {
  80.                 cout << e << " ";
  81.         }
  82.         cout << endl;
  83.         /* auto的介绍 */
  84.         int i = 0;
  85.         int j = i;
  86.         // 自动通过右边初始化值推导k的类型
  87.         auto k = i;
  88.         // auto第一个价值:替代长类型
  89.         // list<int>::iterator lit = lt.begin();
  90.         auto lit = lt.begin();
  91.         while (lit != lt.end())
  92.         {
  93.                 cout << *lit << " ";
  94.                 ++lit;
  95.         }
  96.         cout << endl;
  97.         // vector<int>::iterator vit = v.begin();
  98.         auto vit = v.begin();
  99.         while (vit != v.end())
  100.         {
  101.                 cout << *vit << " ";
  102.                 ++vit;
  103.         }
  104.         cout << endl;
  105.         // map示例。在项目中不让展开命名空间里的所有成员。迭代器代码冗长,但是代码可读性强,
  106.         // 用auto就简便很多了,但是auto可读性不强,需要知道右边返回值类型
  107.         std::map<std::string, std::string> dict;
  108.         // std::map<std::string, std::string>::iterator dit = dict.begin();
  109.         auto dit = dict.begin();
  110.         // auto声明指针,两种写法等价,但是auto和auto*有区别。
  111.         auto p1 = &i;//右边可以传非指针。传指针,推出来是int*,不传指针auto p1 = i;,推出来是int
  112.         auto* p2 = &i;//右边必须传指针,不传指针就会报错。这里指定了必须是指针
  113.         auto& ref = i;// 推导auto是int类型,ref是i的别名
  114.         // auto第二个价值:auto可以做参数,也可以做返回值。
  115.         // 但是建议谨慎使用,代码量大的话,查找函数的返回值类型会很麻烦,代码的可读性减弱
  116.         auto ret = func1(i);
  117.         cout << ret << endl;
  118.         //auto xx = func3(11);// 这里的代码量不算大,但是在几十个函数中查找该函数的返回值类型会很麻烦,代码的可读性减弱
  119. }
  120. int main()
  121. {
  122.         test_string4();
  123.         return 0;
  124. }
复制代码


可以把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版本,实际不太常用。

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. void func(const string& s)
  5. {
  6.         //string::iterator it = s.begin();// error
  7.         string::const_iterator it = s.begin();
  8.         while (it != s.end())
  9.         {
  10.                 //(*it)++;// error
  11.                 cout << *it << " ";
  12.                 ++it;
  13.         }
  14.         cout << endl;
  15. }
  16. void test_string1()
  17. {
  18.         // 正/反向迭代器开口方向不同
  19.         string s1("hello world");
  20.         string::iterator it = s1.begin();
  21.         while (it != s1.end())
  22.         {
  23.                 (*it)++;
  24.                 cout << *it << " ";
  25.                 ++it;
  26.         }
  27.         cout << endl;
  28.         // rbegin()在最后一个数据的位置(实际底层实现会有差别,在反向迭代器的底层实现中会讲解)
  29.         // 迭代器都是左闭右开的,所以rend()在第一个数据的前一个位置
  30.         string::reverse_iterator rit = s1.rbegin();
  31.         while (rit != s1.rend())
  32.         {
  33.                 (*rit)--;
  34.                 cout << *rit << " ";
  35.                 ++rit;
  36.         }
  37.         cout << endl;
  38.        
  39.         //像string类中,普通、const的成员函数都存在,就会调用更匹配的
  40.         func(s1);
  41. }
复制代码

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()数据不缩容。
  1. void test_string2()
  2. {
  3.         // size()、length()都表示返回的字符个数,不包含标识字符'\0'
  4.         // 推荐用size()
  5.         string s1("hello world");
  6.         cout << s1.size() << endl;
  7.         cout << s1.length() << endl;
  8.         // max_size能给到的最大长度,不管字符串是长是短,给到的最大长度都是一样的
  9.         cout << s1.max_size() << endl;
  10.         string s2;
  11.         cout << s2.max_size() << endl;
  12.         // capacity返回容量的大小,表示能存储的有效字符个数。例如"hello world",capacity是15,表明能存储的有效字符个数是15,但是实际空间大小是16,会包含标识字符'\0'
  13.         cout << s1.capacity() << endl;
  14.         cout << s2.capacity() << endl;
  15.         // empty()判空
  16.         if (!s1.empty())
  17.         {
  18.                 cout << s1 << endl;
  19.         }
  20.         // clear()清理数据,相当于把size()清理成0,但是不会清理空间capacity()
  21.         s1.clear();
  22.         cout << s1.size() << endl;
  23.         cout << s1.capacity() << endl;
  24. }
复制代码

在了解resize、reserve之前,先了解一下push_back接口(尾插一个字符),观察string是怎样扩容的?
  1. void test_string3()
  2. {
  3.         string s;
  4.         size_t old = s.capacity();
  5.         cout << old << endl;
  6.         for (size_t i = 0; i < 200; ++i)
  7.         {
  8.                 s.push_back('x');
  9.                 if (old != s.capacity())//old == s.capacity()没扩容,!=就是扩容了
  10.                 {
  11.                         //扩容
  12.                         cout << s.capacity() << endl;
  13.                         old = s.capacity();
  14.                 }
  15.         }
  16. }
复制代码

观察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直接开好空间,这样可以镌汰异地扩容的次数,提高效率。
  1. void test_string3()
  2. {
  3.         string s;
  4.         s.reserve(200);//知道需要的空间就提前开好
  5.         size_t old = s.capacity();
  6.         cout << old << endl;
  7.         for (size_t i = 0; i < 200; ++i)
  8.         {
  9.                 s.push_back('x');
  10.                 if (old != s.capacity())//old == s.capacity()没扩容,!=就是扩容了
  11.                 {
  12.                         //扩容
  13.                         cout << s.capacity() << endl;
  14.                         old = s.capacity();
  15.                 }
  16.         }
  17. }
复制代码
有了reserve就不存在多次异地扩容了。扩容结果,取决于平台。vs下capacity结果可能会比200大,Linux的g++下capacity是200。


  1. void test_string3()
  2. {
  3.         string s1("xxxxxxxxxxxxxxxxxxx");
  4.         s1.reserve(100);
  5.         cout << s1.size() << endl;
  6.         cout << s1.capacity() << endl;
  7.         //缩容
  8.         s1.reserve(10);
  9.         cout << s1.size() << endl;
  10.         cout << s1.capacity() << endl;
  11. }
复制代码
对于reserve是否缩容是取决于平台的。vs下是绝对不缩容的。Linux的g++下缩容了。所以照旧不要用reserve缩容了。


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有两个版本的接口:



  1. void test_string4()
  2. {
  3.         string s("xxxxxxxxxxxx");
  4.         cout << s.size() << endl;
  5.         cout << s.capacity() << endl;
  6.         s.resize(15);
  7.         cout << s.size() << endl;
  8.         cout << s.capacity() << endl;
  9.         s.resize(20, 'y');
  10.         cout << s << endl;
  11.         cout << s.size() << endl;
  12.         cout << s.capacity() << endl;
  13.         s.resize(5);
  14.         cout << s << endl;
  15.         cout << s.size() << endl;
  16.         cout << s.capacity() << endl;
  17. }
复制代码

Element access——元素存取

operator[] 接口是获取pos位置的字符。operator[]接口有两个版本。普通对象调用普通版本可以对pos位置的字符进行修改、读;const对象调用const版本只能对pos位置的字符进行读。
at接口也有两个版本,at和operator[]功能一样,只不过operator[]是以运算符重载的形式呈现,at是以函数的形式。
operator[]和at区别:对于越界的检查方式不一样,operator[]底层有断言,断言检查越界(断言会直接中断步伐,是一种更激进的检查方式,断言不能捕获,但是断言在release下会被忽略,只能在debug下有作用);at会抛异常,需要本身捕获,捕获后步伐还能运行。
  1. void test_string4()
  2. {
  3.         s[0] = 'm';
  4.         cout << s << endl;
  5.         //s[10];//断言检查越界
  6.        
  7.         s.at(1) = 'b';
  8.         cout << s << endl;
  9.         //抛异常检查越界
  10.         s.at(10);
  11. }
  12. int main()
  13. {
  14.         try {
  15.                 test_string4();
  16.         }
  17.         // 捕获at抛出的异常
  18.         catch (const exception& e)
  19.         {
  20.                 cout << e.what() << endl;
  21.         }
  22.         return 0;
  23. }
复制代码

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类对象/一个字符。
  1. void test_string5()
  2. {
  3.         /*尾部追加数据*/
  4.         /*operator+=*/
  5.         string s1("xxxxxx");
  6.         cout << (s1 += "mmmm") << endl;
  7.         string s2("hello world");
  8.         s1 += s2;
  9.         cout << s1 << endl;
  10.         cout << (s1 += 'k') << endl;
  11.         //append尾部插入多个字符,里面有一组函数,但是设计的有点冗余,最常用的就是C++98中的第三个string& append(const char* s);再加第一个
  12.         //即可以追加常量字符串或string类对象
  13.         s1.append("opq");
  14.         cout << s1 << endl;
  15.         s1.append(s2);
  16.         cout << s1 << endl;
  17.         s1.append(s2, 0, 3);
  18.         cout << s1 << endl;
  19.         s1.append("hello", 2);
  20.         cout << s1 << endl;
  21.         s1.append(3, 'v');
  22.         cout << s1 << endl;
  23.         //也可以追加迭代区间的值,也可以取迭代区间的一部分(迭代区间都是左闭右开)
  24.         s1.append(s2.begin(), s2.end() - 6);
  25.         cout << s1 << endl;
  26.         s1.append(++s1.begin(), --s1.end());
  27.         cout << s1 << endl;
  28.         /*尾删*/
  29.         //pop_back()
  30.         s1.pop_back();
  31.         cout << s1 << endl;
  32. }
复制代码

不支持提供头插和头删的接口,由于像string、vector这种次序数组结构,头插和头删的效率太低了。
不过可以通过insert、erase间接实现头插和头删。
由于string出现的比较早,没有参考的地方,接口设计的函数就会冗余
insert最常用的就是在pos位置的字符之前插入一个字符、一个字符串、一个string类对象,insert要谨慎利用,是一个时间复杂度为O(n)的接口。谨慎利用insert,由于底层要挪动数据,再插入,效率不高。
erase删除某个迭代器位置、从pos位置删除len个字符(若字符串太短,有多少删除多少;若给的是缺省值(npos是42亿9000万)删除到结尾)、删除迭代器区间。谨慎利用erase,由于底层要挪动数据覆盖删除。
assign赋值,很少用
  1. void test_string6()
  2. {
  3.         //不过可以通过insert、erase间接实现头插和头删
  4.         string s1("hello world");
  5.         string s2 = "stuvwxyz";
  6.         s1.insert(5, s2);
  7.         cout << s1 << endl;
  8.         s1.insert(3, s2, 0, 3);
  9.         cout << s1 << endl;
  10.         s2.insert(0, "opqr");
  11.         cout << s2 << endl;
  12.         s2.insert(0, "lmnxxx", 3);
  13.         cout << s2 << endl;
  14.         s2.insert(2, 3, 'x');
  15.         cout << s2 << endl;
  16.         string s3("abcdefg");
  17.         s3.insert(++s3.begin(), 3, 'y');
  18.         cout << s3 << endl;
  19.         s3.insert(s3.end(), 'h');
  20.         cout << s3 << endl;
  21.         string s4("I love C++");
  22.         s3.insert(s3.begin(), s4.begin(), s4.end());
  23.         cout << s3 << endl;
  24.         //erase删除某个迭代器位置、从pos位置删除len个字符(若字符串太短,有多少删除多少;若给的是缺省值(npos是42亿9000万)删除到结尾)、删除迭代器区间
  25.         string s5("hello world");
  26.         s5.erase(2, 5);
  27.         cout << s5 << endl;
  28.         s5.erase(5);
  29.         cout << s5 << endl;
  30.         s5.erase(s5.begin());
  31.         cout << s5 << endl;
  32.         s5.erase(++s5.begin(), --s5.end());//左闭右开,删除字符的个数 == 区间相减
  33.         cout << s5 << endl;
  34.         //assign赋值,很少用
  35. }
复制代码

谨慎利用replace,由于底层要挪动数据。replace更换,实践当中用处不大。靠前的少数字符更换成多字符后会往后挪动数据,效率不高。同理,多更换成少的也效率不高,数据会往前挪动。只有同比例更换效率才高。
  1. void test_string7()
  2. {
  3.         //谨慎使用replace,因为底层要挪动数据
  4.         string s1("hello world");
  5.         cout << s1 << endl;
  6.         s1.replace(5, 1, "%%%%%");
  7.         cout << s1 << endl;
  8.         //一个字符串中的空格替换成两个%%,下面写法效率极低
  9.         string s2("hello world hello bit");
  10.         for (size_t i = 0; i < s2.size(); ++i)
  11.         {
  12.                 if (s2[i] == ' ')
  13.                 {
  14.                         s2.replace(i, 1, "%%");
  15.                 }
  16.         }
  17.         cout << s2 << endl;
  18.         //新思路
  19.         //用reverse减少扩容
  20.         string s3("hello world hello bit");
  21.         string s4;
  22.         s4.reserve(s3.size());
  23.         for (auto ch : s3)
  24.         {
  25.                 if (ch != ' ')
  26.                 {
  27.                         s4 += ch;
  28.                 }
  29.                 else {
  30.                         s4 += "%%";
  31.                 }
  32.         }
  33.         s3 = s4;
  34.         cout << s3 << endl;
  35. }
复制代码

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很少用。
substrcopy好用,substr取pos位置的len个字符去构建一个string对象返回。
find相干接口:
find默认从0(缺省值)位置依次往后去查找一个字符/一个字符串/一个string类对象,当然也可以本身给一个值,从该位置查找。
npos:整型的最大值,42亿9000万。
find()+substr取子串的共同.后缀比较多的,找真正的后缀就倒着找rfind()
url网址,任何一个网站都有网址,网址是一个字符串,剖析出网址的三个部分协议https、域名legacy…com、资源reference…
左闭右开(右在最后一个数据的下一个位置),区间一减就是中间字符个数。左闭右闭,例如[0, 9]区间一减,少一个数
find_first_of 从前往后找给的字符串匹配参数里的字符串中的任意一个字符,找到就返回,匹配的就屏蔽为’#’
find_last_of 从后往前找…
find_frist_not_of 从前往后找给的字符串不匹配参数里的字符串中的任意一个字符,找到就返回。相当于在给的字符串中只保参数中的字符
  1. void test_string8()
  2. {
  3.         string s1("Test.cpp");
  4.         //FILE* fout = fopen(s1.c_str(), "r");
  5.         //char ch = fgetc(fout);
  6.         //while (ch != EOF)
  7.         //{
  8.         //        cout << ch;
  9.         //        ch = fgetc(fout);
  10.         //}
  11.         // find()+substr取子串的配合。后缀比较多的,找真正的后缀就倒着找rfind()
  12.         string ret;
  13.         string s2("Test.cpp.tar.zip");
  14.         size_t pos = s2.rfind('.');
  15.         if (pos != string::npos)
  16.         {
  17.                 ret = s2.substr(pos);
  18.         }
  19.         cout << ret << endl;
  20.         //url网址,任何一个网站都有网址,网址是一个字符串,解析出网址的三个部分协议https、域名legacy....com、资源reference....
  21.         string url("https://legacy.cplusplus.com/reference/string/string/find/");
  22.         size_t pos1 = url.find(':');
  23.         if (pos1 != string::npos)
  24.         {
  25.                 string sub1 = url.substr(0, pos1);
  26.                 cout << sub1 << endl;
  27.         }
  28.         size_t pos2 = url.find('/', pos1 + 3);
  29.         if (pos2 != string::npos)
  30.         {
  31.                 string sub2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));
  32.                 cout << sub2 << endl;
  33.                 string sub3 = url.substr(pos2 + 1);
  34.                 cout << sub3 << endl;
  35.         }
  36.         //find_first_of 从前往后找给的字符串匹配参数里的字符串中的任意一个字符,找到就返回,匹配的就屏蔽为'#'
  37.         //find_last_of  从后往前找...
  38.         //find_frist_not_of 从前往后找给的字符串不匹配参数里的字符串中的任意一个字符,找到就返回。相当于在给的字符串中只保参数中的字符
  39.         std::string str("Please, replace the vowels in this sentence by asterisks.");
  40.         std::size_t found = str.find_first_not_of("abcd");
  41.         while (found != std::string::npos)
  42.         {
  43.                 str[found] = '#';
  44.                 found = str.find_first_not_of("abcd", found + 1);
  45.         }
  46.         std::cout << str << '\n';
  47. }
复制代码

与string类相干的函数重载玉成局的

relational operators 中的每个运算符重载的后两个版本设计得有点冗余,由于支持单参数隐式类型转换,所以只用第一个版本也是可以的。为什么会重载玉成局的呢?左操作数可能是非当前重载类的类型。

operator+

流提取>>与scanf:流提取不会有越界的题目,底层会动态扩容。C中scanf可以输入字符串到字符数组里,但是要提前盘算好空间,malloc()出来的也要提前盘算好,C++就相对来说更机动。
但是流提取有缺陷,例如下面的例子,输入hello world时,只会输出hello。scanf和cin都有一个规定,空格或换行时会做多项之间的分割。

也就是说若这个字符串中就包含空格,那就不能用流提取了,怎么办理这种题目呢?——getline,获取一行

第二个版本默认是以换行作为结束符。第一个版本可以自定义输入结束符,即可以换行继续输入,调试可以看到换行字符。


四、字符串与整型/浮点数的互相转换


stoi常用

浮点数跟转换后的字符串可能会不一样,浮点数不能精确存储。(C语言:数据在内存中的存储)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

络腮胡菲菲

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表