《一篇拿下C++:string类(详解版)》:教你如何从入门到避坑再到玩转字符串问题

《一篇拿下C++:string类(详解版)》:教你如何从入门到避坑再到玩转字符串问题

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》《优选算法指南-必刷经典100题》

🌟心向往之行必能至


🎬博主简介:

目录

前言

一、别再给char*施压,string类带你轻松处理字符串

1.1、C语言字符串的三大痛处

1.2、string类的优点

二、0基础快速上手:string的核心接口使用

2.1 字符串创建:包含4 种常用构造形式

2.2 字符串遍历:3种便捷方式

2.2.1 operator [] 下标访问

2.2.2 范围for遍历(C++11支持,浅显易懂)

2.2.3 迭代器遍历(适配所有容器)

2.3 字符串修改(高频操作):拼接/插入/删除/清空

2.3.1 尾部追加:3种方式对比(operator+=/push_back/append)

2.3.2 插入和删除操作:insert和erase 在指定位置插入和删除

2.3.3 内容清空:clear(只清有效字符)

2.4 字符串替换:replace() 修改指定位置内容

三. 卓有成效:string容量管理避坑攻略

3.1 先理清:size ()、length ()、capacity () 的区别

3.3 调整有效字符:resize()的2种使用情况

3.4 高频踩坑点:shrink_to_fit () 按需缩容

四.环境差异:VS 和 g++ 下 string 存储差异

4.1 VS环境:28字节结构

4.2 g++环境:4字节指针,指向堆空间

五.加餐补充:string常用场景的一些实用接口和技巧

5.1 字符串查找:find() 找字符/子串

5.2 整行输入:getline()读取带空格的字符串

5.3 子串截取:substr()从指定位置取指定长度

5.4 C字符转换:c_str () 适配C语言库函数

结尾


前言

你还在为字符串内存管理、越界等情况烦恼嘛?大家不用急,学会string类的接口,让你从创建、遍历、修改,到容量优化、跨平台避坑到快速上手解决需求

一、别再给char*施压,string类带你轻松处理字符串

1.1、C语言字符串的三大痛处

--我们在之前C语言字符串的学习中,肯定体会到以下痛苦

  • 容易越界访问:'\0'标识符来识别字符串的结束位置,一旦忘加'\0'就可能会出现问题
  • 内存需要手动管理:malloc开辟空间,free释放空间,往往会出现多开辟和二次释放
  • 接口散、不灵活:拼接用strcat,计算字符串长度用strlen,拷贝用strcpy,而且不灵活,需要自己提前处理

1.2、string类的优点

--string类可以很好的解决掉以上痛处

  • 接口丰富,使用灵活:从构造到修改,查找这些常用操作全部都实现成了接口,不用自己实现,直接使用即可
  • 自动内存管理:不用手动new/delete,底层会按需进行扩容,释放,有效避免了内存的泄漏
  • OOP设计:把字符串和操作(如拼接,查找等)封装在一起,比strcat灵活
  • 使用场景广泛:不管是日常开发还是OJ刷题,string类的使用都很频繁,并且天然兼容中文,英文,Unicode字符

二、0基础快速上手:string的核心接口使用

--在使用之前,给大家cplusplus文档,以后大家要学会看文档,看文档学习可以提升你对这些接口的熟悉程度

cplusplus官方文档:

string - C++ Refrence

不要忘记带上头文件以及using namespace std;后续的代码演示中就不一个个带这些了

#include<iostream> #include<string> #include<algorithm> using namespace std;

2.1 字符串创建:包含4 种常用构造形式

--string构造方式有很多种,这里我们就学习最常见的就可以了,当然这里string还重载了 = 也可以用

string::string - C++ Reference

最常用的4种:

// 1. 空字符串构造(默认构造) string s1; // s1是空字符串,底层已初始化,不用手动加'\0' // 2. C字符串构造(最常用,把char*转成string) string s2("hello C++"); // s2 = "hello C++" // 3. 重复字符构造(创建n个相同字符的字符串) string s3(5, 'a'); // s3 = "aaaaa"(5个'a') // 4. 拷贝构造(用已有的string创建新对象) string s4(s2); // s4 = "hello C++"(和s2内容一样)

代码演示:(注意看注释)

