一、学习string类的原因
1. C语言字符串的不足
-
本质:以
'\0'结尾的字符集合,依赖C标准库的str系列函数(如strcpy、strlen)操作。 -
缺陷:函数与字符串分离,不符合面向对象(OOP)思想;底层空间需用户手动管理,易出现内存泄漏或越界访问。
2. 实际应用需求
-
OJ题目中字符串相关问题多以
string类形式出现。 -
工作中使用
string类更简洁、高效,无需关注底层空间管理。
二、C++11辅助语法(方便string类使用)
1. auto关键字
(1)核心作用
- C++11中作为类型指示符,编译器在编译时自动推导变量类型,无需显式声明。
(2)使用规则
|------|----------------------------------|-----------------------------------------------------|
| 规则 | 示例 | 说明 |
| 指针声明 | auto* p = &x; 与 auto p = &x; | 效果一致,均可推导为指针类型 |
| 引用声明 | auto& ref = x; | 必须加&,否则推导为值类型 |
| 多行声明 | auto a = 1, b = 2; | 同一行变量类型必须相同,auto c = 3, d = 4.0; 编译报错 |
| 限制场景 | 不可作为函数参数、不可直接声明数组 | void func(auto a){} 报错;auto arr[] = {1,2,3}; 报错 |
| 适用场景 | 容器迭代器简化 | map<string, string>::iterator it 可简化为 auto it |
2. 范围for循环
(1)语法格式
cpp
for (auto 迭代变量 : 可迭代对象) {
// 循环体
}
- 第一部分:迭代变量(可加
&修改原对象);第二部分:被迭代范围(数组、容器等)。
(2)核心优势
-
自动迭代、自动取数据、自动判断结束,无需手动控制索引,减少越界错误。
-
底层原理:替换为迭代器遍历(汇编层面可验证)。
(3)示例
cpp
int arr[] = {1,2,3,4,5};
for (auto& e : arr) e *= 2; // 修改原数组元素
for (auto e : arr) cout << e; // 遍历输出
string str = "hello";
for (auto ch : str) cout << ch; // 遍历字符串
三、string类核心知识点
1. 头文件与命名空间
-
必须包含:
#include <string> -
需使用命名空间:
using namespace std;(否则需写std::string)
2. 常见构造函数(重点)
|----------------------------|--------------|---------------------------------|
| 构造函数 | 功能说明 | 示例 |
| 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); |
3. 容量操作(重点)
|----------------------------|-----------|-------------------------------------------------------------------------------|
| 函数 | 功能说明 | 注意事项 |
| size() | 返回有效字符长度 | 与length()功能一致,推荐使用size()(与其他容器接口统一) |
| capacity() | 返回总空间大小 | 大于等于size(),预分配空间以减少扩容次数 |
| empty() | 判断是否为空串 | 返回bool值,空串返回true |
| clear() | 清空有效字符 | 仅重置size()为0,不改变capacity()(底层空间不变) |
| reserve(size_t n) | 预留n个字符空间 | 1. 不改变有效字符数;2. n小于当前capacity()时无效果;3. 提前预留空间可避免频繁扩容 |
| resize(size_t n, char c) | 调整有效字符数为n | 1. n>原size():新增部分用c填充(默认用\0),可能扩容;2. n<原size():仅截断有效字符,capacity()不变 |
示例
cpp
string s = "hello";
s.reserve(10); // 预留10个空间,capacity()≥10,size()仍为5
s.resize(8, 'x'); // 有效字符变为8个,"helloxxx",size()=8
s.clear(); // size()=0,capacity()仍为10
4. 访问与遍历(重点)
|---------------------|-----------|-------------------------------------------------------------------------|
| 方式 | 功能说明 | 示例 |
| operator[] | 访问pos位置字符 | string s = "hello"; cout << s[1]; // 输出'e'(pos从0开始) |
| begin() + end() | 迭代器遍历 | for (auto it = s.begin(); it != s.end(); ++it) cout << *it; |
| rbegin() + rend() | 反向迭代器遍历 | for (auto it = s.rbegin(); it != s.rend(); ++it) cout << *it; // 反向输出 |
| 范围for | 简洁遍历 | for (auto ch : s) cout << ch; |
5. 修改操作(重点)
|----------------------------------|------------|------------------------------------------|
| 函数 | 功能说明 | 示例 |
| push_back(char c) | 尾插单个字符 | s.push_back('!'); |
| append(const string& str) | 追加字符串 | s.append("world"); |
| operator+= | 追加字符/字符串 | 推荐使用!s += 'a'; s += "bc";(支持多种类型,简洁高效) |
| c_str() | 返回C风格字符串 | 用于兼容C库函数,返回const char*(以'\0'结尾) |
| find(char c, size_t pos=0) | 从pos开始向后找c | 返回字符位置(size_t类型),未找到返回string::npos |
| rfind(char c, size_t pos=npos) | 从pos开始向前找c | 用法同find,反向查找 |
| substr(size_t pos, size_t n) | 从pos截取n个字符 | 返回截取后的子串,n省略则截取到末尾 |
关键注意
-
尾插效率:
push_back≈append≈operator+=,operator+=功能最灵活(支持字符/字符串)。 -
查找技巧:结合
npos判断是否找到,如if (s.find('a') != string::npos)。
示例
cpp
string s = "hello";
s += " world"; // "hello world"
size_t pos = s.find('w'); // pos=6
string sub = s.substr(6, 5); // "world"
6. 非成员函数(重点)
|------------------------|---------|------------------------------|
| 函数 | 功能说明 | 示例 |
| operator>> | 输入字符串 | cin >> s;(遇到空格/换行结束) |
| operator<< | 输出字符串 | cout << s; |
| getline(cin, s) | 读取一行字符串 | 包含空格,解决cin >> s的局限 |
| relational operators | 字符串比较 | 支持==、!=、<、>等(按字典序比较) |
示例
cpp
string s;
getline(cin, s); // 读取整行,包括空格
cout << (s == "hello") << endl; // 比较字符串是否相等
7. 底层结构差异(vs vs g++)
|----------|------|------------------------------------------------------------------------|
| 编译器 | 占用空间 | 核心设计 |
| vs(32位) | 28字节 | 1. 联合体存储:字符串长度<16时用内部固定数组(效率高),≥16时从堆分配;2. 包含size、capacity、指针字段 |
| g++(32位) | 4字节 | 1. 写时拷贝(Copy-On-Write);2. 仅存一个指针,指向堆空间(包含size、capacity、引用计数、字符串数据) |
四、OJ实战题目(核心应用)
1. 仅仅反转字母
2. 找字符串中第一个只出现一次的字符
3. 最后一个单词的长度
4. 验证回文串
5. 字符串相加
五、string类模拟实现(面试重点)
1. 核心问题:浅拷贝 vs 深拷贝
(1)浅拷贝(问题)
-
编译器默认拷贝构造/赋值运算符重载:仅拷贝指针地址,导致多个对象共享同一块内存。
-
后果:对象销毁时重复释放内存,程序崩溃。
(2)深拷贝(解决方案)
-
每个对象独立分配内存,拷贝数据内容,不共享资源。
-
必须显式实现:拷贝构造函数、赋值运算符重载、析构函数。
2. 深拷贝实现(传统版)
cpp
class String {
public:
// 构造函数
String(const char* str = "") {
if (nullptr == str) str = ""; // 处理nullptr
_str = new char[strlen(str) + 1]; // 分配空间(含'\0')
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() {
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
3. 深拷贝实现(现代版,更简洁)
cpp
class String {
public:
String(const char* str = "") {
if (nullptr == str) str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造:利用临时对象交换资源
String(const String& s) : _str(nullptr) {
String tmp(s._str); // 构造临时对象
swap(_str, tmp._str); // 交换指针,临时对象销毁时释放旧空间
}
// 赋值运算符重载:传值参数生成临时对象,交换后临时对象销毁
String& operator=(String s) {
swap(_str, s._str);
return *this;
}
~String() {
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
4. 写时拷贝(了解)
-
基于浅拷贝+引用计数:读取时共享资源,写入时才进行深拷贝。
-
核心:用引用计数记录资源使用者数量,销毁时计数减1,计数为0时释放资源。
-
缺陷:多线程环境下需处理线程安全问题,现代STL中部分已弃用。
六、课后作业
七、复习要点
-
核心接口:构造、
size()/capacity()、resize()/reserve()、operator[]、find()、substr()、operator+=。 -
底层特性:扩容机制、vs/g++结构差异、浅拷贝与深拷贝。
-
实战应用:OJ题目中的字符串遍历、反转、查找、拼接、大数运算。
-
面试重点:string类模拟实现(深拷贝的两种写法)。