📑 目录
1 为啥偏要学 string? 告别 C 语言字符串的 “坑”,少踩雷
2 标准库 string 类:必学招式 从创建到修改,手把手教你用对接口
3 不同编译器的小秘密 知道这些,调试不懵圈
4 OJ 刷题实战:5 道题练会核心用法 学完就上手,成就感拉满
5 模拟实现 string:搞定面试高频题 从原理到代码,初学者也能看懂的深拷贝
6 避坑 & 复习指南 划重点、防踩坑,复习不迷路
- 为啥偏要学 string?
作为刚学 C++ 的小白,一开始我也疑惑:C 语言的char[]不是也能存字符串吗?为啥还要专门学string类?直到踩了这些坑才明白 ——
1.1 C 语言字符串的 “血泪教训”
新手写 C 语言字符串,最容易犯这几个错:
- ✖️ 手动分配空间:比如char str[10]存 “hello world”,直接越界,程序崩了都不知道为啥;
- ✖️ 忘记加’\0’:用strcpy拷贝时,少加结束符,打印出来全是乱码;
- ✖️ 手动释放内存:用malloc申请的字符串,忘了free,妥妥的内存泄漏。
举个我踩过的坑:
// 新手错误示范:空间不够+忘加'\0'
char str[5];
strcpy(str, "hello"); // "hello"是6个字符(含'\0'),直接越界
printf(str); // 输出乱码,甚至程序崩溃
1.2 string 类的 “新手友好 buff”
用string类之后,这些问题全没了:
- ✅ 自动管理空间:存多长的字符串都不用手动分配,底层会自动扩容;
- ✅ 自带操作函数:拼接、查找、比较都有现成方法,不用记strcat/strcmp这些零散函数;
- ✅ 更安全:访问字符时越界会有提示(新手能快速定位问题),而 C 语言直接崩。
比如同样存 “hello”,string 只要一行:
string str = "hello"; // 不用管空间,不用加'\0',新手友好!
2. 标准库 string 类:必学招式
先划重点:用string必须加头文件,还要写using namespace std;(新手别忘,不然编译报错)。
2.1 先搞懂:string 到底是个啥?
- 对新手来说,不用记复杂的模板定义,简单理解:
- string就是 C++ 专门用来存字符串的 “容器”,把字符串的 “存、取、改、删” 都封装好了,我们不用关心底层咋实现,只管调用方法就行。
2.2 必会:创建 string 对象(构造函数)
这 4 种是最常用的,记牢就行:

#include <iostream>
#include <string>
using namespace std; // 新手别漏!
int main() {
string s1; // 空字符串
string s2("我是新手,学string"); // 存中文也没问题(注意编码)
string s3(3, '*'); // 存"***"
string s4(s2); // 拷贝s2,s4也是"我是新手,学string"
cout << "s1是空的:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
return 0;
}
2.3 必懂:容量操作(别被 “空间” 搞晕)
起初我们最容易搞混size()、capacity()、resize()、reserve(),用 “装水的杯子” 比喻帮你理解:
- size():杯子里实际装了多少水(有效字符数);
- capacity():杯子总共能装多少水(底层空间大小);
- resize(n):把水的量改成 n 杯,不够就加水(补 ‘\0’),多了就倒掉;
- reserve(n):把杯子的容量改成 n 杯,水的量不变(提前留空间,避免频繁换杯子)。
int main() {
string s = "hello";
cout << "当前长度:" << s.size() << endl; // 输出5
cout << "当前容量:" << s.capacity() << endl; // 不同编译器结果不同,比如VS可能是15
s.resize(8, '!'); // 改成8个字符,补"!!!"
cout << "resize后:" << s << endl; // 输出"hello!!!"
cout << "resize后长度:" << s.size() << endl; // 8
cout << "resize后容量:" << s.capacity() << endl; // 还是15(没超原容量)
s.reserve(20); // 预留20个空间
cout << "reserve后容量:" << s.capacity() << endl; // 变成20
cout << "reserve后长度:" << s.size() << endl; // 还是8(长度不变)
s.clear(); // 清空内容
cout << "clear后:" << s << endl; // 空字符串
cout << "clear后容量:" << s.capacity() << endl; // 还是20(空间没释放)
return 0;
}
2.4 必练:访问和遍历(3 种简单方法)
遍历字符串就是 “逐个看字符”,先掌握这 3 种,足够应付大部分场景:
方法 1:数组式访问
string s = "hello";
for (int i = 0; i < s.size(); i++) {
cout << s[i] << " "; // 输出h e l l o
}
✅ 备注:s[i]可以直接改字符,比如s[0] = ‘H’,s 就变成 “Hello”。
方法 2:范围 for(C++11,最简洁)
string s = "hello";
for (char ch : s) { // 逐个取字符存到ch里
cout << ch << " ";
}
// 想改字符就加&:for (char& ch : s) { ch = toupper(ch); }
✅ 备注:不用记下标,适合只想遍历不想改下标的场景
方法 3:迭代器(了解即可,后续学 STL 再深入)
string s = "hello";
for (string::iterator it = s.begin(); it != s.end(); it++) {
cout << *it << " ";
}
✅ 备注:迭代器是 STL 通用的遍历方式,现在先知道有这个东西就行。
2.5 必用:修改字符串(拼接 / 查找 / 截取)
这部分是 string 最实用的功能,重点记+=、find、substr。
核心函数表