void test_string1() { string s1; string s2("hello world"); string s3(s2); cout << s1 << endl; cout << s2 << endl; cout << s3 << endl; string s4(s2, 0, 5);//从s2下标为0的位置拷贝5个过去构造s4; cout << s4 << endl; //pos位置一直拷贝到结尾 //1.写一个超过s2长度的 string s5(s2, 6, 15); cout << s5 << endl; //2.直接不写,默认使用缺省值npos string s6(s2, 6); cout << s6 << endl; string s7("hello world", 6);//取前6个 cout << s7 << endl; string s8(10, 'x');//用10个x cout << s8 << endl; s7 = "xxxxxx";//这样也可以 cout << s7 << endl; } int main() { test_string1(); }

--析构这里只做了解,不需要我们实现

2.2 字符串遍历:3种便捷方式

--在日常使用中,遍历是高频的操作,这里博主比较推荐大家使用前两种,更加简单直观

2.2.1 operator [] 下标访问

--这个逻辑很简单易懂,和数组下标访问逻辑类似,支持读和写,注意下标从0开始: 

string::operator[] - C++ Reference

代码演示:(注意看注释)

//operate[] void test_string2() { string s1("hello world"); cout << s1 << endl; s1[0]='x';//可以直接使用下标来访问修改,类似于上面那样 cout << s1 << endl; cout << s1[0] << endl; //相比于数组这个越界有严格的检查 //s1[12];//断言 //s1.at(12);//抛异常,这里at的使用简单看看就行 //size和length一样,但更推荐size cout << s1.size() << endl; cout << s1.length() << endl; } int main() { try { test_string2(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
2.2.2 范围for遍历(C++11支持,浅显易懂)

不用管下标和长度,直接遍历每个字符

void test_string5() { string s1("hello world"); cout << s1 << endl; //C++11 //范围for,自动取容器数据赋值,自动迭代++,自动判断结束 //其实底层还是迭代器,这个看反汇编可以发现 //for (auto ch : s1)//其实可以直接使用&,可以修改 for(auto& ch:s1) { ch -= 1; } for (const auto& ch : s1) { cout << ch << ' ';//只能读不能改 } cout << endl; //支持迭代器的容器,都可以使用范围for //数组也支持,这里先使用一点C风格 int a[10] = { 1,2,3 }; for (auto e : a) { cout << e << ' '; } cout << endl; } int main() { test_string5(); }
2.2.3 迭代器遍历(适配所有容器)

迭代器是STL容器的一个通用遍历方式,begin() 指向第一个字符,end() 指向最后一个字符的下一位

代码演示:(注意看注释)

void Print(const string& s) { //2.const版本 //const string::iterator it1=s.cbegin(); //上面这样使用是不对的,const不应该用来修饰整个迭代器,这样都遍历不了了,而是修饰指向的对象 string::const_iterator it1 = s.cbegin();//这里使用cbgin和普通的都可以 while (it1 != s.cend()) { //*it1 = 'x';不能修改 cout << *it1 << " "; ++it1; } cout << endl; //3.reverse版本,加上const一起演示,逆序输出 string::const_reverse_iterator it2 = s.rbegin();//这里使用rbegin while (it2 != s.rend()) { //*it2 = 'x';//不能修改 cout << *it2 << " "; ++it2; } cout << endl; } //下标遍历,迭代器 void test_string3() { string s1("hello world"); cout << s1 << endl; //下标+【】 //遍历or修改 for (size_t i = 0; i < s1.size(); i++) { s1[i]++; } cout << s1 << endl; //迭代器 //行为像指针一样的东西 //1.常规使用 string::iterator it1 = s1.begin(); while (it1 != s1.end()) { //(*it1)-;//修改 cout << *it1 << " "; ++it1; } //相对于下标+[]来说,迭代器更加通用,我们这里再来看看在链表中的使用 list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); list<int>::iterator lit = lt.begin(); while (lit != lt.end()) { cout << *lit << " "; ++lit; } cout << endl; //迭代器的其它使用形式 Print(s1); } int main() { test_string3(); }

2.3 字符串修改(高频操作):拼接/插入/删除/清空

2.3.1 尾部追加:3种方式对比(operator+=/push_back/append)

--就相当于链表里面的尾插操作,大家赶紧哪个用着方便用哪个

  • operator+=:支持追加单个字符和字符串,比 push_back 和 append 更加灵活
  • push_back:仅支持单个字符,功能单一,适合明确追加单个字符的场景
  • append:支持字符串、子串,适合需要追加部分内容的场景

代码演示:(注意看注释)

//push_back,append,+=,+; //appear,= void test_string7() { string s1("hello world"); s1.push_back('&');//尾插一个字符 s1.append("hello bit");//尾插一个字符串 cout << s1 << endl; s1.append(10, 'x');//尾插10个x cout << s1 << endl; //还可以配着迭代器使用 string s3; string s2(" apple hello!"); //我不想要空格和! s3.append(++s2.begin(), --s2.end()); cout << s3 << endl; //其实我们直接使用+=更加方便 string s4("hello world"); s4 += ' '; s4 += "hello bit"; cout << s4 << endl; //为什么不把 + 重载为成员的而是全局,因为这样可以不用一定把成员变量写在左边 cout << s4 + "xxxx" << endl; cout << "xxxx" + s4 << endl; //assign,没有直接赋值好用 s4 = "xxx"; cout << s4 << endl; s4.assign("yyy"); cout << s4 << endl; } int main() { test_string7(); }
2.3.2 插入和删除操作:insert和erase 在指定位置插入和删除

--insert和erase可以在任意位置插入和删除字符,字符串,很灵活。但是频繁使用会导致元素的频繁移动,升高了时间复杂度,降低了效率

代码演示:(注意看注释)

void test_string8() { string s1("hello world"); //上面都是尾插,这里实现一个头插 s1.insert(0, "xxxxx"); cout << s1 << endl; //但是头插一个必须这样写 s1.insert(0, 1, '*'); cout << s1 << endl; //第5个位置插入一个* s1.insert(5, 1, '*'); cout << s1 << endl; //迭代器 s1.insert(s1.begin(), '&'); cout << s1 << endl<<endl; string s2("hello world"); s2.erase(0, 1);//头删 cout << s2 << endl; s2.erase(s2.begin());//头删 cout << s2 << endl; s2.erase(5, 2);//指定位置开始删除2个 cout << s2 << endl<<endl; //没给的话就全删掉 s2.erase(5);//这里应该也是默认npos cout << s2 << endl; } int main() { test_string8(); }
2.3.3 内容清空:clear(只清有效字符)

--clear() 会把字符串的有效字符全部清空,但是不会释放空间,如有需要,需要自己手动释放

代码演示:(注意看注释)

string s = "hello world"; s.clear(); // s变成空串,但底层容量不变 cout << s.size(); // 输出0(有效字符数为0)

2.4 字符串替换:replace() 修改指定位置内容

--replace() 能直接替换字符串中指定位置,指定长度的内容。可以替换为字符,字符串或者子串,使用起来很灵活

代码演示:(注意看注释)

void test_string8() { string s3("hello world"); s3.replace(5, 1, "&&&");//把5这个位置的1个替换成&&& cout << s3 << endl; s3.replace(5, 3, "*");//从5开始的三个替换成* cout << s3 << endl; //我们再来看看怎么把所有空格都替换成%% string s4("hello world hello wugongda"); cout << s4 << endl; size_t pos = s4.find(' '); while (pos != string::npos) { s4.replace(pos, 1, "%%"); //找到下一个空格 pos = s4.find(' ', pos + 2); } cout << s4 << endl; //这样的话效率不是很高,我们换个思路优化一下 string s5("hello world hello wugongda"); cout << s5 << endl; string s6; s6.reserve(s5.size()); for (auto ch : s5) { if (ch != ' ') { s6 += ch; } else { s6 += "%%"; } } cout << s6 << endl; //s5 = s6; } int main() { test_string8(); }

三. 卓有成效:string容量管理避坑攻略

我们在使用string时,如果不注意容量,可能会导致频繁扩容,拖慢程序的效率。我们先了解一下下面会涉及的三个接口,再来学学优化技巧

3.1 先理清:size ()、length ()、capacity () 的区别

很多人会混淆这三个接口的作用,我们先通过表格对比看看吧

接口功能使用情况
string::size - C++ Reference返回有效字符个数(比如 "hello" 返回 5)优先用,和其他 STL 容器接口一致
string::length - C++ Reference和size()功能完全一样(历史遗留接口)少用,不如size()通用
string::capacity - C++ Reference返回底层已分配的空间大小(能存多少字符,不含 '\0')优化时使用

我们先来看看一个整体的代码,里面还涉及到了resize,shrink_to_fit等接口的使用

代码演示:(注意看注释)

void TestCapacity() { string s1; //s1.reserve(200);//确定要插入多少时,可以提前扩容 size_t old = s1.capacity(); cout << s1.capacity() << endl; for (size_t i = 0; i < 200; i++) { s1.push_back('x');//尾插 if (s1.capacity() != old) { cout << s1.capacity() << endl; old = s1.capacity(); } } cout << endl << endl; } void test_string6() { string s1("hello world"); cout << s1.max_size() << endl;//了解下即可 cout << s1.size() << endl;//不包含结尾的\0 cout << s1.capacity() << endl;//存储实际有效字符的个数,不包含结尾的\0 s1.clear();//空间不会清理 cout << s1.size() << endl;//不包含结尾的\0 cout << s1.capacity() << endl<<endl;//存储实际有效字符的个数,不包含结尾的\0 //测试空间增容 TestCapacity(); //reserve最好只用来增容 string s2("hello world"); cout << s2.size() << endl; cout << s2.capacity() << endl; s2.reserve(20);//会开的比20大 cout << s2.size() << endl; cout << s2.capacity() << endl; //s2.reserve(5);//vs上不会缩容 s2.shrink_to_fit();//这个可以实现缩容,但是一般不会用,代价比较大 cout << s2.size() << endl; cout << s2.capacity() << endl; string s3(s2); cout << s3 << endl; // < 当前对象的size时,相当于保留前n个,删除后面的数据 s3.resize(5); cout << s3 << endl; // > 当前对象的size时,插入数据 s3.resize(10, 'x'); cout << s3 << endl; s3.resize(30, 'y'); cout << s3 << endl; } int main() { test_string6(); }
注意:如果 reserve(n) 的 n 比当前 capacity() 小,vs上不会缩小空间

3.3 调整有效字符:resize()的2种使用情况

resize(n) 用来修改有效字符的个数,分以下两种情况:

  1. 若n比当前size()大:补字符(默认会补‘\0’,'也可以自己指定);
  2. 若n比当前size()小:保留前n个,删除后面的,但底层空间是不变的

代码演示:(注意看注释)

string s = "hello"; // 1.n比size()大:补'x'到10个字符 s.resize(10, 'x'); // 2.n比size()小:截断到3个字符 s.resize(3); 

3.4 高频踩坑点:shrink_to_fit () 按需缩容

前面我们讲过 clear() 只会清理有效字符,但是底层空间不变。那我们如果想要进行缩容,就可以使用 shrink_to_fit() (但是这个只是一个建议,而且我们使用的也少,代价太大了)


四.环境差异:VS 和 g++ 下 string 存储差异

不同编译器的string底层实现不一样,大家可以一起来看看差异。下述结构都是在32位平台下进行验证的,32位平台指针占4个字节

4.1 VS环境:28字节结构

VS下string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间

  • 当字符串长度 < 16 时:直接存在数组里(栈空间),不用开堆内存,效率高;
  • 当字符串长度≥16 时:从堆上分配空间,数组存堆指针。
union _Bxty { // storage for small buffer or pointer to larger one value_type _Buf[_BUF_SIZE]; pointer _Ptr; char _Alias[_BUF_SIZE]; // to permit aliasing } _Bx;

这种设计也是具有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量

最后:还有一个指针做一些其它事情。

所以总共占28个字节

4.2 g++环境:4字节指针,指向堆空间

g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符有效长度
  • 引用计数
struct _Rep_base { size_type _M_length; size_type _M_capacity; _Atomic_word _M_refcount; }
  • 指向堆空间的指针,用来存储字符串

五.加餐补充:string常用场景的一些实用接口和技巧

--除了前面讲的一些以外,其实我们的string还有很多比较实用的接口,这里就再给大家分享一部分,如果有没分享到但是大家需要使用的话可以自己查阅参考文档去了解一下用法

5.1 字符串查找:find() 找字符/子串

find() 从左往右找字符或者子串,返回第一次出现的下标;没有找到就返回 string :: npos(整形的最大数)

代码演示:(注意看注释)

nt main() { string s = "hello world"; // 1. 找字符'w' size_t pos1 = s.find('w'); if (pos1 != string::npos) { cout << "'w'在位置:" << pos1 << endl;// 输出6 } // 2. 找子串"world" size_t pos2 = s.find("world"); if (pos2 != string::npos) { cout << "world在位置:" << pos2 << endl;// 输出6 } // 3. 从下标3开始找字符'l' size_t pos3 = s.find('l', 3); // 从第3位(0开始)往后找 cout << "'l'在位置:" << pos3 << endl;// 输出3(s[3]是'l') }

5.2 整行输入:getline()读取带空格的字符串

在平常的使用中,如果使用 cin>>string 读取字符串时,遇到空格就会停止。而 getline() 能读取一整行的内容,包括空格。默认回车结束,也可以自己指定

比如我们下面这个题就必须使用getline,直接用cin是不行的

题目链接:字符串最后一个单词的长度_牛客题霸_牛客网

代码演示:(注意看注释)

#include <iostream> #include <string> using namespace std; int main() { string str; // cin >> str;//这个不行 getline(cin, str); //getline(cin, str, '#');//指定碰到#结束 size_t pos = str.rfind(' '); if (pos != str.size()) { cout << str.size() - (pos + 1) << endl; } else { cout << str.size() << endl; } }

5.3 子串截取:substr()从指定位置取指定长度

--substr(pos,len):从pos位置开始,取len个字符,如果len不写,就取到字符结尾

代码演示:(注意看注释)

int main() { string s = "hello world"; // 1. 从位置6开始,取5个字符 string sub1 = s.substr(6, 5); // sub1 = "world" // 2. 从位置0开始,取5个字符 string sub2 = s.substr(0, 5); // sub2 = "hello" // 3. 从位置6开始,取到末尾 string sub3 = s.substr(6); // sub3 = "world" }

5.4 C字符转换:c_str () 适配C语言库函数

我们在一些特殊的场景下需要使用C语言的char*(比如printf输出),用c_str ()把string转换成const char*

代码演示:(注意看注释)

#include <cstring> int main() { string s = "hello"; // 1. printf输出(printf不直接支持string) printf("s = %s\n", s.c_str()); // 输出:s = hello // 2. 调用C库函数strlen(需要包含<cstring>) size_t len = strlen(s.c_str()); // len = 5 }

补充示例:(涉及到几个接口的使用,大家可以自己试试)

void SplitFilename(const std::string& str) { std::cout << "Splitting: " << str << '\n'; std::size_t found = str.find_last_of("/\\"); std::cout << " path: " << str.substr(0, found) << '\n'; std::cout << " file: " << str.substr(found + 1) << '\n'; } void test_string7() { string s("test.cpp.zip"); size_t pos = s.rfind('.'); string suffix = s.substr(pos); cout << suffix << endl; std::string str("Please, replace the vowels in this sentence by asterisks."); std::cout << str << '\n'; std::size_t found = str.find_first_not_of("abcdef"); while (found != std::string::npos) { str[found] = '*'; found = str.find_first_not_of("abcdef", found + 1); } std::cout << str << '\n'; std::string str1("/usr/bin/man"); std::string str2("D:\\1-草莓熊Lotso\\1-课件\\4.C++课件\\C++进阶课件"); SplitFilename(str1); SplitFilename(str2); string url2("https://legacy.cplusplus.com/reference/string/string/substr/"); string url1("http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=65081411_1_oem_dg&wd=%E5%90%8E%E7%BC%80%20%E8%8B%B1%E6%96%87&fenlei=256&rsv_pq=0xc17a6c03003ede72&rsv_t=7f6eqaxivkivsW9Zwc41K2mIRleeNXjmiMjOgoAC0UgwLzPyVm%2FtSOeppDv%2F&rqlang=en&rsv_dl=ib&rsv_enter=1&rsv_sug3=4&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=1588&rsv_sug4=6786"); string protocol, domain, uri; size_t i1 = url1.find(':'); if (i1 != string::npos) { protocol = url1.substr(0, i1 - 0); cout << protocol << endl; } // strchar size_t i2 = url1.find('/', i1+3); if (i2 != string::npos) { domain = url1.substr(i1+3, i2-(i1+3)); cout << domain << endl; uri = url1.substr(i2 + 1); cout << uri << endl; } } void test_string4() { string s("test.cpp.zip"); size_t pos = s.find('.'); string suffix = s.substr(pos); cout << suffix.c_str() << endl; string copy(s); cout << copy.c_str() << endl; s = suffix; cout << suffix.c_str() << endl; cout << s.c_str() << endl; s = s; cout << s.c_str() << endl; } void test_string5() { string s1("hello world"); string s2("hello world"); cout << (s1 < s2) << endl; cout << (s1 == s2) << endl; cout << ("hello world" < s2) << endl; cout << (s1 == "hello world") << endl; //cout << ("hello world" == "hello world") << endl; cout << s1 << s2 << endl; string s0; cin >> s0; cout << s0 << endl; } void test_string6() { string s1("hello world"); string s2 = s1; cout << s1 << endl; cout << s2 << endl; string s3("xxxxxxxxxxxxxx"); s1 = s3; cout << s1 << endl; cout << s3 << endl; }

结尾

往期回顾:

《一篇拿下!C++内存管理》:new和delete的崛起

《一篇拿下!C++:模板(初阶)》:模板的精妙绝伦!

《C++:STL》详细深入解析string类(一):

总结:熟练掌握string类,可以帮你轻松解决80%的字符串难题,而另外的20%需要自己去挖掘发现

Could not load content