Chapter 45: String View
std::string_view 解决的是字符串接口中的一个具体工程问题:调用方经常已经拥有一段字符存储,函数只是读取其中一段内容,此时再构造 std::string 会引入分配、拷贝和所有权误导。本章建立的判断能力是:看到一个字符串参数、切片结果或解析中间态时,能够判断它该由 std::string、std::string_view 还是 const char* 表达,并能追踪这个选择对生命周期、终止符、分配和返回值的影响。
贯穿材料使用一个 HTTP 请求行解析场景。输入缓冲区由网络层或上层字符串对象持有,解析层只需要读取方法、路径和版本三个片段。std::string_view 可以把这些片段表达成指针加长度的视图,使解析过程围绕“谁拥有存储、视图覆盖哪一段、存储何时失效”展开。
C++ 工作草案把 basic_string_view 描述为可以引用一段常量连续字符序列的对象,并在说明性成员中展示了 data_ 与 size_ 两个状态字段;这给本章提供了事实基线:C++ working draft, string.view。后续所有判断都围绕这两个状态展开:data() 给出起点,size() 给出长度,所有权仍留在外部对象手里。
下面的简化代码是本章贯穿材料。它不申请新字符串,只把请求行拆成三个视图:
#include <string_view>
struct RequestLineView {
std::string_view method;
std::string_view target;
std::string_view version;
};
RequestLineView parse_request_line(std::string_view line) {
const auto first_space = line.find(' ');
const auto second_space = line.find(' ', first_space + 1);
return {
line.substr(0, first_space),
line.substr(first_space + 1, second_space - first_space - 1),
line.substr(second_space + 1)
};
}
这段代码的关键点不在 find 或 substr 的 API 用法,而在返回对象的状态关系:三个成员都指向 line 所覆盖的底层字符存储。调用方让底层存储继续有效,三个视图才继续有效;调用方释放、修改或重分配底层存储,视图就失去可靠含义。
45.1 string_view 设计目标
std::string_view 的设计目标是把“读取字符串内容”和“拥有字符串存储”拆开。std::string 表达拥有一段字符序列,拥有者负责分配、释放、扩容和字符生命周期;std::string_view 表达观察一段已有字符序列,视图对象本身只保存起点和长度。这个拆分让只读接口可以统一接收字符串字面量、std::string、字符数组和切片结果。
从接口现象看,很多函数的真实需求只是读取字符:匹配前缀、解析 token、查找分隔符、打印日志字段、把字段传给状态机。把这些函数写成接收 const std::string& 会让调用方把字符串字面量或字符数组临时包装成 std::string;把它写成接收 const char* 又会丢失长度信息,并把接口绑定到 null terminator 约定。std::string_view 给出的接口信号更准确:函数读取一段字符范围,函数不接管存储。
贯穿材料中的 parse_request_line 就是这个设计目标的直接应用。请求行缓冲区可能来自 std::string,也可能来自 char[],解析函数只需要扫描字符并返回片段位置。参数使用 std::string_view 后,函数不再关心输入的拥有者类型;返回值中的 method、target、version 也都沿用同一段底层存储。
这个设计还改变了性能判断。传入 std::string_view 通常只是复制两个标量状态;传入 std::string 可能触发构造、分配和字符拷贝。复制 std::string_view 本身成本很低,标准说明中也把 basic_string_view 作为可平凡复制的类型描述。这个结论只覆盖视图对象自身,不覆盖它指向的字符访问成本;搜索、比较和解析仍然要读取字符区间。
下面这个例子展示了参数入口如何统一多种来源:
#include <string>
#include <string_view>
bool is_get(std::string_view method) {
return method == "GET";
}
void demo() {
std::string owned = "GET";
char buffer[] = {'G', 'E', 'T'};
bool a = is_get("GET");
bool b = is_get(owned);
bool c = is_get(std::string_view{buffer, 3});
(void)a;
(void)b;
(void)c;
}
is_get 的参数没有承诺保存输入,也没有承诺返回输入的一部分,所以 std::string_view 是合适入口。第三个调用还说明了一个重要事实:视图可以覆盖没有终止符的字符数组,只要调用方明确给出长度。
源码层面的常见实现形状也符合这个目标。标准库实现通常把 basic_string_view 做成类似 const charT* 加 size_t 的小对象,成员函数围绕这两个状态计算 begin、end、substr、compare 和 find。不同实现的内部名字、布局顺序和调试包装可以不同,但工程判断无需依赖这些细节;只要坚持“视图复制的是位置和长度,字符存储由外部对象持有”,绝大多数接口选择都能落到稳定结论。
45.2 non-owning string reference
non-owning string reference 指的是:对象引用一段字符存储,却不拥有这段存储,也不负责延长它的生命周期。这里的 reference 是工程语义,不等同于 C++ 语言里的引用类型。std::string_view 本身是一个普通对象,可以复制、赋值、作为成员保存;它保存的只是对外部字符区间的观察位置。
这个语义让生命周期责任变得集中:拥有者负责让字符存储存在,视图使用者负责在存储有效期间访问它。把这个关系画成状态图更清楚:
图中的关键路径是 Owner → View → Algorithm。只要拥有者存储稳定,视图就能把字符区间交给解析或匹配逻辑。Owner → Invalid 这条路径说明另一面:视图没有析构责任,也没有共享所有权计数;拥有者发生释放或重分配后,视图中的指针值可能仍然保留旧地址,但这个地址已经无法作为有效字符区间使用。
贯穿材料中的 RequestLineView 最适合临时结果传递。调用方在同一个作用域内持有请求行字符串,然后马上读取解析出来的片段,这个使用方式清晰:
#include <iostream>
#include <string>
#include <string_view>
void handle_line(const std::string& raw_line) {
RequestLineView view = parse_request_line(raw_line);
std::cout << "method=" << view.method
<< " target=" << view.target
<< " version=" << view.version << '\n';
}
raw_line 在 view 使用期间保持有效,三个成员都可以安全读取。注意这里的安全来自调用结构:raw_line 是调用方持有的拥有者对象,RequestLineView 的使用被限制在拥有者生命周期内部。std::string_view 没有记录这种关系,编译器通常也无法替你验证这种调用结构。
把视图放进长期对象时,语义就发生变化。下面的类型把 std::string_view 保存为成员,它隐含要求外部配置字符串比 RouterRule 活得更久:
#include <string_view>
struct RouterRule {
std::string_view prefix;
};
RouterRule make_rule(std::string_view prefix) {
return RouterRule{prefix};
}
make_rule 本身没有错误;风险来自调用点。如果调用方传入字符串字面量,prefix 指向静态存储,规则对象可以长期保存。如果调用方传入临时 std::string,规则对象保存的就是临时对象中的字符地址,函数返回后该地址失效。因此保存 std::string_view 成员时,需要把“外部存储拥有者是谁、它的生命周期覆盖到哪里”写入类型设计或调用约定。
工程上可以用一个判断顺序处理 non-owning 语义。先看视图来源:字面量、全局表、调用方字符串、局部临时、容器元素。再看视图去向:只在当前表达式读取、在当前函数内传递、作为返回值、作为成员保存、跨线程传递。最后看拥有者是否会释放、移动、扩容或重分配。视图来源稳定、去向短、拥有者无重分配,std::string_view 的语义就清晰;去向长期或拥有者不稳定,接口应改成复制 std::string 或显式保存拥有者。
45.3 data / size
data() 和 size() 是 std::string_view 的状态核心。data() 返回视图起点,size() 返回视图覆盖的字符数量;begin() 可以理解为起点迭代器,end() 可以理解为起点加长度。C++ 工作草案对 data() 的说明还明确了一个边界:它返回的缓冲区可以没有 null terminator,因此把它直接交给只接收 const char* 且按终止符扫描的 C 接口,通常会产生错误读取:C++ working draft, string.view access。
这个状态模型比 null terminator 更适合切片。请求行 GET /index.html HTTP/1.1 中的 target 片段是原缓冲区中间的一段,它后面紧跟一个空格。target.data() 指向 /,target.size() 是路径长度;target.data() 后面的第一个非路径字符可能是空格,通常没有终止符。视图的可视范围由长度约束,不由内存中某个 \0 决定。
下面的例子专门展示 data() 与 size() 的边界。字符数组中间含有 \0,视图仍然可以覆盖完整 7 个字符:
#include <iostream>
#include <string_view>
void print_bytes(std::string_view bytes) {
std::cout << "size=" << bytes.size() << '\n';
for (unsigned char ch : bytes) {
std::cout << static_cast<int>(ch) << ' ';
}
std::cout << '\n';
}
void demo_binary_like_text() {
const char payload[] = {'a', 'b', 'c', '\0', 'd', 'e', 'f'};
std::string_view view{payload, sizeof(payload)};
print_bytes(view);
}
print_bytes 按 size() 遍历,所以中间的 \0 只是一个普通字符值。这个例子说明 std::string_view 可以表达“带长度的字符序列”,适合解析协议字段、文件片段、日志切片和含有零字节的文本状数据。它不让字符序列自动获得字符串拥有权,也不让 C 风格函数自动知道长度。
substr 与 remove_prefix 也围绕 data / size 修改视图状态。substr 返回新视图,通常保持原视图不变;remove_prefix 直接推进当前视图起点并减少长度;remove_suffix 只缩短长度。它们都不移动底层字符,也不销毁字符对象。用贯穿材料改写一个逐步解析函数,可以看到状态变化:
#include <string_view>
std::string_view take_token(std::string_view& input) {
const auto pos = input.find(' ');
std::string_view token = input.substr(0, pos);
if (pos == std::string_view::npos) {
input.remove_prefix(input.size());
} else {
input.remove_prefix(pos + 1);
}
return token;
}
take_token 返回的 token 和被推进后的 input 指向同一段底层存储的不同范围。这里没有字符搬移,没有分配,也没有所有权变化。函数的前提是 input 的底层存储在 token 使用期间继续有效。这个前提一旦不成立,data() 和 size() 保存的数值形式仍然存在,语义已经断裂。
访问接口也应按 data / size 判断。operator[] 假定下标在范围内;at 会在越界时抛出 std::out_of_range;front 与 back 要求视图非空。调用者先用 size() 或 empty() 建立边界,再读取字符。这个顺序适用于所有非拥有视图:先确认可视范围,再访问范围内元素。
45.4 与 string 的区别
std::string 和 std::string_view 的核心差异是所有权。std::string 持有字符序列,负责分配、扩容、释放和维持自身不变量;std::string_view 观察字符序列,依赖外部拥有者维持存储有效。这个差异会传导到可变性、分配、返回值、异常路径和成员保存策略。
| 维度 | std::string | std::string_view |
|---|---|---|
| 所有权 | 拥有字符存储 | 观察外部字符存储 |
| 修改能力 | 可以修改自身字符并改变长度 | 视图接口提供只读字符访问,可以改变视图范围 |
| 分配行为 | 构造、拼接、扩容可能分配 | 复制视图自身通常不分配 |
| 生命周期 | 字符跟随对象生命周期 | 字符跟随外部拥有者生命周期 |
| 返回值含义 | 返回独立拥有的字符串 | 返回外部存储的一段视图 |
| 成员保存 | 类型自身保存内容 | 类型保存借用关系 |
std::string_view 的只读接口需要精确理解。它让调用者通过视图读取 const char 序列,但底层字符是否真的不可变由拥有者决定。如果视图指向一个可变 std::string 的内部存储,拥有者仍然可以修改字符串;视图随后读到的是修改后的字符,或者在重分配后变成失效视图。因此只读视图表达的是“本接口不修改”,不表达“底层世界不会改变”。
返回值是二者最容易分叉的位置。返回 std::string 表示函数构造了一个新拥有者,调用方拿到结果后可以独立保存。返回 std::string_view 表示函数返回的是输入、全局表或某个外部对象的一段观察范围。返回视图时,函数文档或类型设计要让拥有者关系可见。
#include <string>
#include <string_view>
std::string copy_target(std::string_view line) {
RequestLineView parsed = parse_request_line(line);
return std::string(parsed.target);
}
std::string_view view_target(std::string_view line) {
RequestLineView parsed = parse_request_line(line);
return parsed.target;
}
copy_target 返回拥有结果,调用方可以长期保存。view_target 返回输入范围中的一段,调用方必须让传入的 line 底层存储继续有效。两个函数都可能合理;选择标准来自返回值使用场景。结果要进入缓存、队列、异步任务或对象成员时,std::string 更直接。结果只在当前解析链路中继续匹配、路由或计数时,std::string_view 保持轻量。
容器也会放大这个差异。std::vector<std::string> 拥有每个字符串元素,容器元素随容器生命周期管理;std::vector<std::string_view> 只保存若干借用关系。后者适合做临时索引、排序视图、解析窗口和只读目录表,前提是被索引的原始字符存储稳定。若底层 std::string 容器发生扩容并移动元素,元素内部字符地址可能改变;原先保存的视图就需要重新建立。
异常安全角度也不同。构造或追加 std::string 可能在分配失败时抛出异常,函数需要考虑提交点和回滚状态。复制 std::string_view 自身通常不会分配,失败面主要来自后续访问、搜索、范围检查和底层拥有者变化。这个差异不表示 std::string_view 自动更安全;它只是把资源管理问题从当前对象转移到拥有者和调用协议上。
45.5 与 const char* 的区别
const char* 表达一个字符地址,许多 C 风格字符串接口还约定从该地址开始一直读到 \0。std::string_view 表达一个地址加长度的范围。二者都可以观察外部字符存储,但 std::string_view 把长度放进对象状态,能表达中间切片和含零字节的序列;const char* 的传统字符串语义依赖终止符,无法单独表达范围长度。
| 维度 | const char* | std::string_view |
|---|---|---|
| 状态 | 起始地址 | 起始地址与长度 |
| 字符串结束 | 常见约定是扫描到 \0 | 由 size() 决定 |
| 中间切片 | 需要额外长度参数 | 一个对象即可表达 |
| 含零字节 | 传统字符串函数会提前停止 | 范围遍历可以保留零字节 |
| 空值表达 | 可能为 null,需要约定 | 默认构造为空视图,缺席值应由额外状态表达 |
| 接口信号 | 调用方需推断是否保留长度 | 直接表明函数读取一个字符范围 |
这个差异在贯穿材料里非常具体。target 指向请求行中间,后面是空格。把 target.data() 传给 std::strlen,扫描会越过路径继续读后续字符,直到遇到某个 \0。把 target 作为 std::string_view 传递,接收方用 size() 限定范围,就能得到准确字段。
#include <cstring>
#include <iostream>
#include <string_view>
void wrong_c_api_boundary(std::string_view field) {
std::cout << std::strlen(field.data()) << '\n';
}
void correct_range_boundary(std::string_view field) {
std::cout << field.size() << '\n';
}
第一个函数把带长度的范围退化成终止符字符串。只有当 field.data() 指向的缓冲区从该位置开始刚好以 \0 结束,并且 field.size() 覆盖到终止符前,结果才与视图长度一致。第二个函数直接使用范围长度,和 std::string_view 的语义一致。
这并不意味着 C 接口无法接收视图。正确边界是把地址和长度一起传递,或者在必须调用只接受终止符字符串的接口前显式构造拥有字符串。前者适合 write(fd, view.data(), view.size()) 这种带长度接口;后者适合历史 API、系统库或第三方库只接受 null-terminated 字符串的场景。
#include <cstdio>
#include <string>
#include <string_view>
void call_length_aware_api(std::string_view field) {
std::fwrite(field.data(), 1, field.size(), stdout);
}
void call_null_terminated_api(std::string_view field) {
std::string owned(field);
std::puts(owned.c_str());
}
call_length_aware_api 保留了视图范围,fwrite 也接收长度。call_null_terminated_api 在边界处复制出 std::string,让 c_str() 提供调用所需的终止符。这里的复制是接口适配成本,位置应尽量集中在边界层,解析和匹配链路内部继续使用 std::string_view 即可。
const char* 还有一个空指针问题。很多老接口用 null 表达缺席值,调用方必须先判断指针。std::string_view 的默认空状态通常表现为 data() == nullptr 且 size() == 0,但空视图的主要判断方式应是 empty() 或 size()。一般空字符串判断应使用 empty() 或 size(),因为某些空视图可能来自非空缓冲区的零长度切片,指针值仍然指向某个有效位置。
45.6 生命周期悬垂问题
生命周期悬垂是 std::string_view 的核心故障模式。视图对象保存的地址和长度仍然存在,但地址指向的字符对象已经结束生命周期,或者拥有者修改导致原先区间失效。故障表现可能是乱码、偶发正确、测试通过后线上失败、优化级别变化后行为改变,也可能是安全检查工具直接报错。
最典型触发条件是从临时 std::string 创建视图并让视图活过临时对象。下面的函数返回一个悬垂视图:
#include <string>
#include <string_view>
std::string_view bad_header_name() {
std::string name = "Content-Type";
return std::string_view{name};
}
name 是函数内的拥有者对象,函数返回时它的字符存储随之释放。返回值中的 std::string_view 复制了起点和长度,却没有复制字符。调用方得到的对象形式完整,语义已经失效。修复策略取决于目标语义:需要长期保存就返回 std::string;只需返回静态常量就指向字符串字面量或静态存储;需要从输入中切片就让返回视图绑定到输入参数。
#include <string>
#include <string_view>
std::string owned_header_name() {
return "Content-Type";
}
std::string_view static_header_name() {
return "Content-Type";
}
std::string_view first_word(std::string_view line) {
const auto pos = line.find(' ');
return line.substr(0, pos);
}
三个函数表达三种不同所有权。owned_header_name 给调用方独立拥有者;static_header_name 指向静态存储;first_word 返回输入的一段视图。第三个函数的返回值有效期由调用方传入的底层存储决定,这个关系必须在调用点保持。
第二类触发条件是拥有者重分配。std::string 追加内容、扩容、赋新值时可能改变内部字符地址。已经保存的视图仍然指向旧地址,后续读取就会出错。这个问题在日志标签、解析缓存和路由表构建时常出现:代码先从字符串中取视图保存起来,随后继续修改原字符串。
#include <iostream>
#include <string>
#include <string_view>
void reallocation_example() {
std::string line = "GET /a HTTP/1.1";
std::string_view target = parse_request_line(line).target;
line += " extra bytes that may reallocate";
std::cout << target << '\n';
}
这段代码的风险来自 line += ...。如果追加导致 line 重分配,target 中的地址失效。如果实现因小字符串优化或剩余容量足够而没有重分配,程序可能看起来正常;这种偶发正常不能作为正确性依据。稳定做法是先完成对拥有者的修改,再创建视图;或者在保存前复制出 std::string。
第三类触发条件是容器元素移动。视图指向 std::vector<std::string> 中某个字符串元素的内部字符,随后 vector 扩容移动元素,原视图可能失效。即使字符串对象移动后源对象仍处于可析构状态,源对象的字符内容也不再是视图可依赖的稳定存储。把视图索引建立在容器元素上时,要先固定容器容量、冻结元素修改,或者改用稳定存储区域。
生命周期排查可以按四步执行。第一步,找到 std::string_view 的来源表达式,确认底层字符来自字面量、外部参数、局部变量、临时对象、容器元素还是分配缓冲区。第二步,找到视图最长使用点,确认它是否越过拥有者作用域、容器重分配点或字符串修改点。第三步,检查视图是否被保存为成员、放入容器、跨线程传递或异步回调捕获。第四步,根据结果选择复制、缩短视图使用范围、冻结拥有者、改为静态存储或重建视图。
这个顺序比单纯搜索 std::string_view 更可靠,因为故障点常常出现在拥有者变化处。视图声明行看起来简单,真正破坏关系的是临时对象结束、字符串扩容、容器移动和异步延后执行。调试时也应优先记录拥有者地址、view.data()、view.size() 和发生修改的位置。
45.7 工程使用场景
std::string_view 最适合表达短期、只读、带长度的字符串观察。使用场景可以归纳为四类:参数传递、解析切片、日志格式化和减少拷贝。每一类的前提都相同:函数读取字符范围,函数不接管存储,视图使用期被限制在拥有者有效期内。
参数传递是最常见场景。函数只做匹配、查找、比较、转换入口判断时,std::string_view 可以作为统一参数类型。它接收字符串字面量、std::string 和显式长度范围,并让函数签名表达“读取一段字符”。参数函数内部若要保存结果,保存处应复制出拥有字符串,参数视图只承担入口读取职责。
解析切片是收益最大的场景。协议解析、命令行解析、CSV 字段扫描、日志字段切分都可以先保留原始缓冲区,再用视图表示字段。贯穿材料中的 parse_request_line 就是典型路径:原始请求行只有一个拥有者,解析结果只是若干范围。这样可以减少字段级分配,也能让后续路由匹配、方法比较和版本检查直接在原缓冲区上完成。
日志格式化需要更细的边界。短期同步日志函数接收 std::string_view 很合适,因为函数马上读取并写出内容。异步日志系统保存消息到队列时,std::string_view 就只能作为入口类型;入队前应复制到日志缓冲区,或者把拥有者消息对象一起移动进队列。判断点是“日志写出发生在当前调用栈内,还是延后到另一个线程”。
减少拷贝并非无条件目标。std::string_view 减少的是字符串内容复制,不减少字符扫描;find、compare、starts_with、contains 仍然受输入长度影响。若后续需要长期保存、排序、多次查找或跨线程使用,一次性复制成 std::string 可能让所有权关系更清晰。性能判断要同时看分配次数、访问次数、生命周期和接口边界。
下面给出一个工程判断表,帮助把 std::string_view 放在合适位置:
| 场景 | 推荐表达 | 判断理由 |
|---|---|---|
| 只读参数、同步使用 | std::string_view | 不接管存储,入口兼容多种字符来源 |
| 从输入缓冲区切字段 | std::string_view | 字段是原缓冲区中的范围,复制收益有限 |
| 返回新构造文本 | std::string | 函数创建了新内容,调用方需要拥有结果 |
| 长期对象成员 | std::string 或带明确拥有者的视图 | 成员生命周期通常长于一次调用 |
| 调用带长度 C API | data() 加 size() | C 边界也接收范围长度 |
| 调用 null-terminated C API | 临时 std::string | 边界需要终止符和独立缓冲 |
| 字符串字面量常量表 | std::string_view | 字面量处于静态存储期,视图稳定 |
把这些场景统一成一个可复用判断顺序:先判断函数是否拥有或保存字符;再判断输入是否天然带长度;然后判断底层存储是否稳定覆盖视图使用期;接着检查是否进入 C 风格终止符接口;最后根据返回值或成员保存需求决定复制位置。这个顺序把 std::string_view 从“性能技巧”变成了“所有权和范围表达工具”。
本章的最终结论是:std::string_view 的能力来自 data / size,风险来自 non-owning 生命周期。把它用于读取和切片时,它能减少分配并让接口范围更准确;把它用于保存和延后执行时,必须先证明外部拥有者稳定。无法证明时,复制成 std::string 是更清晰的工程边界。
最小自检任务
阅读下面代码,判断 parse_and_store 返回后的 route.method 与 route.target 是否可以长期使用,并给出修复方式。要求按“来源、去向、拥有者变化、边界选择”的顺序分析。
#include <string>
#include <string_view>
struct RouteRecord {
std::string_view method;
std::string_view target;
};
RouteRecord parse_and_store() {
std::string line = "GET /index.html HTTP/1.1";
RequestLineView parsed = parse_request_line(line);
return RouteRecord{parsed.method, parsed.target};
}
答案要点
parsed.method 与 parsed.target 的来源是局部 std::string line 的内部字符存储。它们的去向是 RouteRecord 返回值,使用点会越过 parse_and_store 的函数作用域。函数返回时 line 析构,两个 std::string_view 中的地址和长度还在,底层字符对象已经结束生命周期,因此返回后的 route.method 与 route.target 不具备长期使用条件。
修复方式取决于 RouteRecord 的语义。如果记录对象需要长期保存字段,应把成员改成 std::string,在返回前复制字段内容。若记录对象只是输入缓冲区的临时视图,应让函数接收外部 std::string_view line,并要求调用方保证底层存储覆盖 RouteRecord 的使用期。若字段来自固定协议常量表,可以使用字符串字面量或静态存储作为视图来源。
#include <string>
#include <string_view>
struct OwnedRouteRecord {
std::string method;
std::string target;
};
OwnedRouteRecord parse_and_own(std::string_view line) {
RequestLineView parsed = parse_request_line(line);
return {
std::string(parsed.method),
std::string(parsed.target)
};
}
这个修复把返回值语义改成拥有字段内容,调用方不再依赖输入缓冲区。复制发生在所有权边界处,解析链路内部仍然可以使用 std::string_view 表达切片。
本章知识点总结
- 设计目标:
std::string_view把读取字符内容和拥有字符存储拆开,用一个轻量对象表达只读字符串范围。 - 状态核心:视图由起点和长度定义,
data()提供起始地址,size()提供可读字符数量。 - 所有权边界:
std::string拥有字符,std::string_view观察字符,生命周期责任留在外部拥有者。 - 接口信号:只读同步参数适合使用
std::string_view,因为函数只是消费字符范围。 - 切片语义:
substr、remove_prefix和remove_suffix改变视图范围,不移动底层字符。 - 终止符边界:
std::string_view::data()返回的缓冲区可以没有\0,带长度 C API 应同时接收地址和长度。 - string 区别:返回或保存长期结果时,
std::string表达独立拥有,std::string_view表达外部借用。 - 指针区别:
const char*只有起始地址,传统字符串语义依赖终止符;std::string_view把长度纳入对象状态。 - 悬垂来源:局部字符串、临时字符串、拥有者重分配和容器元素移动都会让已有视图失效。
- 排查顺序:先找视图来源,再找最长使用点,再检查拥有者变化,最后决定复制、缩短使用范围或重建视图。
- 成员保存:长期对象成员保存
std::string_view时,需要明确外部拥有者和生命周期覆盖关系。 - 工程边界:
std::string_view是范围表达工具,无法证明底层存储稳定时,应在边界处复制成std::string。