C++ std::string_view 全面讲解
文章目录
- [C++ `std::string_view` 全面讲解](#C++
std::string_view全面讲解) - [二、它和 `std::string` 有什么区别?](#二、它和
std::string有什么区别?) -
- [`std::string` 拥有字符](#
std::string拥有字符) - [`std::string_view` 不拥有字符](#
std::string_view不拥有字符) - 对比表
- [`std::string` 拥有字符](#
- [三、`string_view` 的本质:指针和长度](#三、
string_view的本质:指针和长度) - [四、创建 `string_view` 的各种方式](#四、创建
string_view的各种方式) -
- [1. 从字符串字面量创建](#1. 从字符串字面量创建)
- [2. 从 `std::string` 创建](#2. 从
std::string创建) - [3. 使用指针和长度创建](#3. 使用指针和长度创建)
- [4. 使用 `sv` 字面量](#4. 使用
sv字面量)
- [五、内嵌 `'\0'` 的重要区别](#五、内嵌
'\0'的重要区别) -
- 普通形式
- [`sv` 字面量形式](#
sv字面量形式)
- 六、最重要的问题:生命周期
- 七、原字符串修改后,视图可能失效
- [八、`data()` 不保证以 `'\0'` 结尾](#八、
data()不保证以'\0'结尾) -
- 正确输出方式
- [`string_view` 没有 `c_str()`](#
string_view没有c_str())
- 九、常用成员函数
-
- [1. 长度和判空](#1. 长度和判空)
- [2. 访问字符](#2. 访问字符)
- [3. 取子串](#3. 取子串)
- [4. 查找](#4. 查找)
- [5. 前缀和后缀](#5. 前缀和后缀)
- [6. `contains`](#6.
contains) - [7. 删除视图前缀和后缀](#7. 删除视图前缀和后缀)
- 十、一个实用例子:去除两端空格
- 十一、一个实用例子:解析键值对
- 十二、函数参数应该怎么选择?
-
- [场景 1:函数只读取文本,不保存](#场景 1:函数只读取文本,不保存)
- [场景 2:函数要保存字符串](#场景 2:函数要保存字符串)
- [场景 3:函数要修改调用者的字符串](#场景 3:函数要修改调用者的字符串)
- [场景 4:参数必须以 `'\0'` 结尾](#场景 4:参数必须以
'\0'结尾) - [`string_view` 应按值传递](#
string_view应按值传递)
- [十三、为什么 `const std::string&` 还没有被淘汰?](#十三、为什么
const std::string&还没有被淘汰?) -
- [`string_view` 的优势](#
string_view的优势) - [`const std::string&` 的优势](#
const std::string&的优势)
- [`string_view` 的优势](#
- [十四、不要轻易把 `string_view` 作为成员变量](#十四、不要轻易把
string_view作为成员变量) - [十五、不要把不稳定的 `string_view` 当哈希表键](#十五、不要把不稳定的
string_view当哈希表键) - [十六、空 `string_view` 的细节](#十六、空
string_view的细节) - 十七、不要向它传入空指针
- [十八、转换回 `std::string`](#十八、转换回
std::string) - 十九、不能直接修改字符
- 二十、没有自然的字符串拼接能力
- 二十一、性能分析
- [二十二、`string_view` 不理解 Unicode 字符](#二十二、
string_view不理解 Unicode 字符) - 二十三、版本相关功能
- 二十四、常见错误总结
-
- 错误:保存临时字符串的视图
- 错误:返回局部字符串的视图
- [错误:把 `data()` 当作 C 字符串](#错误:把
data()当作 C 字符串) - 错误:原字符串变化后继续使用视图
- [错误:把 view 当作拥有者](#错误:把 view 当作拥有者)
- [错误:通过 `data()` 判断空](#错误:通过
data()判断空)
- 二十五、工程中的选择口诀
-
-
- 只读并且只在函数调用期间使用
- 要修改调用者的字符串
- 要取得所有权或长期保存
- [要与要求 `'\0'` 的 C API 交互](#要与要求
'\0'的 C API 交互) - [返回 `string_view`](#返回
string_view)
-
- 最核心的理解
std::string_view 可以概括为:
一个不拥有字符串内容、只保存"字符地址 + 字符数量"的只读字符串窗口。
它在 C++17 中加入标准库,定义在 <string_view> 中。标准将它描述为"对连续只读字符序列的引用";典型实现内部只需要一个指针和一个长度。(Cppreference)
cpp
#include <string_view>
std::string_view sv = "hello";
可以把它想象成:
text
底层字符:
[ h ][ e ][ l ][ l ][ o ][ \0 ]
^
|
sv.data()
sv.size() == 5
它只是观察这些字符:
- 不负责分配内存;
- 不负责释放内存;
- 通常不复制字符;
- 不能通过
string_view修改字符; - 必须保证底层字符活得比
string_view久。
最后一点------生命周期 ------是 string_view 最重要、也最容易出错的地方。
一、最基本的使用
cpp
#include <iostream>
#include <string>
#include <string_view>
void print(std::string_view text)
{
std::cout << text << '\n';
}
int main()
{
std::string s = "hello";
print(s); // std::string
print("world"); // 字符串字面量
print(std::string_view{"C++"});
}
std::string 和 const char* 都可以隐式转换为 std::string_view,所以一个接收 string_view 的函数可以同时处理字符串对象、字符串字面量和字符指针。标准也明确把这种统一字符串参数的能力作为其设计目的之一。(Eel.is)
二、它和 std::string 有什么区别?
std::string 拥有字符
cpp
std::string s = "hello";
大致可以理解为:
text
std::string
|
+-- 拥有一块内存
[h][e][l][l][o][\0]
std::string 负责:
- 保存字符;
- 分配和释放内存;
- 扩容;
- 修改内容;
- 管理生命周期。
std::string_view 不拥有字符
cpp
std::string s = "hello";
std::string_view sv = s;
大致是:
text
std::string s
|
+-- [h][e][l][l][o][\0]
^
|
string_view sv
pointer + length
sv 只指向 s 的字符。它不会延长 s 的生命周期,也不会保存一份副本。
对比表
| 特性 | std::string |
std::string_view |
|---|---|---|
| 是否拥有字符 | 是 | 否 |
| 是否通常分配内存 | 可能 | 否 |
| 是否复制字符 | 可能 | 通常不复制 |
| 能否修改字符 | 可以 | 不能通过 view 修改 |
| 能否保存结果 | 可以 | 要保证底层生命周期 |
| 子串操作 | 通常复制字符 | 通常只调整指针和长度 |
是否保证结尾有 \0 |
c_str() 保证 |
不保证 |
| 适合做只读参数 | 可以 | 通常更灵活 |
| 适合长期存储 | 是 | 需要非常谨慎 |
三、string_view 的本质:指针和长度
可以用下面这个简化结构理解它:
cpp
class simplified_string_view
{
const char* data_;
std::size_t size_;
};
这不是标准要求的实际实现代码,但标准的说明性定义确实表现为一个 const charT* 和一个 size_type。因此复制一个 string_view,通常只是复制地址和长度,而不是复制整个字符串。(Eel.is)
例如:
cpp
std::string_view sv = "hello world";
std::string_view sub = sv.substr(6, 5);
内存大致是:
text
[h][e][l][l][o][ ][w][o][r][l][d][\0]
^ ^
| |
sv.data() sub.data()
sv.size() == 11
sub.size() == 5
sub 并没有创建一个新的 "world" 字符串,只是指向了原字符序列中间的位置。substr() 返回视图,复杂度为常数级。(Cppreference)
四、创建 string_view 的各种方式
1. 从字符串字面量创建
cpp
std::string_view sv = "hello";
这里使用接收 const char* 的构造函数。这个构造函数需要查找结尾的 '\0',因此构造复杂度是 O(n)。(Eel.is)
2. 从 std::string 创建
cpp
std::string s = "hello";
std::string_view sv = s;
此时 sv 指向 s 的内部字符。
cpp
std::cout << sv; // hello
但是必须保证 s 仍然存在,而且没有执行可能使内部地址失效的操作。
3. 使用指针和长度创建
cpp
const char buffer[] = {'a', 'b', '\0', 'c', 'd'};
std::string_view sv(buffer, 5);
此时:
cpp
sv.size() == 5;
string_view 可以包含中间的 '\0',因为它依靠长度,而不是依靠空字符判断结束。
指针加长度的构造不需要扫描字符串,只记录指针和长度。(Eel.is)
4. 使用 sv 字面量
cpp
#include <string_view>
using namespace std::string_view_literals;
std::string_view sv = "hello"sv;
也可以使用:
cpp
using namespace std::literals;
auto sv = "hello"sv;
"..."sv 会直接获得编译器提供的字符数量,特别适合处理内嵌 '\0'。标准库为不同字符类型提供了对应的 sv 后缀。(Eel.is)
五、内嵌 '\0' 的重要区别
来看这个例子:
cpp
using namespace std::string_view_literals;
std::string_view a = "ab\0cd";
std::string_view b = "ab\0cd"sv;
结果是:
cpp
a.size() == 2;
b.size() == 5;
为什么?
普通形式
cpp
std::string_view a = "ab\0cd";
调用的是:
cpp
string_view(const char* str);
它使用类似 char_traits<char>::length(str) 的逻辑,在第一个 '\0' 处停止。标准规定这个构造函数会计算空字符终止字符串的长度。(Eel.is)
sv 字面量形式
cpp
std::string_view b = "ab\0cd"sv;
字面量运算符同时收到:
cpp
const char* pointer;
std::size_t length;
因此能保留完整长度。
处理二进制数据或可能包含 '\0' 的文本时,应该使用显式长度。
六、最重要的问题:生命周期
string_view 不管理内存,所以它可能变成悬空视图,也就是指向已经失效的内存。
错误 1:指向临时 std::string
cpp
std::string_view sv = std::string("hello");
// 临时 std::string 已经销毁
std::cout << sv; // 未定义行为
执行完初始化语句后,临时 std::string 被销毁,而 sv 仍然保存着原来的地址。
这和下面的情况类似:
cpp
const char* p = std::string("hello").data();
指针不会自动延长原对象的生命周期。
正确方式
需要长期保存时,让拥有者活得足够久:
cpp
std::string owner = "hello";
std::string_view sv = owner;
std::cout << sv; // 正确
或者直接保存一份字符串:
cpp
std::string result = std::string("hello");
错误 2:函数返回指向局部字符串的视图
cpp
std::string_view make_name()
{
std::string name = "Alice";
return name; // 错误
}
函数结束时 name 被销毁,返回的视图立即悬空。
正确方式是返回拥有数据的 std::string:
cpp
std::string make_name()
{
std::string name = "Alice";
return name;
}
现代 C++ 的返回值优化和移动语义通常会让这种写法非常高效。
正确返回 string_view 的情况
返回字符串字面量
cpp
std::string_view get_name()
{
return "Alice";
}
字符串字面量具有静态存储期,程序结束前一直有效,所以这是安全的。
返回参数的一部分
cpp
std::string_view get_extension(std::string_view filename)
{
auto pos = filename.rfind('.');
if (pos == std::string_view::npos)
return {};
return filename.substr(pos + 1);
}
调用:
cpp
std::string file = "report.pdf";
std::string_view ext = get_extension(file);
只要 file 仍然有效,ext 就有效。
但下面仍然危险:
cpp
std::string_view ext = get_extension(std::string("report.pdf"));
临时字符串在该语句结束后销毁,ext 会悬空。
七、原字符串修改后,视图可能失效
cpp
std::string s = "hello";
std::string_view sv = s;
s += " a very long suffix";
std::cout << sv; // 可能未定义行为
std::string 扩容时可能重新分配内存。重新分配后,sv 仍指向旧地址。
标准规定,多种非 const 字符串操作可能使指向 std::string 元素的指针、引用和迭代器失效;string_view 本质上保存的也是这样的地址。(Eel.is)
需要特别警惕:
cpp
s.append(...);
s += ...;
s.push_back(...);
s.insert(...);
s.erase(...);
s.replace(...);
s.resize(...);
s.reserve(...);
s.clear();
s = ...;
稳妥的工程规则是:
从
std::string创建视图以后,在视图使用完之前,不要对原字符串进行可能改变其内容、大小或存储位置的操作。
即使操作没有重新分配内存,也可能改变视图所观察的字符:
cpp
std::string s = "hello";
std::string_view sv = s;
s[0] = 'H';
std::cout << sv; // Hello
string_view 是"通过自身只读",并不意味着底层数据永远不会变化。
八、data() 不保证以 '\0' 结尾
这是另一个非常大的坑。
cpp
std::string_view sv = "hello world";
std::string_view sub = sv.substr(0, 5);
sub 表示 "hello",但它的底层内存实际可能是:
text
[h][e][l][l][o][ ][w][o][r][l][d][\0]
^ ^
data() data() + size()
sub.data() 指向 h,但第五个字符之后是空格,不是 '\0'。
因此:
cpp
printf("%s\n", sub.data());
可能输出:
text
hello world
而不是:
text
hello
甚至可能读取越界。标准只保证 [data(), data() + size()) 是有效范围,不保证 data() + size() 所在位置存放空字符。(Cppreference)
正确输出方式
C++ 流可以识别长度:
cpp
std::cout << sub;
写入文件或二进制流时:
cpp
std::fwrite(sub.data(), 1, sub.size(), stdout);
必须调用要求 C 风格空结尾字符串的接口时,可以创建拥有的字符串:
cpp
std::string temp(sub);
some_c_api(temp.c_str());
string_view 没有 c_str()
这是有意的:一个视图并不保证末尾有 '\0',所以不能安全地提供 c_str()。
九、常用成员函数
1. 长度和判空
cpp
std::string_view sv = "hello";
sv.size(); // 5
sv.length(); // 5
sv.empty(); // false
size() 和 length() 含义相同。
2. 访问字符
cpp
char a = sv[0];
char b = sv.at(1);
char c = sv.front();
char d = sv.back();
区别:
cpp
sv[index]
要求下标合法,不检查越界。
cpp
sv.at(index)
会检查越界,越界时抛出 std::out_of_range。(Cppreference)
注意空视图不能调用:
cpp
sv.front();
sv.back();
3. 取子串
cpp
std::string_view sv = "hello world";
auto first = sv.substr(0, 5); // hello
auto last = sv.substr(6); // world
substr() 返回的仍然是 string_view,不会复制字符,复杂度为常数级。(Cppreference)
如果:
cpp
pos > sv.size()
则抛出 std::out_of_range。
如果长度过大:
cpp
sv.substr(6, 1000);
不会越界,而是截取到原视图末尾。
4. 查找
cpp
std::string_view sv = "hello world";
auto pos1 = sv.find("world"); // 6
auto pos2 = sv.find('o'); // 4
auto pos3 = sv.rfind('o'); // 7
没找到时返回:
cpp
std::string_view::npos
常见写法:
cpp
if (sv.find("world") != std::string_view::npos) {
// 找到了
}
还有:
cpp
find_first_of()
find_last_of()
find_first_not_of()
find_last_not_of()
标准对这些搜索操作给出的最坏复杂度上界通常是 O(size × pattern_size),同时鼓励实现采用更好的算法。(Eel.is)
5. 前缀和后缀
C++20:
cpp
std::string_view path = "/usr/bin";
path.starts_with("/"); // true
path.ends_with("bin"); // true
starts_with() 和 ends_with() 从 C++20 开始提供。(Cppreference)
6. contains
C++23:
cpp
std::string_view text = "hello world";
text.contains("world"); // true
text.contains('x'); // false
contains() 本质上相当于:
cpp
text.find(target) != std::string_view::npos;
它从 C++23 开始提供。(Cppreference)
7. 删除视图前缀和后缀
cpp
std::string_view sv = " hello ";
sv.remove_prefix(2); // "hello "
sv.remove_suffix(2); // "hello"
它们不会删除底层字符,只会调整视图的起始地址或长度。
大致相当于:
cpp
// remove_prefix(n)
data_ += n;
size_ -= n;
// remove_suffix(n)
size_ -= n;
要求 n <= size();超过当前长度会违反前置条件。(Cppreference)
十、一个实用例子:去除两端空格
cpp
#include <string_view>
std::string_view trim(std::string_view text)
{
while (!text.empty() && text.front() == ' ') {
text.remove_prefix(1);
}
while (!text.empty() && text.back() == ' ') {
text.remove_suffix(1);
}
return text;
}
使用:
cpp
#include <iostream>
#include <string>
int main()
{
std::string input = " hello world ";
std::string_view result = trim(input);
std::cout << '[' << result << "]\n";
}
输出:
text
[hello world]
这个操作:
- 不修改
input; - 不创建新的字符缓冲区;
- 不复制
"hello world"; - 只是改变视图的起点和长度。
十一、一个实用例子:解析键值对
输入:
text
name=Alice
解析函数:
cpp
#include <optional>
#include <string_view>
#include <utility>
using KeyValue =
std::pair<std::string_view, std::string_view>;
std::optional<KeyValue> parse_key_value(std::string_view text)
{
const auto pos = text.find('=');
if (pos == std::string_view::npos) {
return std::nullopt;
}
std::string_view key = text.substr(0, pos);
std::string_view value = text.substr(pos + 1);
return KeyValue{key, value};
}
使用:
cpp
#include <iostream>
#include <string>
int main()
{
std::string line = "name=Alice";
auto result = parse_key_value(line);
if (result) {
std::cout << "key: " << result->first << '\n';
std::cout << "value: " << result->second << '\n';
}
}
这里没有复制 "name" 和 "Alice";两个结果视图都指向 line 内部。因此 line 必须比结果活得更久。
十二、函数参数应该怎么选择?
场景 1:函数只读取文本,不保存
优先考虑:
cpp
void log(std::string_view message);
它可以接受:
cpp
log("hello");
std::string s = "world";
log(s);
std::string_view sv = "C++";
log(sv);
这种接口很灵活,且调用时通常不需要创建新的字符串。
场景 2:函数要保存字符串
应该接收或构造 std::string:
cpp
class User
{
public:
explicit User(std::string name)
: name_(std::move(name))
{
}
private:
std::string name_;
};
因为对象需要拥有名字。
也可以接收视图后复制:
cpp
class User
{
public:
explicit User(std::string_view name)
: name_(name)
{
}
private:
std::string name_;
};
但这意味着无论调用者传左值还是右值,构造时都会复制字符。
按值接收 std::string 往往更能利用移动:
cpp
User(std::string name)
: name_(std::move(name))
{
}
场景 3:函数要修改调用者的字符串
使用:
cpp
void normalize(std::string& text);
而不是 string_view。
场景 4:参数必须以 '\0' 结尾
不要仅接收 string_view,因为它不提供这个保证。
可以接收:
cpp
void call_c_api(const std::string& text);
然后:
cpp
some_c_api(text.c_str());
或者让接口明确接收拥有的字符串。
string_view 应按值传递
通常写:
cpp
void process(std::string_view text);
而不是:
cpp
void process(const std::string_view& text);
string_view 本身是轻量视图,按值传递还能让函数自由使用:
cpp
text.remove_prefix(...);
而不会修改调用者持有的那个视图对象。
注意,这里修改的只是函数内部视图副本,并不是底层字符串。
十三、为什么 const std::string& 还没有被淘汰?
下面两个接口都可能合理:
cpp
void f(const std::string& text);
void g(std::string_view text);
string_view 的优势
cpp
g("hello");
无需为了参数创建 std::string。
它还可以自然表示:
cpp
g(std::string_view(buffer, length));
包括不是空字符结尾的字符范围。
const std::string& 的优势
它明确要求调用者提供一个 std::string,因此函数内部可以使用:
cpp
text.c_str();
text.capacity();
text.get_allocator();
并且知道字符串对象提供标准 std::string 的完整语义。
所以并不是看到所有 const std::string& 都应该机械替换成 string_view。只有当函数真正只需要"一个只读字符序列"时,string_view 才更符合抽象。
十四、不要轻易把 string_view 作为成员变量
危险设计:
cpp
class User
{
public:
explicit User(std::string_view name)
: name_(name)
{
}
private:
std::string_view name_;
};
调用:
cpp
User user(std::string("Alice"));
构造完成后临时字符串销毁,name_ 立即悬空。
安全设计:
cpp
class User
{
public:
explicit User(std::string name)
: name_(std::move(name))
{
}
private:
std::string name_;
};
只有在生命周期关系非常明确时,才适合存储视图,例如视图始终指向对象内另一个稳定成员:
cpp
class ParsedLine
{
public:
explicit ParsedLine(std::string text)
: storage_(std::move(text))
{
auto pos = storage_.find('=');
if (pos != std::string::npos) {
key_ = std::string_view(storage_).substr(0, pos);
value_ = std::string_view(storage_).substr(pos + 1);
}
}
private:
std::string storage_;
std::string_view key_;
std::string_view value_;
};
但这种类还要谨慎处理复制和移动:对象移动后,视图是否仍指向正确位置可能取决于字符串存储是否改变。工程上经常使用"保存偏移量"或自定义复制、移动逻辑来避免问题。
十五、不要把不稳定的 string_view 当哈希表键
下面的类型本身可以编译:
cpp
std::unordered_map<std::string_view, int> table;
标准库提供了 string_view 的哈希支持。(Eel.is)
但是这样使用很危险:
cpp
std::string key = "hello";
table[key] = 42;
key = "something else";
问题有两个:
- 修改或重新分配可能让视图悬空;
- 即使地址没失效,字符内容发生变化也会改变键的哈希值。
哈希容器要求键在存储期间保持稳定。字符发生变化后,元素可能位于与新哈希值不匹配的桶中,破坏容器查找逻辑。
长期保存键通常应该使用:
cpp
std::unordered_map<std::string, int>
十六、空 string_view 的细节
cpp
std::string_view a;
std::string_view b = "";
两者都满足:
cpp
a.empty();
b.empty();
但:
cpp
a.data() == nullptr; // 默认构造的当前标准语义
而 b.data() 通常指向字符串字面量中的终止字符,不一定是空指针。默认构造视图的标准后置条件是长度为 0、内部地址为空。(Eel.is)
因此判断空视图应该使用:
cpp
sv.empty()
不要使用:
cpp
sv.data() == nullptr
"长度为零"和"指针为空"不是同一个概念。
十七、不要向它传入空指针
错误:
cpp
const char* p = nullptr;
std::string_view sv(p);
接收 const char* 的构造函数需要对指针计算字符串长度,因此要求它指向有效的空字符结尾字符序列。当前标准草案还显式删除了 nullptr_t 构造形式。(Eel.is)
需要表示"没有内容"时使用:
cpp
std::string_view sv{};
需要区分"没有值"和"存在但为空"时,可以用:
cpp
std::optional<std::string_view>
例如:
cpp
std::optional<std::string_view> find_value(...);
十八、转换回 std::string
当需要拥有字符时:
cpp
std::string_view sv = "hello";
std::string s(sv);
这会复制 sv.size() 个字符。
也可以:
cpp
std::string s{sv};
转换后:
cpp
s
拥有独立存储,不再依赖 sv 原本观察的对象。
十九、不能直接修改字符
cpp
std::string_view sv = "hello";
sv[0] = 'H'; // 编译错误
因为字符访问返回只读引用。标准中 iterator 与 const_iterator 也是同一种只读迭代器类型。(Eel.is)
但是可以修改视图范围:
cpp
sv.remove_prefix(1);
sv.remove_suffix(1);
sv = sv.substr(1, 2);
这只是在修改:
text
从哪里开始看
看多少字符
而不是修改字符本身。
二十、没有自然的字符串拼接能力
下面通常不能表示你想要的结果:
cpp
std::string_view a = "hello";
std::string_view b = "world";
// auto c = a + b; // 没有独立拥有拼接结果的 string_view
原因是拼接后的字符必须存储在某个地方,而 string_view 不负责存储。
正确做法:
cpp
std::string result;
result.reserve(a.size() + b.size());
result.append(a);
result.append(b);
或者:
cpp
std::string result(a);
result += b;
较新的标准工作草案改善了 std::string 与 string_view 的混合拼接接口,但核心原则不变:拼接结果需要由 std::string 一类拥有对象保存。 (Eel.is)
二十一、性能分析
string_view 并不是"所有字符串操作都会自动变快",它主要优化的是:
- 参数传递;
- 子串表示;
- 避免不必要的分配;
- 避免不必要的字符复制。
常见复杂度
| 操作 | 典型复杂度 |
|---|---|
| 默认构造 | O(1) |
| 复制视图 | O(1) |
| 从指针和长度构造 | O(1) |
从 const char* 构造 |
O(n) |
size()、empty()、data() |
O(1) |
remove_prefix() |
O(1) |
remove_suffix() |
O(1) |
substr() |
O(1) |
| 比较两个字符串 | 与比较字符数量有关 |
find() |
与文本和模式长度有关 |
从 C 字符串构造之所以不是 O(1),是因为必须查找结尾 '\0';标准明确规定了该构造函数的线性复杂度。(Eel.is)
例如下面的代码可能反复扫描:
cpp
void process(std::string_view);
const char* p = get_c_string();
for (int i = 0; i < 1000; ++i) {
process(p); // 每次构造 view 都可能重新计算 strlen
}
可以改为:
cpp
std::string_view view(p);
for (int i = 0; i < 1000; ++i) {
process(view);
}
二十二、string_view 不理解 Unicode 字符
cpp
std::string_view text = "你好";
在 UTF-8 环境中:
cpp
text.size()
返回的是 char 代码单元数量,通常也就是 UTF-8 字节数,不是人眼看到的汉字数量。
string_view 只知道它观察的是一个连续的 char、char8_t、char16_t、char32_t 或 wchar_t 序列;它不负责 Unicode 解码、字素簇计算或规范化。标准为这些字符类型提供了对应的 string_view 别名。(Eel.is)
例如:
cpp
std::u8string_view utf8;
std::u16string_view utf16;
std::u32string_view utf32;
std::wstring_view wide;
即使使用 u8string_view,它表示的仍然是 UTF-8 代码单元视图,不等于"Unicode 字符视图"。
二十三、版本相关功能
| 标准版本 | 相关功能 |
|---|---|
| C++17 | std::string_view、substr、查找、比较、remove_prefix 等 |
| C++20 | starts_with、ends_with,以及更完整的字符类型支持 |
| C++23 | contains |
| 更新标准草案 | 继续补充与 string、范围及子视图相关的接口 |
基础类型和主要 API 可以从 C++17 开始使用;contains() 等接口要根据项目使用的语言标准选择。(Cppreference)
编译时例如:
bash
g++ -std=c++17 main.cpp
或者:
bash
g++ -std=c++20 main.cpp
g++ -std=c++23 main.cpp
二十四、常见错误总结
错误:保存临时字符串的视图
cpp
std::string_view sv = std::string("hello");
应改为:
cpp
std::string s = "hello";
std::string_view sv = s;
或者直接拥有内容:
cpp
std::string s = "hello";
错误:返回局部字符串的视图
cpp
std::string_view f()
{
std::string s = "hello";
return s;
}
应返回:
cpp
std::string f()
{
return "hello";
}
错误:把 data() 当作 C 字符串
cpp
printf("%s", sv.data());
应使用长度感知接口,或先复制成 std::string。
错误:原字符串变化后继续使用视图
cpp
std::string s = "hello";
std::string_view sv = s;
s += " world";
use(sv); // 可能失效
应在修改前用完视图,或重新创建视图。
错误:把 view 当作拥有者
cpp
struct Config
{
std::string_view name;
};
除非生命周期契约非常清楚,否则应该保存:
cpp
struct Config
{
std::string name;
};
错误:通过 data() 判断空
cpp
if (sv.data() == nullptr) {
}
应使用:
cpp
if (sv.empty()) {
}
二十五、工程中的选择口诀
可以记住下面几条:
只读并且只在函数调用期间使用
cpp
void process(std::string_view text);
要修改调用者的字符串
cpp
void process(std::string& text);
要取得所有权或长期保存
cpp
void process(std::string text);
或者成员中保存:
cpp
std::string text_;
要与要求 '\0' 的 C API 交互
使用:
cpp
std::string
并调用:
cpp
c_str()
返回 string_view
只有在能明确保证:
text
返回视图所指向的数据,比返回值活得更久
时才这样做。
最核心的理解
std::string_view 的优势并不是"它是一种更快的字符串",而是:
它把字符串内容的所有权与字符串内容的观察分开了。
它特别适合:
- 只读函数参数;
- 零复制解析;
- 子串切分;
- 日志和格式处理;
- 配置解析;
- 协议解析;
- 编译期字符串操作。
它最大的风险则是:
视图还活着,但它所指向的字符已经不存在或已经改变。
只要始终问自己一句话,绝大多数问题都能避免:
这个
string_view指向谁?那个对象还能活多久?