ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ std::string_view 不是字符串:从悬空引用到安全用法

C++ std::string_view 不是字符串:从悬空引用到安全用法 Cstd::string_view不是字符串从悬空引用到安全用法摘要std::string_view能减少不必要的字符串复制但它不拥有字符数据。只要底层字符串被销毁、重新分配或修改视图就可能失效。本文通过可编译示例解释它的对象模型、常见悬空场景、接口设计原则以及什么时候应该改用std::string。std::string_view是 C17 引入的只读字符串视图。它通常可以理解为一对数据指向字符序列开头的指针以及字符序列的长度。它不会分配内存也不会复制字符。这个特点让它适合只读参数、解析器和文本切片但也带来一个核心约束std::string_view的生命周期不能超过底层字符数据。先看对象模型下面这段程序可以在支持 C17 的编译器中运行#includeiostream#includestring#includestring_viewintmain(){std::string texthello, string_view;std::string_view viewtext;std::couttext.data(): static_castconstvoid*(text.data())\n;std::coutview.data(): static_castconstvoid*(view.data())\n;std::coutview: view\n;}一种编译方式如下g-stdc17-Wall-Wextra-Wpedanticdemo.cpp-odemo ./demo在同一次运行中text.data()与view.data()通常指向同一段字符数据。view只是观察text并没有保存一份副本。这也解释了为什么复制std::string_view很便宜复制的是指针和长度而不是整段字符串。危险场景一返回局部字符串的视图下面的代码可以通过编译但返回值在函数结束时就已经悬空#includestring#includestring_viewstd::string_viewmake_message(){std::string messagetemporary message;returnmessage;}message在make_message返回时被销毁视图里保存的指针不再指向有效字符序列。之后读取这个视图属于未定义行为。不能通过“调试时还能打印出来”判断代码安全。未定义行为可能暂时看似正常也可能随着编译器版本、优化级别或周围代码变化而暴露。如果调用者需要拥有返回内容应返回std::stringstd::stringmake_message(){returntemporary message;}现代编译器可以通过返回值优化或移动语义减少多余复制。为了避免一次未经测量的复制而返回悬空视图通常得不偿失。危险场景二从临时对象构造这段代码同样存在问题std::string_view viewstd::string(temporary);右侧临时std::string在完整表达式结束时被销毁下一条语句开始时view就已经悬空。但字符串字面量的情况不同std::string_view viewstatic storage;字符串字面量具有静态存储期程序运行期间一直存在因此视图不会因为离开当前作用域而失效。关键不是“右侧看起来像字符串”而是字符数据由谁拥有、能活多久。危险场景三底层字符串重新分配即使原始std::string仍在作用域内视图也不一定始终有效#includeiostream#includestring#includestring_viewintmain(){std::string textabc;std::string_view viewtext;textstd::string(10000,x);// 不要再读取 viewtext 的扩容可能已经使原指针失效。std::coutnew size: text.size()\n;}std::string扩容时可能申请新的存储空间并把字符移动过去。此时旧视图仍保存原地址却无法知道地址已经失效。还需要警惕这些操作对底层字符串执行可能触发重新分配的追加、插入和赋值移动、交换或销毁底层字符串对容器进行可能使元素或其内部存储失效的操作把视图保存到比所有者寿命更长的对象中。标准库对不同操作的失效规则有细节差异。设计代码时不应依赖某次运行中容量“刚好够用”。作为函数参数时为什么好用只读函数通常可以接受std::string_view#includeiostream#includestring#includestring_viewvoidprint_name(std::string_view name){std::coutname\n;}intmain(){print_name(Alice);std::string userBob;print_name(user);}这个接口可以接收字符串字面量、std::string和其他连续字符序列的视图函数内部也不需要复制内容。前提是函数只在调用期间使用视图不把它保存到更长寿命的位置。下面这个类的设计就需要额外约束classUserView{public:explicitUserView(std::string_view name):name_(name){}private:std::string_view name_;};如果构造参数来自局部std::stringUserView可能比字符串活得更久。除非能严格保证外部所有者的生命周期否则成员字段更适合使用std::stringclassUser{public:explicitUser(std::string name):name_(std::move(name)){}private:std::string name_;};substr的返回类型容易混淆std::string::substr和std::string_view::substr名字相同语义却不同#includestring#includestring_view#includetype_traitsintmain(){std::string textabcdef;std::string_view viewtext;autoownedtext.substr(1,3);autoborrowedview.substr(1,3);static_assert(std::is_same_vdecltype(owned),std::string);static_assert(std::is_same_vdecltype(borrowed),std::string_view);}前者创建拥有字符数据的std::string后者只创建原字符序列的另一个窗口。borrowed的有效期仍受text约束。不保证以\0结尾std::string_view表示“指针加长度”视图末尾不一定有空字符std::string texthello world;std::string_viewword(text.data(),5);把word.data()直接传给只接受 C 风格字符串的接口可能导致读取越过视图边界。若接口支持长度应该同时传递长度#includecstdiostd::printf(%.*s,static_castint(word.size()),word.data());如果目标接口只接受以\0结尾的字符串则需要创建拥有内容的std::stringstd::stringowned(word);legacy_api(owned.c_str());一个实用的接口选择表需求更合适的类型原因函数只读参数调用期间使用std::string_view避免复制可接收多种字符串来源函数需要修改自己的副本std::string明确拥有数据返回新生成的文本std::string返回值需要独立生命周期返回输入参数的切片std::string_view可以零复制但必须说明依赖输入寿命类需要长期保存文本通常是std::string避免外部生命周期耦合调用只接受 C 字符串的旧接口std::string或长度感知接口视图不保证空字符结尾如何借助工具发现问题编译器警告不能发现所有生命周期错误但应该先打开基础警告g-stdc20-Wall-Wextra-Wpedantic-Wconversiondemo.cpp-odemoAddressSanitizer 有时能捕获已经发生的越界或释放后使用g-stdc20-O1-g-fsanitizeaddress,undefined demo.cpp-odemo ./demo不过工具没有报错不代表视图一定安全。某些悬空指针仍可能指向尚未被覆盖的内存。代码审查时仍要明确回答两个问题这段字符数据由谁拥有所有使用视图的地方是否都早于所有者失效结论std::string_view的价值不是“比std::string更高级”而是让接口表达借用一段只读字符序列。当生命周期关系清晰时它能减少复制并让接口更通用当生命周期不清晰时它会把原本由类型管理的问题变成调用者必须自行证明的约束。可以记住一条简单规则短期观察用std::string_view需要保存或返回新数据时用std::string。性能优化应该建立在正确的所有权模型和实际测量之上。
返回列表