当前位置: 首页 > news >正文

深度剖析 C++ 之 string(上)篇

前言

在 C++ 的世界里,std::string 既熟悉又容易被低估
很多初学者在刚接触它时,觉得无非就是个“更好用的 char 数组”,但真正用得深入,你会发现——string 远比你想象的强大,也更容易踩坑。

无论是在刷题、写项目,还是面试中,字符串处理几乎无处不在:

  •  算法题:翻转、查找、替换、拼接……

  •  工程代码:日志处理、输入输出、协议解析……

  •  面试问题:SSO、小字符串优化、底层实现……

本篇是笔者 C++ 专栏中 “string”上篇
我会带你从零开始,一步步掌握 std::string常用接口、使用技巧、经典易错点。


本文分为上下两篇:

  • 上篇:完整讲解 string 使用层面,包含构造、访问、容量管理、修改、查找、I/O、易错点和性能技巧

  • 下篇:深入剖析 string 实现原理,手写一个 string 类,理解它背后的原理。

准备好了吗?让我们从 C 字符串 vs std::string 开始,一起深挖 C++ 字符串的世界。


一、C 字符串 vs std::string

在 C 语言里,字符串就是一块 char[]char* 内存,以 \0 结尾。操作全靠 strlenstrcpystrcmp 这些函数来完成,数据和操作是分开的,一不小心就越界或者内存泄漏了

char str[10] = "hello";
strcpy(str, "world");
printf("%s\n", str);

C++ 给我们提供了 std::string,它内部帮我们管理内存,接口也丰富多了,拼接、查找、插入、删除啥都有。更重要的是:它用得舒服,错也错得少!

#include <iostream>
#include <string>
using namespace std;int main() {string s = "hello";s += " world";cout << s << endl;  // hello world
}

二、构造、赋值、拷贝与移动

1. 常见构造方式

 string 的构造方式非常灵活,最常用的几种就是:

string s1;               // 默认构造,空字符串 ""
string s2("hello");      // 从 C 字符串构造
string s3(5, 'x');       // 构造 "xxxxx"
string s4(s2);           // 拷贝构造,s4 == "hello"

 小提示:

  • 默认构造出来的 string 为空,但内部还是有对象结构的,size() 是 0。
  • (n, ch) 这种构造在初始化固定长度时很方便,刷题的时候经常用来预分配。

2. 赋值与拼接

 除了构造之外,我们经常会给 string 重新赋值,或者在后面拼接一些内容:

string a = "hello";
string b;b = a;                   // 拷贝赋值,b 变成 "hello"
b += " world";           // 在 b 后拼接一个字符串
b.append("!!!");         // append 和 += 效果类似,也是追加
cout << b << endl;       // hello world!!!

区别:

  • = 是整体替换。
  • +=append 是追加。append 比较适合循环中拼接,因为它语义更明确,效率也不错。

3. 移动语义(C++11)

 移动构造听起来有点高深,其实就是“把资源偷过来”,避免不必要的拷贝

string x = "abc";
string y = std::move(x);   // y 接管了 x 的内部资源
cout << y << endl;        // abc
// x 处于“有效但内容未定义”的状态,不要再依赖 x 的值

在返回值、容器中插入、临时对象传递时,移动语义非常有用,能显著减少拷贝。


三、元素访问与遍历

下面我们来讲讲 string 怎么访问字符,怎么遍历掌握好迭代器和访问方式,能写出非常优雅的字符串处理代码。


1. 下标与 at()

operator[] 就是最直观的访问方式,就像访问数组一样;

at() 的功能差不多,但是多了边界检查。

string s = "hello";
cout << s[1] << endl;    // 'e'
s[1] = 'X';              // 修改也没问题
cout << s << endl;       // hXllo

但要注意:如果你写 s[100]越界了不会报错,结果是未定义行为,很可能直接崩。

如果你想更安全一点,可以用 at()