int main() {
string s = "hello";
s += " world"; // 拼接成"hello world"
cout << s << endl;
int pos = s.find('w'); // 找'w'的位置,返回6
if (pos != string::npos) { // 找到才处理(npos表示没找到)
cout << "找到'w',位置:" << pos << endl;
}
string sub = s.substr(6, 5); // 从6开始取5个字符,得到"world"
cout << "截取的子串:" << sub << endl;
// 转C语言字符串(新手用printf时需要)
printf("C语言风格输出:%s\n", s.c_str());
return 0;
}
✅ 避坑:find没找到会返回string::npos(值是 - 1),一定要判断,不然会出错!
2.6 必知:输入输出(别踩 cin 的坑)
- cin >> s:输入字符串,但遇到空格 / 回车就停(比如输入 “hello world”,s 只存 “hello”);
- getline(cin, s):输入整行字符串,包括空格(新手处理带空格的输入必用)。
int main() {
string s1, s2;
cout << "输入带空格的字符串(cin):";
cin >> s1; // 输入"hello world"
cout << "cin结果:" << s1 << endl; // 只输出"hello"
cin.ignore(); // 清空缓冲区(不然getline会读空)
cout << "输入带空格的字符串(getline):";
getline(cin, s2); // 输入"hello world"
cout << "getline结果:" << s2 << endl; // 输出"hello world"
return 0;
}
✅ 备注:用cin之后接getline,一定要加cin.ignore()清空缓冲区,不然getline会直接读空!
3. 不同编译器的小秘密(不用深究,了解即可)
不用纠结底层实现,但知道这些能避免调试时懵圈:
3.1 VS 编译器(Windows 常用)
- 用 “小字符串优化”:字符串长度 < 16 时,直接存在对象里(不用堆空间),效率高;
- 长度≥16 时,才从堆上分配空间;
- 新手看到capacity()初始值是 15 别慌(比如空字符串的 capacity 是 15),正常现象。
3.2 g++ 编译器(Linux 常用) - 早期用 “写时拷贝”:多个对象共享空间,改的时候才拷贝;
- 现在也逐渐用小字符串优化,新手知道 “不同编译器 capacity 结果不同” 就行。
4. OJ 刷题实战:5 道题练会核心用法
学完接口一定要刷题,这 5 道题都是新手能上手的,覆盖核心用法:
题 1:仅仅反转字母(双指针入门)
**题目:**只反转字符串中的字母,非字母保留原位(比如输入 “a-bC-dEf-ghIj”,输出 “j-Ih-gfE-dCba”)。
思路:
左指针从开头找字母,右指针从结尾找字母;
找到就交换,然后左指针右移,右指针左移;
直到左指针≥右指针。
#include <iostream>
#include <string>
#include <cctype> // isalpha函数头文件
using namespace std;
class Solution {
public:
string reverseOnlyLetters(string s) {
int left = 0; // 左指针
int right = s.size() - 1; // 右指针
while (left < right) {
// 左指针找字母(跳过非字母)
while (left < right && !isalpha(s[left])) {
left++;
}
// 右指针找字母(跳过非字母)
while (left < right && !isalpha(s[right])) {
right--;
}
// 交换字母
swap(s[left], s[right]);
left++;
right--;
}
return s;
}
};
int main() {
Solution sol;
string s = "a-bC-dEf-ghIj";
cout << "原字符串:" << s << endl;
cout << "反转后:" << sol.reverseOnlyLetters(s) << endl;
return 0;
}
题 2:找第一个只出现一次的字符(计数入门)
题目:返回字符串中第一个只出现一次的字符的下标(比如输入 “leetcode”,返回 0;输入 “loveleetcode”,返回 2)。
思路:
用数组统计每个字符出现的次数(ASCII 字符共 256 个,数组大小设 256);
再遍历字符串,找第一个计数为 1 的字符。
#include <iostream>
#include <string>
using namespace std;
class Solution {
public:
int firstUniqChar(string s) {
int count[256] = {0}; // 初始化为0
// 第一步:统计每个字符出现次数
for (char ch : s) {
count[ch]++; // 字符转ASCII码当下标
}
// 第二步:找第一个计数为1的字符
for (int i = 0; i < s.size(); i++) {
if (count[s[i]] == 1) {
return i;
}
}
return -1; // 没找到返回-1
}
};
int main() {
Solution sol;
string s1 = "leetcode";
string s2 = "loveleetcode";
cout << "s1第一个唯一字符下标:" << sol.firstUniqChar(s1) << endl; // 0
cout << "s2第一个唯一字符下标:" << sol.firstUniqChar(s2) << endl; // 2
return 0;
}
题 3:最后一个单词的长度(rfind 入门)
题目:输入一个字符串,返回最后一个单词的长度(比如输入 “Hello World”,返回 5;输入 “fly me to the moon”,返回 4)
思路:
- 用rfind找最后一个空格的位置;
- 总长度减去空格位置再减 1,就是最后一个单词的长度;
- 没有空格说明只有一个单词,返回总长度。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
// 注意:用getline读带空格的输入
while (getline(cin, s)) {
size_t pos = s.rfind(' '); // 找最后一个空格
// 处理末尾有空格的情况(比如"abc ")
while (pos == s.size() - 1) {
s.erase(pos); // 删除末尾空格
pos = s.rfind(' ');
}
if (pos == string::npos) { // 没有空格
cout << s.size() << endl;
} else { // 有空格
cout << s.size() - pos - 1 << endl;
}
}
return 0;
}
题 4:验证回文字符串(双指针 + 忽略非字母数字)
题目:忽略非字母数字、不区分大小写,判断是否是回文(比如输入 “A man, a plan, a canal: Panama”,返回 true;输入 “race a car”,返回 false)。
思路:
- 双指针分别从开头和结尾出发;
- 跳过非字母数字的字符;
- 把字母转成小写(或大写),比较是否相等;
- 只要有一对不相等,就不是回文。
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
class Solution {
public:
bool isPalindrome(string s) {
int left = 0;
int right = s.size() - 1;
while (left < right) {
// 跳过非字母数字
while (left < right && !isalnum(s[left])) {
left++;
}
while (left < right && !isalnum(s[right])) {
right--;
}
// 转小写比较
if (tolower(s[left]) != tolower(s[right])) {
return false;
}
left++;
right--;
}
return true;
}
};
int main() {
Solution sol;
string s1 = "A man, a plan, a canal: Panama";
string s2 = "race a car";
cout << "s1是否回文:" << boolalpha << sol.isPalindrome(s1) << endl; // true
cout << "s2是否回文:" << boolalpha << sol.isPalindrome(s2) << endl; // false
return 0;
}
题 5:字符串相加(模拟加法,必练)
题目:给定两个字符串形式的非负整数,返回它们的和(不能转成 int/long,因为数字太长)。
思路:
- 双指针从两个字符串末尾开始;
- 逐位相加,记录进位;
- 结果先尾插(避免头插效率低),最后反转。
#include <iostream>
#include <string>
#include <algorithm> // reverse函数头文件
using namespace std;
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.size() - 1; // num1末尾指针
int j = num2.size() - 1; // num2末尾指针
int carry = 0; // 进位
string res; // 结果
while (i >= 0 || j >= 0 || carry > 0) {
// 取当前位的数字,没有就取0
int n1 = (i >= 0) ? (num1[i] - '0') : 0;
int n2 = (j >= 0) ? (num2[j] - '0') : 0;
// 计算当前位和
int sum = n1 + n2 + carry;
carry = sum / 10; // 更新进位
res.push_back((sum % 10) + '0'); // 存当前位(转字符)
// 指针左移
i--;
j--;
}
// 反转结果(因为是尾插,顺序反了)
reverse(res.begin(), res.end());
return res;
}
};
int main() {
Solution sol;
string num1 = "123456789";
string num2 = "987654321";
cout << num1 << " + " << num2 << " = " << sol.addStrings(num1, num2) << endl; // 1111111110
return 0;
}
5. 模拟实现 string:搞定面试高频题(易懂)
新手一开始不用写完整的 string 类,但要搞懂深拷贝(面试必考),先从核心函数入手。
5.1 新手先懂:为啥要深拷贝?
如果用编译器默认的 “浅拷贝”,多个 string 对象会共享同一块空间:
- 比如string s1(“hello”); string s2 = s1;,s1 和 s2 的指针都指向同一块内存;
- 销毁 s1 时释放了内存,s2 的指针就成了 “野指针”,程序崩了!
深拷贝就是:每个对象都有自己的内存空间,互不影响。
5.2 新手能写的:深拷贝(传统版)
先写核心的构造、拷贝构造、赋值重载、析构,能看懂的写法:
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;
class MyString {
public:
// 构造函数:创建字符串
MyString(const char* str = "") {
assert(str != nullptr); // 防止传空指针
// 申请空间:长度+1(存'\0')
_str = new char[strlen(str) + 1];
strcpy(_str, str); // 拷贝内容
}
// 拷贝构造:深拷贝
MyString(const MyString& s) {
// 重新申请空间
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str); // 拷贝内容
}
// 赋值运算符重载:深拷贝
MyString& operator=(const MyString& s) {
// 防止自赋值(s = s)
if (this != &s) {
// 先释放旧空间
delete[] _str;
// 再申请新空间
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this; // 支持连续赋值
}
// 析构函数:释放空间
~MyString() {
if (_str) { // 防止重复释放
delete[] _str;
_str = nullptr; // 置空,避免野指针
}
}
// 辅助函数:打印字符串(新手调试用)
void print() {
cout << _str << endl;
}
private:
char* _str; // 存储字符串的指针
};
// 测试:深拷贝是否生效
int main() {
MyString s1("hello");
MyString s2 = s1; // 拷贝构造
MyString s3("world");
s3 = s1; // 赋值重载
s1.print(); // hello
s2.print(); // hello
s3.print(); // hello
return 0;
}
5.3 进阶一点:深拷贝(现代版,了解)
用 “交换” 简化代码,更优雅:
class MyString {
public:
MyString(const char* str = "") {
assert(str != nullptr);
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造:用临时对象交换
MyString(const MyString& s) : _str(nullptr) {
MyString tmp(s._str); // 创建临时对象
swap(_str, tmp._str); // 交换指针
}
// 赋值重载:传值参数(自动拷贝)
MyString& operator=(MyString s) {
swap(_str, s._str); // 交换后,临时对象销毁旧空间
return *this;
}
~MyString() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
void print() {
cout << _str << endl;
}
private:
char* _str;
};
✅手备注:现代版不用手动申请 / 释放空间,靠临时对象的析构自动处理,新手先理解传统版,再看现代版。
6. 小白避坑 & 复习指南
6.1 常见坑(避坑清单)
- ❌ 忘记加头文件:编译报错 “未定义的标识符 string”;
- ❌ 用cin读带空格的字符串:只读到空格前,改用getline;
- ❌ 忽略find的返回值:没找到时返回npos(-1),一定要判断;
- ❌ 混淆resize和reserve:resize改长度,reserve改容量;
- ❌ 浅拷贝导致崩溃:模拟实现时一定要写深拷贝。
6.2 复习路线
1.第一遍:记核心接口(+=、size、find、getline),能写简单的遍历和拼接;
2.第二遍:做 5 道实战题,掌握双指针、计数、模拟加法等思路;
3.第三遍:理解深拷贝的原理,能手写传统版深拷贝代码;
4.第四遍:回顾避坑点,调试自己写的代码,解决常见错误。
6.3 提问:常见疑问解答
Q:string 能存中文吗?
A:能!但要注意编码(比如 UTF-8),size()返回的是字节数(一个中文占 3 字节),不是字符数。
Q:为什么capacity()的初始值不一样?
A:不同编译器的扩容策略不同,新手不用纠结,知道reserve能预留空间就行。
Q:模拟实现 string 需要写所有函数吗?
A:不用!新手先写构造、拷贝构造、赋值重载、析构这 4 个核心函数,面试足够了
总结
核心用法:新手先掌握string的构造、+=拼接、find查找、getline输入、双指针遍历,能覆盖 80% 的刷题场景;
核心原理:深拷贝是string的灵魂,解决浅拷贝导致的内存问题,面试必考;
新手技巧:用 “杯子装水” 比喻理解容量操作,用刷题巩固接口使用,避开cin读空格、忘记判npos等坑。

1218

被折叠的 条评论
为什么被折叠?



