核心区别
一句话先记住:
- string 是"拥有数据"的字符串。
- string_view 是"只看不拥有"的字符串视图。
也就是说:
- string 自己管理字符内存,像"房主"。
- string_view 只是指向一段字符范围,像"租客看房",它不负责这段内存的生死。
1. string 是什么
string 的特点:
- 自己持有字符串内容。
- 可以修改内容。
- 通常会分配和管理内存。
- 生命周期由对象自己控制。
例子:
cpp
#include <iostream>
#include <string>
int main() {
std::string s = "hello";
s += " world";
std::cout << s << std::endl;
}
这里 s 真正保存了 hello world 这段数据。
2. string_view 是什么
string_view 是 C++17 引入的,表示"对一段字符串的只读观察"。
它的特点:
- 不拥有字符串。
- 不负责分配和释放内存。
- 一般只保存两个东西:
- 一个指针
- 一个长度
- 不能直接修改字符内容。
例子:
cpp
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "hello world";
std::cout << sv << std::endl;
}
这里 sv 并没有拷贝 hello world,它只是指向这段字符数据。
3. 最本质的区别
可以用这个表来记:
| 对比项 | string | string_view |
|---|---|---|
| 是否拥有数据 | 是 | 否 |
| 是否管理内存 | 是 | 否 |
| 是否可修改内容 | 是 | 否 |
| 是否可能发生拷贝 | 会 | 通常不会 |
| 适合长期保存吗 | 适合 | 要看原数据是否还活着 |
| 适合做只读参数吗 | 可以 | 更合适 |
4. 一个直观类比
假设有一本书:
- string 像你复印了一整本书,副本归你自己管。
- string_view 像你拿着目录卡,只记录"从第几页到第几页"。
所以:
- string 更安全,因为数据归自己。
- string_view 更轻量,因为不用复制。
5. 用法上的最大区别:生命周期
这部分最重要。
string_view 不拥有数据,所以它依赖原字符串继续存在。
安全例子:
cpp
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string s = "hello";
std::string_view sv = s;
std::cout << sv << std::endl;
}
这里安全,因为 s 还活着,sv 指向的内容有效。
危险例子:
cpp
#include <iostream>
#include <string>
#include <string_view>
std::string_view bad() {
std::string s = "hello";
return s;
}
int main() {
std::string_view sv = bad();
std::cout << sv << std::endl;
}
这里有问题,因为:
- bad 里的 s 是局部变量。
- 函数返回后 s 已经销毁。
- sv 变成悬空视图。
所以 string_view 最大的坑就是:
它自己不保活数据。
6. 什么时候用 string
适合这些场景:
- 你需要拥有这段字符串。
- 你要修改字符串内容。
- 你要长期保存字符串。
- 你要拼接、插入、删除。
- 你不想操心原数据生命周期。
例如:
cpp
#include <string>
#include <vector>
int main() {
std::vector<std::string> names;
std::string name = "alice";
names.push_back(name);
}
这里放进容器里,通常应该存 string,而不是存 string_view。
因为容器需要自己拥有数据,不能赌外部字符串一直有效。
7. 什么时候用 string_view
适合这些场景:
- 函数参数只读,不需要修改。
- 不想为传参产生额外拷贝。
- 只想看某一段字符串。
- 做解析、切片、查找时追求轻量。
最典型的是函数参数:
cpp
#include <iostream>
#include <string>
#include <string_view>
void print(std::string_view sv) {
std::cout << sv << std::endl;
}
int main() {
std::string s = "hello";
print(s);
print("world");
}
这个接口很好,因为它既能接:
- std::string
- 字符串字面量
- std::string_view
而且一般不需要额外拷贝。
这也是现代 C++ 里 string_view 最常见的用法。
8. 作为函数参数时怎么选
这是实战里最有价值的地方。
如果函数只是读取字符串内容,优先考虑 string_view:
void log(std::string_view msg);
优点是:
- 调用方传 string 可以。
- 传字面量也可以。
- 不需要额外构造一个新的 string。
如果函数需要拥有或保存参数,应该用 string:
cpp
class User {
public:
void setName(std::string name) {
name_ = std::move(name);
}
private:
std::string name_;
};
因为这里类成员要长期保存名字,所以必须自己拥有数据。
9. 子串操作的区别
string 的 substr 会构造新字符串,通常会拷贝。
例子:
cpp
#include <iostream>
#include <string>
int main() {
std::string s = "hello world";
std::string sub = s.substr(0, 5);
std::cout << sub << std::endl;
}
这里 sub 是新的字符串对象。
string_view 的 substr 只是生成一个新的视图,一般不拷贝:
cpp
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "hello world";
std::string_view sub = sv.substr(0, 5);
std::cout << sub << std::endl;
}
这里 sub 只是"继续指向原来的那块数据中的前 5 个字符"。
所以做解析、切片时,string_view 很高效。
10. 性能上的区别
一般来说:
- string 复制时可能拷贝字符数据。
- string_view 复制时通常只复制指针和长度,代价很小。
例如:
cpp
#include <string>
#include <string_view>
void f1(std::string s) {
}
void f2(std::string_view sv) {
}
如果你频繁调用只读函数:
- f1 可能产生字符串拷贝。
- f2 通常只是传一个轻量视图。
所以在"只读入参"场景里,string_view 常常更高效。
但要注意:
性能提升的前提是你真的只是读,不是存,不是改,不是跨生命周期保留。
11. string_view 不是 C 风格字符串
这是一个很容易踩坑的点。
string_view 记录的是:
- 指针
- 长度
它不保证末尾一定有 '\0',尤其是看某个子串时更明显。
例如:
cpp
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "hello world";
std::string_view sub = sv.substr(0, 5);
std::cout << sub << std::endl;
}
sub 表示 hello,长度是 5。
但它的 data() 后面不一定正好就是字符串结尾。
所以如果你把 string_view 直接丢给只接受 C 字符串的接口,就可能出问题。
比如这类接口通常要求:
- 数据以 '\0' 结尾
- 并且读到 '\0' 为止
而 string_view 只是"知道长度",不是"保证以 '\0' 截止"。
如果必须传给 C 接口,通常要先转成 string:
cpp
std::string tmp(sub);
c_api(tmp.c_str());
12. string_view 不能修改内容
它是只读视图。
下面这种不行:
cpp
std::string_view sv = "hello";
// sv[0] = 'H'; // 不允许
如果你需要改内容,就该用 string。
13. 返回值怎么选
这个点也很重要。
如果函数返回的是"新构造的数据",通常返回 string:
std::string makeName() {
return "alice";
}
如果函数返回的是"对已有长生命周期数据的视图",可以返回 string_view:
std::string_view getTag() {
return "INFO";
}
这个例子安全,因为字符串字面量的生命周期很长。
但是不要返回指向局部 string 的 string_view:
std::string_view bad() {
std::string s = "abc";
return s;
}
这一定有风险。
所以简单规则是:
- 返回新数据,用 string。
- 返回已有稳定数据的只读观察,用 string_view。
- 只要生命周期不确定,就别轻易返回 string_view。
14. 实战建议
可以直接记这几条:
- 读参数时优先考虑 string_view。
- 需要拥有、修改、保存时用 string。
- 不要把 string_view 存起来,除非你非常确定原数据活得更久。
- 不要随便把 string_view.data() 当成 C 字符串。
- 解析文本、切片、查找时,string_view 很有优势。
15. 一个综合示例
下面这个例子能把两者的典型用法串起来:
cpp
#include <iostream>
#include <string>
#include <string_view>
void printPrefix(std::string_view text) {
std::string_view prefix = text.substr(0, 5);
std::cout << prefix << std::endl;
}
class Person {
public:
void setName(std::string name) {
name_ = std::move(name);
}
void show() const {
std::cout << name_ << std::endl;
}
private:
std::string name_;
};
int main() {
std::string s = "hello world";
printPrefix(s);
printPrefix("abcdefg");
Person p;
p.setName(s);
p.show();
}
这里:
- printPrefix 只是读字符串,所以参数用 string_view。
- Person 要保存名字,所以成员用 string。
- 这是两者最典型的职责分工。
一句话总结
string 解决"我要拥有这段字符串"的问题。
string_view 解决"我只想高效地看这段字符串"的问题。
函数传参时,const string& 和string_view的区别的效率
如果函数只是"读字符串、不保存、不修改",在 C++17 以后一般优先用 std::string_view。
但"效率"要分场景看,不是所有情况下 string_view 都明显更快。
一、两者本质区别
-
const std::string&
表示"引用一个 std::string 对象"。
-
std::string_view
表示"查看一段字符串范围",通常内部就是:
- 一个字符指针
- 一个长度
所以它不要求调用方一定传 std::string,也可以传:
- std::string
- 字符串字面量
- const char*
- 某段子串视图
二、效率对比的关键结论
场景 1:传入的本来就是 std::string
cpp
void f1(const std::string& s);
void f2(std::string_view s);
std::string name = "hello";
f1(name);
f2(name);
这种情况下,两者效率通常非常接近。
原因:
- const std::string& 本质上是传一个引用,成本很低。
- std::string_view 会从 name 构造一个小视图,通常只是取出 data 和 size,成本也很低。
实际工程里,这个差距通常小到可以忽略。
可以粗略理解为:
- const std::string& 是"传一个地址"
- std::string_view 是"传一个指针 + 一个长度"
在 64 位平台上,string_view 往往就是 16 字节左右,也很轻。
所以:
对现成 std::string 来说,两者性能差不多,不要指望这里有数量级差异。
场景 2:传入的是字符串字面量或 const char*
cpp
f1("hello");
f2("hello");
这里 string_view 通常更高效。
原因是:
- 传给 const std::string& 时,往往需要先构造一个临时 std::string
- 这个临时 string 要做长度计算,还可能分配内存并复制字符
- 传给 string_view 时,只需要形成一个视图,不需要复制字符
所以这类调用里,string_view 的优势很明显。
不过有一个细节:
- string_view 从 const char* 构造时,通常也要扫一遍找到结尾的 '\0'
- 但它不分配内存、不复制整段数据
因此总体上仍然通常比构造 std::string 更省。
场景 3:传子串、切片、解析文本
这是 string_view 优势最大的地方之一。
看下面两种写法:
cpp
std::string s = "hello world";
auto a = s.substr(0, 5); // a 是新的 std::string,通常会拷贝
std::string_view b(s.data(), 5); // b 只是个视图,不拷贝
如果你的函数参数是:
cpp
void g(const std::string& s);
那你想传一个子串时,很多时候得先构造一个新的 std::string。
如果你的函数参数是:
cpp
void g(std::string_view s);
那就可以直接传某段视图,通常零拷贝。
所以在"字符串切片、协议解析、日志解析、命令行解析"这类场景里,string_view 往往更高效。
三、为什么很多人说 string_view 更适合做只读参数
因为它的接受范围更广,而且通常避免不必要构造。
例如:
cpp
void print(std::string_view s);
这个接口可以很自然地接收:
- std::string
- "hello"
- const char*
- 某个子串视图
如果你写成:
cpp
void print(const std::string& s);
那么调用方如果传字面量,通常就要走一次临时 string 构造。
所以从"接口通用性 + 避免额外分配"两点看,string_view 常常更适合作为只读入参。
四、const std::string& 什么时候并不差
不要误以为 const std::string& 就"落后"了。
如果你的调用方几乎总是传现成的 std::string,比如:
- 业务代码里早就把数据存在 string 里
- 调用链上所有函数都处理 string
- 你也不需要接收字面量、子串视图、char*
那么 const std::string& 的性能完全可以接受,而且语义也直接。
也就是说:
如果输入几乎总是 std::string,本身没有什么额外构造,const std::string& 和 string_view 在效率上通常没什么本质差距。
五、真正的差异不只在效率,还在语义
这是实战里更重要的一点。
const std::string& 的语义更像:
"我接收的是一个字符串对象。"
std::string_view 的语义更像:
"我接收的是一段只读字符序列。"
所以 string_view 更像"泛化的只读文本参数"。
六、string_view 的效率优势,换来的代价是什么
代价就是:它不拥有数据。
这意味着它更容易踩生命周期问题。
例如:
cpp
std::string_view bad() {
std::string s = "hello";
return s;
}
这是错的,因为 s 已经销毁了,返回的 view 悬空。
但如果参数只是函数调用期间临时使用,一般没问题:
cpp
void print(std::string_view s) {
std::cout << s << '\n';
}
print(std::string("hello"));
这次调用是安全的,因为那个临时 string 会活到整个函数调用结束。
所以要记住:
- string_view 很适合"只读参数"
- 不适合"长期保存,除非你非常确定原数据生命周期"
七、什么时候反而更适合 const std::string&
这几种情况里,const std::string& 往往更稳一些:
- 你的接口明确只接受 std::string
- 你内部需要依赖它是一个真正的 string 对象
- 你后面要把它传给要求以 '\0' 结尾的 C 接口,而且希望语义更直接
- 团队对 string_view 生命周期坑不熟,接口希望更保守
补一个细节:
string_view 的 data() 不等于"这是一个安全的 C 字符串"。
因为 view 只保证"指针 + 长度",不保证这段视图末尾正好是 '\0'。
八、面试或实战可以怎么回答"效率区别"
可以直接这样答:
如果调用方本来就传 std::string,那么 const std::string& 和 std::string_view 的传参成本都很低,性能通常接近;但如果调用方传的是字符串字面量、const char* 或子串,std::string_view 通常更高效,因为它不需要构造临时 std::string,也避免了额外的内存分配和字符拷贝。
所以在只读参数场景下,string_view 的优势主要体现在"减少不必要构造和拷贝",而不是在"引用传参"这一步本身比 const std::string& 快很多。
九、实战建议
可以直接按这个规则用:
- 只读、不保存、不修改:优先 std::string_view
- 需要拥有、保存、修改:用 std::string
- 输入几乎总是 std::string,而且团队更看重稳妥简单:const std::string& 也完全可以
- 做文本解析、切片、协议处理:优先 string_view,收益通常更明显
一句话总结
const std::string& 和 std::string_view 在"传现成 std::string"时效率通常差不多;
string_view 真正的效率优势在于:它能避免把字面量、char*、子串先转成临时 std::string 的那一步。
string_view 常见生命周期坑点示例版
最常见的坑就一句话:
string_view 不拥有数据,所以只要底层字符先死了,或者底层存储位置变了,view 就悬空了。
下面把最容易踩的几类坑直接列出来,每类都给你一个错误示例和正确写法。
1. 返回局部 string 的 string_view
错误:
cpp
#include <string>
#include <string_view>
std::string_view bad() {
std::string s = "hello";
return s;
}
原因:
- s 是局部变量。
- 函数结束后 s 已经析构。
- 返回的 string_view 指向无效内存。
正确做法一:直接返回 string
cpp
std::string good() {
std::string s = "hello";
return s;
}
正确做法二:只返回长生命周期数据的 view
cpp
std::string_view good() {
return "hello";
}
因为字符串字面量生命周期足够长。
2. 从临时 string 构造 string_view
错误:
cpp
#include <string>
#include <string_view>
int main() {
std::string_view sv = std::string("hello");
}
原因:
- 右边这个临时 string 在这一行结束后就销毁。
- sv 立刻变成悬空视图。
类似错误还有:
cpp
std::string_view sv = std::to_string(123);
std::string_view sv = a + b;
因为 to_string 和字符串拼接都会产生临时 string。
正确做法:
cpp
std::string s = std::string("hello");
std::string_view sv = s;
或者如果只是想临时传参,直接传给函数,不要存起来:
cpp
void print(std::string_view sv);
print(std::string("hello"));
这个是安全的,因为临时 string 会活到整个函数调用结束。
关键区别是:
- 直接传参,通常安全
- 保存到变量里长期用,通常危险
3. 对 std::string 调 substr 后再赋给 string_view
这是很经典的坑。
错误:
cpp
#include <string>
#include <string_view>
int main() {
std::string s = "hello world";
std::string_view sv = s.substr(0, 5);
}
很多人以为 sv 指向 s 的前 5 个字符,其实不是。
原因:
- s.substr(0, 5) 返回的是一个新的 std::string 临时对象。
- 这个临时对象在语句结束后销毁。
- sv 悬空。
正确写法一:先有 view,再对 view 做 substr
cpp
std::string s = "hello world";
std::string_view sv = std::string_view(s).substr(0, 5);
正确写法二:
cpp
std::string s = "hello world";
std::string_view sv(s.data(), 5);
前提是你确定长度没问题。
4. 把 string_view 存进容器,但底层 string 已失效
错误:
cpp
#include <string>
#include <string_view>
#include <vector>
int main() {
std::vector<std::string_view> vec;
{
std::string s = "hello";
vec.push_back(s);
}
// 这里 vec[0] 已经悬空
}
原因:
- vec 里存的不是字符串副本。
- 它只是记住了 s 的地址和长度。
- s 离开作用域就没了。
正确做法一:容器里存 string
cpp
std::vector<std::string> vec;
std::string s = "hello";
vec.push_back(s);
正确做法二:只有在你能保证原字符串长期有效时,才存 string_view。
例如原数据来自全局配置、静态字符串池、长生命周期缓存。
5. 原 string 修改后导致 view 失效
错误:
cpp
#include <string>
#include <string_view>
#include <iostream>
int main() {
std::string s = "hello";
std::string_view sv = s;
s += " world";
std::cout << sv << '\n';
}
为什么危险:
- string 扩容后可能重新分配内存。
- sv 还指向旧地址。
- 旧地址可能已经失效。
要注意,不只是拼接,下面这些操作也可能让 view 失效:
- append
- push_back
- reserve
- assign
- 非小范围修改导致重分配
更稳妥的做法:
- 如果后面还要改 string,就不要长期保留它的 string_view。
- 修改完 string 之后,重新生成 view。
例如:
cpp
std::string s = "hello";
s += " world";
std::string_view sv = s;
6. 把 string_view 传到异步任务里
这和回调、线程那类生命周期问题是一样的。
错误:
cpp
#include <string>
#include <string_view>
#include <thread>
#include <chrono>
#include <iostream>
void asyncPrint(std::string_view sv) {
std::thread([sv] {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << sv << '\n';
}).detach();
}
int main() {
std::string s = "hello";
asyncPrint(s);
}
如果 main 很快结束,或者 s 先销毁,线程里的 sv 就悬空。
更安全的做法是在线程里持有 string:
cpp
void asyncPrint(std::string s) {
std::thread([s = std::move(s)] {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << s << '\n';
}).detach();
}
经验规则很简单:
- 同步只读参数,string_view 很适合
- 异步保存、延迟执行,优先自己持有 string
7. 误把 data() 当 C 字符串
错误思路不是语法错,而是语义错。
看这个例子:
cpp
#include <string_view>
#include <cstdio>
int main() {
std::string_view sv = "hello world";
std::string_view sub = sv.substr(0, 5);
std::printf("%s\n", sub.data());
}
你想打印 hello,但这段代码不可靠。
原因:
- string_view 只保证一段字符范围。
- sub.data() 指向的是 h 的位置。
- 但 sub 只"长度为 5",并不保证第 6 个字符就是字符串结束符。
- printf 按 C 字符串规则会一直读到遇到 0 为止。
所以它可能打印成 hello world,而不是 hello。
正确做法:
cpp
std::string tmp(sub);
std::printf("%s\n", tmp.c_str());
或者使用支持长度的接口。
8. 指向容器内部字符数据,容器变化后失效
例如:
cpp
std::string s = "abcdef";
std::string_view sv = s;
s.clear();
此时 sv 也不该再用了。
或者:
s.resize(1000);
如果发生重分配,sv 同样可能失效。
你可以把 string_view 理解成:
它只是借住在别人家里,房东搬家、拆房、清空房间,你这张地址卡就没用了。
9. 一个容易误判但其实安全的场景
这个很多人会误会。
下面是安全的:
cpp
void print(std::string_view sv) {
std::cout << sv << '\n';
}
int main() {
print(std::string("hello"));
}
原因:
- 临时 string 的生命周期会延续到整个函数调用结束。
- print 里面立刻用完 sv,没有保存它。
- 所以没问题。
但如果你在 print 里把 sv 存到全局变量、成员变量、异步任务里,就又危险了。
所以关键不在"参数是不是临时对象",而在"你有没有把 view 留到调用结束之后"。
10. 最实用的判断规则
每次想用 string_view,都先问自己 3 个问题:
- 这段字符是谁拥有的?
- 在我用完之前,这块内存一定还活着吗?
- 底层 string 在这期间会不会被改到重分配?
只要有一个答案不确定,就别长期保存 string_view。
一组实战建议
- 函数只读参数优先用 string_view。
- 需要保存、缓存、异步使用时优先用 string。
- 不要返回指向局部 string 的 string_view。
- 不要把临时 string 的结果存进 string_view 变量里。
- 对原 string 进行可能重分配的修改后,旧 view 一律视为失效。
- 传给 C 接口前,不要默认 string_view.data() 就是安全的 C 字符串。
一句话总结
string_view 最适合"当前这次调用里只读使用";一旦跨作用域、跨线程、跨生命周期保存,它的风险就会迅速升高。