try {string s = "abc";cout << s.at(10);    // 越界,抛异常
} catch (out_of_range& e) {cout << "越界啦:" << e.what() << endl;
}

 小结:

  • 调试期、边界敏感:用 at(),安全。
  • 性能敏感、确定没越界:用 [],快。

2. 迭代器使用

迭代器就像一个“指针”,指向字符串里的每个字符,让你可以用统一的方式遍历、修改、配合算法使用

正向迭代器

最基本的遍历:

string s = "abc";
for (string::iterator it = s.begin(); it != s.end(); ++it) {cout << *it << ' ';
}
// 输出:a b c

 这里 it 的类型可以省略成 auto,写起来更舒服:

for (auto it = s.begin(); it != s.end(); ++it) {*it = toupper(*it);   // 也可以直接修改
}

const_iterator(只读遍历)

 如果你只是想读取字符,不打算改内容,用 cbegin() / cend(),迭代器类型是只读的:

string s = "xyz";
for (auto it = s.cbegin(); it != s.cend(); ++it) {cout << *it << ' ';
}
// 输出:x y z

反向迭代器

rbegin()rend() 是反着遍历,从最后一个字符开始向前:

string s = "12345";
for (auto rit = s.rbegin(); rit != s.rend(); ++rit) {cout << *rit << ' ';
}
// 输出:5 4 3 2 1

这个在需要倒序输出、逆序处理的时候很好用,比如字符串翻转。


迭代器 + STL 算法

迭代器的真正威力在于能和算法库搭配

例如查找字符:

#include <algorithm>
string s = "Hello World";
auto it = find(s.begin(), s.end(), 'W');
if (it != s.end()) {cout << "找到 W,位置是:" << distance(s.begin(), it) << endl;
}

或者一行把字母全转大写:

transform(s.begin(), s.end(), s.begin(),[](unsigned char c){ return toupper(c); });
cout << s << endl; // HELLO WORLD

这些算法的好处是写法统一、可读性好,学会了可以直接套到各种容器上,不仅仅是 string


四、大小与容量管理

string 其实就像一个动态数组,它内部维护了 “当前长度(size)” “分配的空间(capacity)”,通过一些成员函数可以方便地管理。


1. size / length

这俩是最基本的函数,用来获取字符串中字符的个数(不包含 \0 结尾),length()size() 完全等价:

string s = "hello";
cout << s.size() << endl;    // 5
cout << s.length() << endl;  // 5

小提示:

  • size() 是 STL 容器的标准写法;
  • length() 保留是为了和 C 风格名字兼容,选哪个都行。

2. capacity

capacity() 返回的是底层已经分配的空间大小(不一定等于当前字符串长度)。它和 size() 的区别就像“房间实际使用面积 vs 房间总面积”

string s = "abc";
cout << s.size() << endl;      // 3
cout << s.capacity() << endl;  // 比 3 大,具体值和实现有关

小知识:

  • capacity 一般是按照倍数扩容,比如 15、31、63……,这是为了减少频繁分配的开销。
  • 你可以通过 reserve() 来提前申请空间。

3. empty / clear

empty() 用来判断字符串是不是空的;clear() 是清空内容,但不会释放空间:

string s = "abc";
cout << s.empty() << endl;  // false
s.clear();
cout << s.empty() << endl;  // true

注意:

  • clear() 并不会让 capacity 变小,只是 size 变成 0;
  • 所以清空后再次拼接,不会重新分配内存,效率挺高的。

4. reserve

reserve(n) 是一个非常实用的性能优化接口,它告诉 string:“我接下来大概要放 n 个字符,麻烦你先准备好空间”。
这样后面 append/push_back 的时候就不会频繁扩容了:

string s;
s.reserve(100);   // 预留 100 个空间
cout << s.capacity() << endl; // >= 100

容易忽略:在循环里频繁用 s = s + "xxx",没预留空间,会不断重新分配 + 拷贝,性能会非常差。


