【C++】string类
C++ string 类全面解析
1. 为什么学习 string 类?
1.1 C语言中的字符串局限性
在C语言中,字符串是以\0结尾的字符数组,这种表示方式存在几个明显的缺陷:
C语言字符串的主要问题:
- 安全性问题:容易发生缓冲区溢出,导致程序崩溃或安全漏洞
- 内存管理复杂:需要手动管理内存分配和释放,容易造成内存泄漏
- 功能有限:标准库函数功能相对基础,复杂的字符串操作需要自行实现
- 不符合面向对象思想:数据与操作分离,不符合现代编程范式
// C语言字符串操作的典型问题char str[10];strcpy(str,"这个字符串太长了会导致溢出");// 潜在的安全风险1.2 实际应用需求
在现代编程中,字符串处理占据了极大的比重。无论是Web开发、数据处理还是系统编程,都离不开高效的字符串操作。string类的出现正是为了解决C语言字符串的种种痛点。
面试题示例(后续详解):
- 字符串转整型数字
- 大数相加(字符串形式)
实践建议:在OJ题目和实际开发中,string类已成为字符串处理的首选工具,相比C字符串库函数更加安全高效。
2. 标准库中的string类
2.1 string类基础
string类是C++标准库中用于表示和操作字符串的类,封装了字符串的存储和常见操作。
基本用法:
#include<string>#include<iostream>usingnamespace std;intmain(){ string s1;// 空字符串 string s2 ="Hello";// 直接初始化 string s3("World");// 构造函数初始化return0;}2.2 C++11新特性:auto和范围for
auto关键字详解
auto是C++11引入的类型推断关键字,让编译器自动推导变量类型。
auto的使用规则:
#include<iostream>#include<map>#include<string>usingnamespace std;intmain(){// 基本类型推断auto a =10;// intauto b =3.14;// doubleauto c ='A';// char// 指针和引用int x =100;auto y =&x;// int*auto* z =&x;// int* (与上面等价)auto& ref = x;// int&// 容器迭代器简化 map<string, string> dict ={{"apple","苹果"},{"banana","香蕉"}};// 传统写法(冗长) map<string, string>::iterator it1 = dict.begin();// auto写法(简洁)auto it2 = dict.begin();// 遍历mapfor(auto it = dict.begin(); it != dict.end();++it){ cout << it->first <<": "<< it->second << endl;}return0;}auto的限制:
- 必须初始化:
auto x;// 错误 - 多变量声明必须类型一致:
auto a=1, b=2.0;// 错误 - 不能用于函数参数(但可以用于返回值)
- 不能声明数组:
auto arr[] = {1,2,3};// 错误
范围for循环(Range-based for loop)
范围for提供了更简洁的遍历语法,特别适合容器遍历。
#include<iostream>#include<vector>#include<string>usingnamespace std;intmain(){// 数组遍历int arr[]={1,2,3,4,5};// 传统遍历方式for(int i =0; i <sizeof(arr)/sizeof(arr[0]);++i){ cout << arr[i]<<" ";} cout << endl;// 范围for遍历(只读)for(auto elem : arr){ cout << elem <<" ";} cout << endl;// 范围for遍历(可修改)for(auto& elem : arr){ elem *=2;// 修改元素}// 字符串遍历 string str ="Hello";for(auto ch : str){ cout << ch <<" ";} cout << endl;return0;}范围for的底层原理:编译器会将范围for转换为基于迭代器的普通循环。
2.3 string类常用接口详解
2.3.1 构造函数
| 构造函数 | 功能说明 | 示例 |
|---|---|---|
string() | 创建空字符串 | string s1; |
string(const char* s) | 用C字符串构造 | string s2("hello"); |
string(size_t n, char c) | n个字符c组成的字符串 | string s3(5, 'A'); // "AAAAA" |
string(const string& str) | 拷贝构造 | string s4(s2); |
voidtestConstructors(){ string s1;// 空字符串 string s2("Hello World");// 从C字符串构造 string s3(s2);// 拷贝构造 string s4(10,'*');// "**********" string s5 ="直接赋值";// 赋值初始化 cout <<"s2: "<< s2 << endl; cout <<"s4: "<< s4 << endl;}2.3.2 容量操作
重要容量方法:
| 方法 | 功能 | 说明 |
|---|---|---|
size()/length() | 返回字符串长度 | 两者功能相同,推荐size() |
capacity() | 返回分配的内存大小 | 通常 ≥ size() |
empty() | 判断是否为空 | 空返回true,否则false |
clear() | 清空内容 | 不释放内存,size=0 |
reserve(size_t n) | 预留空间 | 避免频繁重新分配 |
resize(size_t n, char c) | 调整大小 | 多出部分用c填充 |
voidtestCapacity(){ string str ="Hello"; cout <<"长度: "<< str.size()<< endl;// 5 cout <<"容量: "<< str.capacity()<< endl;// 15(编译器相关) cout <<"是否为空: "<< str.empty()<< endl;// 0(false) str.resize(10,'!'); cout <<"调整后: "<< str << endl;// "Hello!!!!!" str.reserve(100); cout <<"预留后容量: "<< str.capacity()<< endl; str.clear(); cout <<"清空后长度: "<< str.size()<< endl;// 0}resize()详解:
string s ="Hello"; s.resize(3);// "Hel"(截断) s.resize(8,'!');// "Hel!!!!!"(扩展并填充) s.resize(10);// "Hel!!!!! "(扩展,默认填充空格)2.3.3 元素访问和遍历
多种遍历方式:
voidtestTraversal(){ string str ="ABCDE";// 1. 下标操作符[]for(size_t i =0; i < str.size();++i){ cout << str[i]<<" ";// A B C D E} cout << endl;// 2. 迭代器for(auto it = str.begin(); it != str.end();++it){ cout <<*it <<" ";// A B C D E} cout << endl;// 3. 反向迭代器for(auto rit = str.rbegin(); rit != str.rend();++rit){ cout <<*rit <<" ";// E D C B A} cout << endl;// 4. 范围for(推荐)for(auto ch : str){ cout << ch <<" ";// A B C D E} cout << endl;}2.3.4 修改操作
常用修改方法:
| 方法 | 功能 | 示例 |
|---|---|---|
push_back(char c) | 尾部添加字符 | str.push_back('!') |
append(const string& str) | 追加字符串 | str.append(" World") |
operator+= | 追加(最常用) | str += "!!" |
insert(size_t pos, const string& str) | 插入字符串 | str.insert(5, "插入") |
erase(size_t pos, size_t len) | 删除子串 | str.erase(5, 2) |
replace(size_t pos, size_t len, const string& str) | 替换子串 | str.replace(0, 5, "Hi") |
voidtestModification(){ string str ="Hello";// 追加操作 str.push_back('!');// "Hello!" str.append(" World");// "Hello! World" str +="!!";// "Hello! World!!"(最常用)// 插入和删除 str.insert(6,"C++ ");// "Hello! C++ World!!" str.erase(0,7);// "C++ World!!" str.replace(4,5,"String");// "C++ String!!" cout <<"最终结果: "<< str << endl;}2.3.5 字符串操作
查找和子串操作:
voidtestStringOperations(){ string str ="Hello World, Hello C++";// 查找操作 size_t pos1 = str.find("Hello");// 0 size_t pos2 = str.find("Hello",1);// 13(从位置1开始找) size_t pos3 = str.rfind("Hello");// 13(从后往前找)// 子串提取 string sub1 = str.substr(6,5);// "World" string sub2 = str.substr(6);// "World, Hello C++"// 比较操作 string s1 ="apple", s2 ="banana";int result = s1.compare(s2);// 负数(apple < banana) cout <<"find结果: "<< pos1 <<", "<< pos2 <<", "<< pos3 << endl; cout <<"子串: "<< sub1 <<", "<< sub2 << endl;}2.4 不同编译器下的string实现
VS下的string实现(小字符串优化)
Visual Studio采用小字符串优化(SSO)策略:
内存布局(32位平台):
- 16字节缓冲区:长度<16时使用栈空间
- 4字节:字符串长度
- 4字节:总容量
- 4字节:其他信息
- 总计28字节
优势:短字符串无需堆分配,提高性能。
// VS中小字符串优化的效果 string shortStr ="short";// 使用内部缓冲区(栈) string longStr ="这是一个很长的字符串...";// 使用堆分配g++下的string实现(写时拷贝)
g++采用写时拷贝(Copy-On-Write)技术:
内存布局:
- 4字节指针:指向堆上的结构体
- 结构体包含:长度、容量、引用计数、字符串数据
优势:拷贝时不立即复制数据,提高拷贝效率。
// g++中的写时拷贝 string s1 ="hello"; string s2 = s1;// 不复制数据,只增加引用计数 s2[0]='H';// 此时才真正复制数据(写时拷贝)2.5 实战练习
示例1:仅反转字母
classSolution{public:boolisLetter(char ch){return(ch >='a'&& ch <='z')||(ch >='A'&& ch <='Z');} string reverseOnlyLetters(string s){if(s.empty())return s;int left =0, right = s.size()-1;while(left < right){// 找到左边的字母while(left < right &&!isLetter(s[left]))++left;// 找到右边的字母while(left < right &&!isLetter(s[right]))--right;// 交换字母if(left < right){swap(s[left], s[right]);++left;--right;}}return s;}};示例2:字符串相加(大数加法)
classSolution{public: string addStrings(string num1, string num2){int i = num1.size()-1, j = num2.size()-1;int carry =0; string result;while(i >=0|| j >=0|| carry >0){int digit1 =(i >=0)? num1[i--]-'0':0;int digit2 =(j >=0)? num2[j--]-'0':0;int sum = digit1 + digit2 + carry; carry = sum /10; result.push_back('0'+(sum %10));}reverse(result.begin(), result.end());return result;}};示例三:字符串最后一个单词的长度
#include<iostream>usingnamespace std;intmain(){ string s;getline(cin,s);//这里不能用流提取。流提取识别不了空格 size_t pos=s.rfind(' '); cout<<s.size()-(pos+1)<<endl;}getlin默认三个参数,输入两个时,默认最后一个是空
3. string类的模拟实现
3.1 浅拷贝问题
浅拷贝的危险性:
// 有问题的String类实现classString{public:String(constchar* str =""){ _str =newchar[strlen(str)+1];strcpy(_str, str);}~String(){delete[] _str;}private:char* _str;};voidtestProblem(){ String s1("hello"); String s2(s1);// 浅拷贝:s1和s2指向同一内存// 析构时:同一内存被删除两次 → 程序崩溃}3.2 深拷贝实现
传统版String类
classString{public:// 构造函数String(constchar* str =""){if(str ==nullptr) str =""; _str =newchar[strlen(str)+1];strcpy(_str, str);}// 拷贝构造函数(深拷贝)String(const String& other){ _str =newchar[strlen(other._str)+1];strcpy(_str, other._str);}// 赋值运算符重载 String&operator=(const String& other){if(this!=&other){// 防止自赋值char* temp =newchar[strlen(other._str)+1];strcpy(temp, other._str);delete[] _str;// 释放原有资源 _str = temp;}return*this;}// 析构函数~String(){delete[] _str;}private:char* _str;};现代版String类(更优雅)
classString{public:String(constchar* str =""){if(str ==nullptr) str =""; _str =newchar[strlen(str)+1];strcpy(_str, str);}// 拷贝构造函数:利用临时对象交换String(const String& other):_str(nullptr){ String temp(other._str);// 用C字符串构造临时对象swap(_str, temp._str);// 交换资源}// 赋值运算符:参数为值传递,利用交换技术 String&operator=(String other){// 注意:参数为值传递swap(_str, other._str);// 交换资源return*this;// other析构时会释放原有资源}~String(){delete[] _str;}private:char* _str;};3.3 写时拷贝(Copy-On-Write)
写时拷贝是一种优化技术,在读取时共享数据,在修改时才进行实际拷贝。
基本原理:
- 多个对象共享同一数据
- 引用计数跟踪共享者数量
- 当有修改操作时,才进行实际拷贝
// 简化的写时拷贝实现classCowString{private:structStringData{char* data;int refCount;// 引用计数StringData(constchar* str){ data =newchar[strlen(str)+1];strcpy(data, str); refCount =1;}~StringData(){delete[] data;}}; StringData* _data;public:// 实现细节略...};4. 最佳实践和性能建议
4.1 字符串操作优化
// 不推荐的写法(性能差) string result;for(int i =0; i <1000;++i){ result +="data";// 可能多次重新分配内存}// 推荐的写法 string result; result.reserve(5000);// 预先分配足够空间for(int i =0; i <1000;++i){ result +="data";// 无重新分配}4.2 选择合适的方法
string str ="hello";// 尾部添加字符的三种方式 str.push_back('!');// 方式1 str.append(1,'!');// 方式2 str +='!';// 方式3(最常用)// 查找操作选择 size_t pos1 = str.find('e');// 查找字符 size_t pos2 = str.find("ll");// 查找子串 size_t pos3 = str.rfind('l');// 反向查找5. 总结
string类是C++中最重要的工具类之一,它:
- 解决了C字符串的安全性问题:自动内存管理,防止缓冲区溢出
- 提供了丰富的操作方法:查找、替换、分割等常用操作一应俱全
- 具有高效的实现:小字符串优化、写时拷贝等技术提升性能
推荐的写法
string result; result.reserve(5000);// 预先分配足够空间for(int i =0; i <1000;++i){ result +="data";// 无重新分配}4.2 选择合适的方法
string str ="hello";// 尾部添加字符的三种方式 str.push_back('!');// 方式1 str.append(1,'!');// 方式2 str +='!';// 方式3(最常用)// 查找操作选择 size_t pos1 = str.find('e');// 查找字符 size_t pos2 = str.find("ll");// 查找子串 size_t pos3 = str.rfind('l');// 反向查找5. 总结
string类是C++中最重要的工具类之一,它:
- 解决了C字符串的安全性问题:自动内存管理,防止缓冲区溢出
- 提供了丰富的操作方法:查找、替换、分割等常用操作一应俱全
- 具有高效的实现:小字符串优化、写时拷贝等技术提升性能
- 支持现代C++特性:与STL算法、范围for等完美配合