C++第八讲:string 类
string 是STL 中最常用的容器,也是所有 C++ 开发者每天都会用到的工具。它彻底解决了 C 语言字符串操作繁琐、容易越界、需要手动管理内存的痛点。
一、为什么必须学 string 类?
1. C 语言字符串的致命缺陷
C 语言中字符串是以\0结尾的字符数组,操作依赖<string.h>库函数,存在以下问题:
内存手动管理:需要自己申请 / 释放空间,容易内存泄漏
容易越界访问:strcpy、strcat 等函数不检查边界,导致缓冲区溢出
不符合面向对象思想:数据和操作分离,使用麻烦
功能单一:很多常用操作(如查找、替换、截取)需要自己实现
2. C++ string 类的优势
自动管理内存,无需手动申请释放
重载了常用运算符(
+、+=、[]、==等),操作直观提供了丰富的成员函数,满足绝大多数字符串操作需求
类型安全,不容易出现越界错误
一句话:工作中 99% 的字符串场景都用 string,几乎没人用 C 语言的字符数组。
二、前置知识:C++11 两个核心语法
在学习 string 之前,先掌握两个 C++11 的重要语法,后面会频繁用到。
1. auto 关键字:自动类型推导
作用
让编译器自动推导变量的类型,不用手动写复杂的类型名。
语法
auto 变量名 = 初始值;示例
int a = 10; auto b = a; // b自动推导为int auto c = 'a'; // c自动推导为char auto d = 3.14; // d自动推导为double // 最常用场景:简化复杂类型 #include <map> map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}}; // 不用写map<string, string>::iterator auto it = dict.begin();易错点
必须初始化:
auto e;编译报错,没有初始值无法推导类型同一行变量类型必须一致:
auto aa=1, bb=2.0;报错,int 和 double 类型不同声明引用必须加 &:
auto& m = a;是引用,auto m = a;是拷贝不能作为函数参数:
void func(auto a);编译报错
2. 范围 for 循环:简化遍历
作用
自动遍历数组、容器等有范围的集合,不用手动控制下标或迭代器。
语法
for (元素类型 变量名 : 遍历范围) { // 操作变量 }示例
// 遍历数组 int arr[] = {1,2,3,4,5}; for (auto e : arr) { cout << e << " "; } // 遍历string string s = "hello"; for (auto ch : s) { cout << ch << " "; } // 修改元素:加引用 for (auto& ch : s) { ch -= 32; // 转大写 }原理
范围 for 的底层就是迭代器,编译器会自动替换为迭代器遍历。
三、string 类常用接口(重点)
使用 string 必须包含头文件:#include <string>,且所有接口都在std命名空间中。
1. 构造函数(4 个最常用)
| 构造函数 | 功能说明 | 示例 |
|---|---|---|
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& s) | 拷贝构造 | string s4(s2); |
代码示例
#include <iostream> #include <string> using namespace std; int main() { string s1; // 空字符串 string s2("hello world"); // 用C字符串构造 string s3(5, 'x'); // "xxxxx" string s4(s2); // 拷贝s2 cout << s1 << endl; // 空 cout << s2 << endl; // hello world cout << s3 << endl; // xxxxx cout << s4 << endl; // hello world return 0; }2. 容量操作
| 函数 | 功能说明 | 注意事项 |
|---|---|---|
size_t size() const | 返回有效字符长度 | ✅ 推荐使用,和其他容器接口统一 |
size_t length() const | 返回有效字符长度 | 和 size () 完全一样,历史遗留 |
size_t capacity() const | 返回总容量(能存多少字符) | 容量≥size,预留空间避免频繁扩容 |
bool empty() const | 判断是否为空 | 空返回 true,否则 false |
void clear() | 清空有效字符 | 不改变底层容量 |
void reserve(size_t n) | 预留 n 个字符的空间 | 只改容量,不改 size;n < 当前容量时无作用 |
void resize(size_t n, char c='\0') | 把有效字符改为 n 个 | n>size:用 c 填充;n<size:截断;可能改变容量 |
核心区别:reserve vs resize
reserve:只预留空间,不改变有效字符个数,用于提前预估大小,避免频繁扩容resize:改变有效字符个数,会初始化新增的字符
代码示例
int main() { string s = "hello"; cout << s.size() << endl; // 5 cout << s.capacity() << endl; // 15(VS下短字符串优化) s.reserve(100); // 预留100个字符空间 cout << s.size() << endl; // 5(不变) cout << s.capacity() << endl; // 100 s.resize(10, 'a'); // 有效字符改为10个,新增的用'a'填充 cout << s << endl; // helloaaaaa cout << s.size() << endl; // 10 s.resize(3); // 截断为3个字符 cout << s << endl; // hel cout << s.size() << endl; // 3 cout << s.capacity() << endl; // 100(不变) s.clear(); // 清空 cout << s.size() << endl; // 0 cout << s.capacity() << endl; // 100(不变) return 0; }3. 访问与遍历(3 种方式)
| 方式 | 语法 | 特点 |
|---|---|---|
operator[] | s[pos] | ✅ 最常用,像数组一样访问,支持读写 |
| 迭代器 | begin()/end() | 通用所有容器,支持反向迭代器rbegin()/rend() |
| 范围 for | for (auto ch : s) | ✅ C++11 推荐,最简洁 |
代码示例
int main() { string s = "hello"; // 1. []访问(推荐) for (int i=0; i<s.size(); ++i) { cout << s[i] << " "; s[i] += 1; // 可以修改 } cout << endl; // 2. 迭代器 string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; // 反向迭代器:从后往前遍历 string::reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; ++rit; } cout << endl; // 3. 范围for(最简洁) for (auto ch : s) { cout << ch << " "; } cout << endl; return 0; }4. 修改操作(最常用)
| 函数 | 功能说明 | 推荐度 |
|---|---|---|
void push_back(char c) | 尾插一个字符 | ⭐⭐ |
string& operator+=(const string& str) | 追加字符串 / 字符 | ⭐⭐⭐ 最常用 |
string& append(const char* s) | 追加 C 风格字符串 | ⭐ |
const char* c_str() const | 返回 C 风格字符串(const char*) | ⭐⭐⭐ 用于和 C 语言接口交互 |
size_t find(char c, size_t pos=0) const | 从 pos 开始找 c,返回下标,找不到返回string::npos | ⭐⭐⭐ |
size_t rfind(char c, size_t pos=npos) const | 从 pos 开始往前找 c | ⭐⭐ |
string substr(size_t pos=0, size_t len=npos) const | 从 pos 开始截取 len 个字符返回 | ⭐⭐⭐ |
代码示例
int main() { string s = "hello"; // 追加 s += ' '; // 追加字符 s += "world"; // 追加字符串 cout << s << endl; // hello world // 查找 size_t pos = s.find('o'); if (pos != string::npos) { cout << "找到'o'在位置:" << pos << endl; // 4 } pos = s.find("world"); if (pos != string::npos) { cout << "找到'world'在位置:" << pos << endl; // 6 } // 截取子串 string sub = s.substr(6, 5); // 从位置6开始截取5个字符 cout << sub << endl; // world // 转C风格字符串 const char* cstr = s.c_str(); printf("%s\n", cstr); // hello world return 0; }5. 非成员函数
| 函数 | 功能说明 | 注意事项 |
|---|---|---|
operator+ | 字符串拼接 | ❌ 尽量少用,传值返回会产生深拷贝,效率低 |
operator>> | 输入字符串 | 遇到空格、换行结束 |
operator<< | 输出字符串 | 正常输出 |
getline(istream& in, string& s) | 读取一行字符串 | ✅ 读取带空格的字符串,遇到换行结束 |
relational operators | 大小比较(==、!=、<、>等) | 按字典序比较 |
易错点:cin vs getline
cin >> s:遇到空格、制表符、换行就停止,无法读取带空格的字符串getline(cin, s):读取整行,直到遇到换行符,会丢弃换行符
int main() { string s; // 错误:输入"hello world"只会读取"hello" // cin >> s; // 正确:读取整行 getline(cin, s); cout << s << endl; return 0; }四、string 的底层结构(面试高频)
不同编译器的 string 实现不同,主要有两种:VS 的短字符串优化和G++ 的写时拷贝。
1. VS 下的 string:短字符串优化(SSO)
结构(32 位平台占 28 字节)
一个联合体:
长度 < 16:用内部 16 字节的字符数组存储(栈上)
长度≥16:用堆空间存储,联合体存指向堆的指针
size_t _Mysize:有效字符长度size_t _Myres:总容量一个指针:用于其他管理
优势
大多数字符串长度都小于 16,直接用栈空间,不需要申请堆内存,效率更高。
2. G++ 下的 string:写时拷贝(COW)
结构(32 位平台占 4 字节)
只有一个指针,指向堆上的控制块:
size_t _M_length:有效长度size_t _M_capacity:总容量_Atomic_word _M_refcount:引用计数后面跟着实际的字符串数据
原理
多个 string 对象共享同一块堆内存,只有当某个对象修改字符串时,才会重新分配空间并拷贝数据(写时才拷贝)。
优势
拷贝构造和赋值效率极高,只需要拷贝指针和增加引用计数。
五、面试必考题:string 类的模拟实现
面试官几乎 100% 会让你手写 string 类的核心函数(构造、拷贝构造、赋值重载、析构),考察你对深拷贝和浅拷贝的理解。
1. 浅拷贝的问题
如果不自己实现拷贝构造和赋值重载,编译器会生成默认的浅拷贝,导致多个对象共享同一块内存,析构时重复释放,程序崩溃。
// 错误的string实现(浅拷贝) class String { public: String(const char* str = "") { _str = new char[strlen(str)+1]; strcpy(_str, str); } ~String() { delete[] _str; _str = nullptr; } private: char* _str; }; int main() { String s1("hello"); String s2(s1); // 浅拷贝,s1和s2的_str指向同一块内存 // 程序结束时,s2先析构释放内存,s1再析构时释放同一块内存,崩溃 return 0; }2. 深拷贝的两种实现方式
方式 1:传统版(容易理解)
自己申请独立的空间,拷贝数据,每个对象有自己的资源。
class String { public: // 构造函数 String(const char* str = "") { if (str == nullptr) { assert(false); return; } _str = new char[strlen(str)+1]; strcpy(_str, str); } // 拷贝构造:深拷贝 String(const String& s) { _str = new char[strlen(s._str)+1]; strcpy(_str, s._str); } // 赋值运算符重载:深拷贝 String& operator=(const String& s) { if (this != &s) { // 防止自己给自己赋值 // 先申请新空间 char* tmp = new char[strlen(s._str)+1]; strcpy(tmp, s._str); // 释放旧空间 delete[] _str; // 指向新空间 _str = tmp; } return *this; } // 析构函数 ~String() { if (_str) { delete[] _str; _str = nullptr; } } private: char* _str; };方式 2:现代版(更简洁高效)
利用局部对象的析构函数自动释放资源,通过 swap 交换指针。
class String { public: String(const char* str = "") { if (str == nullptr) { assert(false); return; } _str = new char[strlen(str)+1]; strcpy(_str, str); } // 拷贝构造 String(const String& s) : _str(nullptr) { String tmp(s._str); // 构造临时对象 swap(_str, tmp._str); // 交换指针,tmp析构时释放旧空间 } // 赋值运算符重载(现代版) String& operator=(String s) { // 传值参数,自动拷贝构造 swap(_str, s._str); // 交换指针,s析构时释放旧空间 return *this; } ~String() { if (_str) { delete[] _str; _str = nullptr; } } private: char* _str; };六、经典 OJ 实战(笔试必练)
1. 反转字符串中的字母
class Solution { public: bool isLetter(char ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } string reverseOnlyLetters(string 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--; swap(s[left], s[right]); left++; right--; } return s; } };2. 字符串中第一个只出现一次的字符
class Solution { public: int firstUniqChar(string s) { int count[256] = {0}; // 统计每个字符出现次数 for (char ch : s) { count[ch]++; } // 找第一个出现一次的字符 for (int i=0; i<s.size(); ++i) { if (count[s[i]] == 1) { return i; } } return -1; } };七、本章核心总结
string 是 STL 最常用的容器,自动管理内存,操作简单安全
常用接口:构造、size、operator []、+=、find、substr、c_str、getline
底层实现:VS 用短字符串优化,G++ 用写时拷贝
面试必考点:深拷贝的实现,浅拷贝的问题
易错点:cin 和 getline 的区别,operator + 的效率问题,clear 不改变容量