5. resize

resize(n) 用来改变字符串的长度:

  • 如果 n 比原来大,多出来的字符会被填充(默认 \0 或你指定的字符);
  • 如果 n 比原来小,就截断。
string s = "abc";
s.resize(5, 'x');  // abcxx
cout << s << endl;s.resize(2);       // ab
cout << s << endl;

resize 经常在字符串预分配或清零时用,比如题目里让你“先准备好长度为 N 的字符串再填充”。


五、修改操作

修改操作是 string 最常用的一类函数,包括:往后追加、在中间插入、删除子串、替换等。用得好可以极大简化代码量。


1. push_back

往字符串尾部追加一个字符:

string s = "hi";
s.push_back('!');
cout << s << endl;  // hi!

常用场景:循环逐个字符拼接时,用 push_back+= 高效而且语义更清晰。


2. append

往字符串尾部追加一个字符串:

string s = "hello";
s.append(" world");
cout << s << endl;  // hello world

你也可以追加另一个 string:

string a = "C++";
string b = " string";
a.append(b);
cout << a << endl;  // C++ string

注意:

  • append 只是追加,不会替换原有内容;
  • 它比多次 += 更清晰,尤其是循环拼接时。

3. += 运算符

 +=最常用的拼接方式,其实本质和 append 差不多:

string s = "I love";
s += " C++";
s += '!';
cout << s << endl;  // I love C++!

4. insert

 insert 用来在指定位置插入内容:

string s = "world";
s.insert(0, "hello ");   // 在开头插入
cout << s << endl;      // hello world

你也可以插入单个字符:

s.insert(s.begin(), '[');
cout << s << endl;  // [hello world

注意:在中间插入的复杂度是 O(n),因为底层要移动字符。


5. erase

erase 用来删除指定位置或区间的字符

string s = "abcdef";
s.erase(1, 3);  // 从下标1开始删3个 -> aef
cout << s << endl;

也可以用迭代器删除:

s.erase(s.begin() + 1);  // 删掉 'e'
cout << s << endl;      // af

小提示:

  • erase(pos, len) 是最常用的;
  • erase(it) 适合在遍历时删单个字符;
  • 支持 erase(begin(), end()-1) 这种整段删除。
s.erase(s.begin(), s.end()); // 整体清空

6. replace

replace(pos, len, str) 用来把某一段替换成另一个字符串:

string s = "I like dogs";
s.replace(7, 4, "cats");  // 从下标7起,替换4个字符
cout << s << endl;       // I like cats

这个在字符串模板替换、敏感词替换等场景非常好用。

常见坑:

  • 替换后字符串长度可能会变长或变短,注意下标偏移;
  • 在循环替换时要更新起始位置,避免死循环
string s = "a b c";
size_t pos = s.find(' ');
while (pos != string::npos) {s.replace(pos, 1, "%%");pos = s.find(' ', pos + 2); // 跳过刚插入的 "%%"
}

六、查找与子串

查找和切片是处理字符串时的“高频武器”,灵活掌握 findrfindsubstr,可以轻松搞定路径解析、文本匹配、OJ 字符串题等。


1. find

 find 会从左到右查找目标字符串或字符,返回第一次出现的位置,如果找不到,就返回 string::npos

string s = "banana";
size_t pos = s.find("na");
if (pos != string::npos) {cout << "找到 na,位置:" << pos << endl;
}

千万不要拿 find 的结果和 -1 比较,这是个经典易错点。


2. rfind

 rfind 从右往左找,适合用来提取文件后缀最后一个分隔符

string file = "test.cpp";
size_t dot = file.rfind('.');
if (dot != string::npos) {string ext = file.substr(dot);cout << ext << endl;  // .cpp
}

3. substr

substr(pos, len) 是用来截取子串的,跟 Python 的切片差不多

string s = "abcdef";
string t = s.substr(2, 3);  // 从下标2截3个,cde
cout << t << endl;

