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 关键字:自动类型推导
作用
让编译器自动推导变量的类型,不用手动写复杂的类型名。
语法
cpp
auto 变量名 = 初始值;
示例
cpp
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 循环:简化遍历
作用
自动遍历数组、容器等有范围的集合,不用手动控制下标或迭代器。
语法
cpp
for (元素类型 变量名 : 遍历范围) {
// 操作变量
}
示例
cpp
// 遍历数组
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); |
代码示例
cpp
#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:改变有效字符个数,会初始化新增的字符
代码示例
cpp
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 推荐,最简洁 |
代码示例
cpp
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 个字符返回 | ⭐⭐⭐ |
代码示例
cpp
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):读取整行,直到遇到换行符,会丢弃换行符
cpp
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. 浅拷贝的问题
如果不自己实现拷贝构造和赋值重载,编译器会生成默认的浅拷贝,导致多个对象共享同一块内存,析构时重复释放,程序崩溃。
cpp
// 错误的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:传统版(容易理解)
自己申请独立的空间,拷贝数据,每个对象有自己的资源。
cpp
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 交换指针。
cpp
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. 反转字符串中的字母
cpp
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. 字符串中第一个只出现一次的字符
cpp
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 不改变容量