如果省略 len,就会截到字符串末尾

cout << s.substr(2) << endl;  // cdef

 注意:

  • 如果 pos > size() 会抛 out_of_range 异常;
  • 如果 pos + len > size(),就截取到末尾,不会抛异常。

七、I/O 与非成员函数

std::string 不仅可以用各种成员函数,还支持很多“操作符”或者标准 I/O 函数,我们来把它们串一下。


1. cin / cout

直接用 cincout 就能输入输出 string,比 scanf / printf 安全太多了

string s;
cin >> s;   // 输入:hello world
cout << s << endl;  // hello

注意:

  • cin >> s 遇到空格就会停止输入,所以如果你想读取整行(包括空格),要用 getline。


2. getline

getline(cin, str) 可以一次性读入一整行内容(包括空格)

string line;
getline(cin, line);
cout << "你输入的是:" << line << endl;

小贴士:

  • 如果你前面用了 cin >>,再立刻用 getline,有可能会读到一个空行。原因是 cin >> 读取完后,换行符还留在缓冲区。
    解决方法:

cin >> s;
cin.ignore();   // 忽略掉一个换行
getline(cin, line);

3. operator+

 + 运算符可以把多个字符串拼接在一起,非常直观

string a = "hello";
string b = "world";
string c = a + " " + b;
cout << c << endl;  // hello world

但是!

string d = "C++" + b;  // 错误!因为 "C++" 是 const char*

 这个是高频坑:

  • "abc" + string 会报错(因为前面是 char*)
  • string + "abc" 是 OK 的
  • 解决办法:把第一个字符串也改成 string
string d = string("C++") + b;  // 正确

3. 比较运算符

 ==!=<><=>= 都可以直接用在 string 上:

string a = "abc", b = "abd";
cout << (a == b) << endl; // 0
cout << (a < b) << endl;  // 1 (按字典序比较)

底层是按字典序比较,就像词典里查单词顺序一样。
这在排序时特别好用:

vector<string> v = {"banana", "apple", "cherry"};
sort(v.begin(), v.end());
for (auto& s : v) cout << s << " ";

输出:

apple banana cherry

八、常见易错点

1. find 返回值和 -1 比较

 这是最高频的错误之一

string s = "abc";
if (s.find('x') == -1) {   // 错了cout << "没找到" << endl;
}

find 返回的是 size_t 类型(无符号),没找到返回 string::npos,这个值其实是 size_t 的最大值。
你拿它和 -1 比较,会发生隐式类型转换,逻辑不对了。

正确写法:

if (s.find('x') == string::npos) {cout << "没找到" << endl;
}

2. c_str() 指针失效

 c_str() 返回的是一个 内部的 const char 指针*,如果字符串内容改变,这个指针就可能失效

string s = "hello";
const char* p = s.c_str();
s += " world";      // 可能导致 p 失效
cout << p << endl;  // 未定义行为

正确做法:**每次使用前重新获取 c_str()**

const char* p = s.c_str();
send_to_api(p);

3. cin >> 和 getline 混用

 这个坑前面提过一次,但太多人中招必须再强调:

string a, b;
cin >> a;
getline(cin, b);    // 错误!b 可能读到空行

原因是 cin >> 读完后,缓冲区里还留着 \ngetline 直接读走了这个换行。

解决办法:

cin >> a;
cin.ignore();       // 清掉换行
getline(cin, b);

4. substr 越界

 substr(pos, len) 如果 pos 超过 size,就会抛异常:

string s = "abc";
try {string t = s.substr(10, 2);
} catch (out_of_range& e) {cout << "越界啦:" << e.what() << endl;
}

所以使用前最好加个判断:

if (pos < s.size()) {string t = s.substr(pos, 2);
}

5. operator+ 拼接效率问题

很多人喜欢在循环里用 s = s + "x"; 来拼接字符串,这其实是个性能大坑:

string s;
for (int i = 0; i < 1000; ++i) {s = s + "x";   // 每次都生成临时字符串,效率极差
}

高效写法:

s.reserve(1000);    // 先预留
for (int i = 0; i < 1000; ++i) {s.push_back('x');
}

九、性能与实践建议

1. 参数传递:用 const string&

如果读者写函数时参数是 string,一定要传引用

void printString(const string& s) {cout << s << endl;
}

不推荐写法:

void printString(string s);  // 每次都会拷贝一份

2. 返回值:放心返回局部变量

C++11 之后编译器会自动做 RVO(返回值优化)或移动,不用担心性能:

string make_hello() {string s = "hello";return s;   // 不会真的拷贝
}

3. 拼接:少用 +,多用 append

+ 很方便,但频繁拼接性能很差:

string s;
s += "abc";
s.append("def");   // 更合适

4. 与 C 交互:小心 c_str()

使用 c_str() 传给 C 接口时,记得它只是临时借用,string 改变就失效。
要么每次都现取,要么自己拷贝一份出来。


5. 多线程:读安全,写要同步

string 自身不是线程安全的。多个线程同时读没问题但只要有一个线程在改,就必须加锁。


总结

到这里,本篇笔者完整地把 C++ std::string 的基础用法讲了个遍:

  • 构造、赋值、移动、拼接
  • 元素访问与迭代器
  • 容量管理、修改操作、查找子串
  • 输入输出与易错点
  • 性能技巧

这一篇的目标就是让读者在写实际项目、刷算法题时,不再对 string 模糊不清,遇到问题能立刻想到对应的接口来解决。


深度剖析 C++ 之 string(下)篇 中,我们将进入“幕后”:

  • 探秘 string 的内部结构:SSO(小字符串优化)、内存布局
  • 自己手写一个 string 类
  • 现代形式的拷贝赋值 & 自定义迭代器
  • 扩展功能实现

看完下篇,读者不仅能用 string,还能实现一个 string!

http://www.dtcms.com/a/443054.html

相关文章:

  • 前端界面不会在浏览器上显示
  • 企业网站选wordpress和织梦科技企业网站如何建设
  • OpenCoordV1.3.0正式发布,新增平面四参数转换、规范法历元转换
  • 【HarmonyOS应用】《账理通》更新啦!
  • 免费1级做爰片打网站租车做什么网站推广
  • 足球网站网站建设咸阳学校网站建设哪家好
  • 工程中标查询网站营销网站 深圳
  • Midb-Manager:轻量级前端数据管理利器,打造专属的.midb数据库
  • 上海网站开发售后服务qq登录网页版登录入口官网
  • 网站标题在哪里凡科网怎么制作小程序
  • linux声卡设置
  • 网站模板库软件前端网站大全
  • 让移动网站重庆百度网站排名
  • 整体设计 逻辑系统程序 之10 三种逻辑表述形式、形式化体系构建及关联规则(正则 / 三区逻辑)之3
  • 织梦模板建站wordpress 图片加链接地址
  • 华为OD机试C卷 - 寻找最大价值矿堆 - DFS - (Java C++ JavaScript Python)
  • 2025:现代硬件限制,系统设计考虑
  • 温州网站外包怎么用网站建设
  • DAY 40 训练和测试的规范写法-2025.10.4
  • 设计一个网站需要多久微信网站开放
  • 网站建设一般多少钱网址wordpress分享到qq空间
  • 外包网站设计公司天猫开店流程及费用标准多少
  • 「机器学习笔记3」机器学习常用评价指标全解析
  • 网站的建设与维护步骤360永久免费建网站
  • 佛山做网站多少钱秦皇岛网站建公司
  • Using per-item Features|使用每项特征
  • 10.Java线程的状态
  • Codeforces Round 1054 B. Unconventional Pairs(2149)
  • 如何做公众号微信杭州百度seo优化
  • 个人网站备案号可以做企业网站吗成都门户网站有哪些