内容简介:本篇文章从在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在在
本篇文章从 string_view 引入的背景出发,依次介绍了其相关的知识点及使用方式,然后对常见的使用陷阱进行了说明,最后对该类型做总结。
一、背景
在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在 C 中指针是完成这一目的的标准数据结构,而 C++ 引入了安全性更高的引用类型。所以在 C++ 中若传递的数据仅仅只读, const string& 成了 C++ 的天然的方式。但这并非完美,从实践来看,它至少有以下几方面问题:
- 字符串字面值、字符数组、字符串指针的传递仍要数据拷贝
这三类低级数据类型与string类型不同,传入时,编译器需要做隐式转换,即需要拷贝这些数据生成string临时对象。const string&指向的实际上是这个临时对象。通常字符串字面值较小,性能损耗可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,会严重影响程序的性能。 -
substrO(n) 复杂度
这是一个特别常用的函数,好在std::string提供了这个函数,美中不足的是其每次都返回一个新生成的子串,很容易引起性能热点。实际上我们本意并不是要改变原字符串,为什么不在原字符串基础上返回呢?
在 C++17 中引入了 string_view ,能很好的解决以上两个问题。
二、std::string_view
从名字出发,我们可以类比数据库视图, view 表示该类型不会为数据分配存储空间,而且该数据类型只能用来读。该数据类型可通过 {数据的起始指针,数据的长度} 两个元素表示,实际上该数据类型的实例不会具体存储原数据,仅仅存储指向的数据的起始指针和长度,所以这个开销是非常小的。
要使用字符串视图,需要引入 <string_view> ,下面介绍该数据类型主要的API。这些API基本上都有 constexpr 修饰,所以能在编译时很好地处理字符串字面值,从而提高程序效率。
2.1 构造函数
constexpr string_view() noexcept; constexpr string_view(const string_view& other) noexcept = default; constexpr string_view(const CharT* s, size_type count); constexpr string_view(const CharT* s);
基本上都是自解释的,唯一需要说明的是:为什么我们代码 string_view foo(string("abc")) 可以编译通过,但为什么没有对应的构造函数?
实际上这是因为 string 类重载了 string 到 string_view 的转换操作符:
operator std::basic_string_view<CharT, Traits>() const noexcept;
所以, string_view foo(string("abc")) 实际执行了两步操作:
-
string("abc")转换为string_view对象 a -
string_view使用对象本篇文章从string_view引入的背景,
2.2 自定义字面量
自定义字面量也是C++17新增的特性,提高了常量的易读。
下面的代码取值 cppreference ,能很好地说明自定义字面值和字符串语义的差异。
#include <string_view>
#include <iostream>
int main()
{
using namespace std::literals;
std::string_view s1 = "abc\0\0def";
std::string_view s2 = "abc\0\0def"sv;
std::cout << "s1: " << s1.size() << " \"" << s1 << "\"\n";
std::cout << "s2: " << s2.size() << " \"" << s2 << "\"\n";
}
输出:
s1: 3 "abc" s2: 8 "abc^@^@def"
以上例子能很好看清二者的语义区别, \0 对于字符串而言,有其特殊的意义,即表示字符串的结束,字符串视图根本不care,它关心实际的字符个数。
2.3 成员函数
下面列举其成员函数:忽略了函数的返回值,若函数有重载,括号内用 ... 填充。这样可以对其有个整体轮廓。
// 迭代器 begin() end() cbegin() cend() rbegin() rend() crbegin() crend() // 容量 size() length() max_size() empty() // 元素访问 operator[](size_type pos) at(size_type pos) front() back() data() // 修改器 remove_prefix(size_type n) remove_suffix(size_type n) swap(basic_string_view& s) copy(charT* s, size_type n, size_type pos = 0) string_view substr(size_type pos = 0, size_type n = npos) compare(...) starts_with(...) ends_with(...) find(...) rfind(...) find_first_of(...) find_last_of(...) find_first_not_of(...) find_last_not_of(...)
从函数列表来看,几乎跟 string 的只读函数一致,使用 string_view 的方式跟 string 基本一致。有几个地方需要特别说明:
-
string_view的substr函数的时间复杂度是O(1),解决了 背景 部分的第二个问题。 - 修改器 中的三个函数仅会修改
string_view的数据指向,不会修改指向的数据。
除此之外,函数名基本是自解释的。
2.4 示例
Haskell中有一个常用函数 lines ,会将字符串切割成行存储在容器里。下面我们用 C++ 来实现
string-版本
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>
void lines(std::vector<std::string> &lines, const std::string &str) {
auto sep{"\n"};
size_t start{str.find_first_not_of(sep)};
size_t end{};
while (start != std::string::npos) {
end = str.find_first_of(sep, start + 1);
if (end == std::string::npos)
end = str.length();
lines.push_back(str.substr(start, end - start));
start = str.find_first_not_of(sep, end + 1);
}
}
上面我们用 const std::string & 类型接收待分割的字符串,若我们传入指向较大内存的字符指针时,会影响程序效率。
使用 std::string_view 可以避免这种情况:
string_view-版本
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <string_view>
void lines(std::vector<std::string> &lines, std::string_view str) {
auto sep{"\n"};
size_t start{str.find_first_not_of(sep)};
size_t end{};
while (start != std::string_view::npos) {
end = str.find_first_of(sep, start + 1);
if (end == std::string_view::npos)
end = str.length();
lines.push_back(std::string{str.substr(start, end - start)});
start = str.find_first_not_of(sep, end + 1);
}
}
上面的例子仅仅是把 string 类型修改成了 string_view 就获得了性能上的提升。一般情况下,将程序中的 string 换成 string_view 的过程是比较直观的,这得益于两者的成员函数的相似性。但并不是所有的“翻译”过程都是这样的,比如:
void lines(std::vector<std::string> &lines, const std::string& str) {
std::stringstream ss(str);
std::string line;
while (std::getline(ss, line, '\n')) {
lines.push_back(line);
}
}
这个版本使用 stringstream 实现 lines 函数。由于 stringstream 没有相应的构造函数接收 string_view 类型参数,所以没法采用直接替换的方式,所以翻译过程要复杂点。
三、使用陷阱
世上没有免费的午餐。不恰当的使用 string_view 也会带来一系列的问题。
-
string_view范围内的字符可能不包含\0
如
#include <iostream>
#include <string_view>
int main() {
std::string_view str{"abc", 1};
std::cout << str.data() << std::endl;
return 0;
}
本来是要打印 a ,但输出了 abc 。这是因为字符串相关的函数都有一条兼容C的约定: \0 代表字符串的结尾。上面的程序打印从开始到字符串结束的所有字符,虽然 str 包含的有效字符是 a ,但 cout 认 \0 。好在这块内存空间有合法的字符串结尾符,如果 str 指向的是一个没有 \0 的字符数组,程序很有可能会出现内存问题,所以我们在将 string_view 类型的数据传入接收字符串的函数时要非常小心。
2.从 [const] char* 构造 string_view 对象时间复杂度 O(n)
这是因为获取字符串的长度需要从头开始遍历。如果对 [const] char* 类型仅仅是一些 O(1) 的操作,相比直接使用 [const] char* ,转为 string_view 是没有性能优势的。只不过是相比 const string& , string_view 少了拷贝的损耗。实际上我们完全可以用 [const] char* 接收所有的字符串,但这个类型太底层了,不便使用。在某些情况下,我们转为 string_view 可能仅仅是想用其中的一些函数,比如 substr 。
3. string_view 指向的内容的生命周期可能比其本身短
string_view 并不拥有其指向内容的所有权,用Rust的术语来说,它仅仅是暂时 borrow (借用)了它。如果拥有者提前释放了,你还在使用这些内容,那会出现内存问题,这跟 悬挂指针 (dangling pointer)或悬挂引用(dangling references)很像。Rust专门有套机制在编译时分析变量的生命期,保证 borrow 的资源在使用期间不会被释放,但C++没有这样的检查,需要人工保证。下面列出一些典型的问题情况:
std::string_view sv = std::string{"hello world"};
string_view foo() {
std::string s{"hello world"};
return string_view{s};
}
auto id(std::string_view sv) { return sv; }
int main() {
std::string s = "hello";
auto sv = id(s + " world");
}
四、总结
string_view 解决了一些痛点,但同时也引入了指针和引用的一些老问题。C++标准并没有对这个类型做太多的约束,这引来的问题是我们可以像平常的变量一样以多种方式使用它,如,可以传参,可以作为函数返回值,可以做普遍变量,甚至我们可以放到容器里。随着使用场景的复杂,人工是很难保证指向的内容的生命周期足够长。所以,推荐的使用方式: 仅仅作为函数参数 ,因为如果该参数仅仅在函数体内使用而不传递出去,这样使用是安全的。
请关注我的公众号哦。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- GitHub 教师工具箱
- 【译】Go 的工具箱综述
- SteamTools 2.4.1 发布,包含多种 Steam 工具功能的工具箱
- 2020年,11种应该加入工具箱的顶级VueJS开发员工具
- 时间序列分析工具箱——tibbletime
- 成为“黑客”前,必拥有的”黑客工具箱“
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java程序设计
宋中山 严千钧 等编 / 清华大学出版社 / 2005-8 / 27.00元
本书全面、系统地介绍了Java语言的基本概念、基本语法和编程方法。主要内容包括:Java语言概述、数据类型与运算符、流程控制语句、类与对象、继承与多态、异常处理、工具类和算法、Applet小应用程序、图形用户界面、输入和输出、Java多线程以及Java高级编程。每章后面附有习题,读者可参考使用。 本书内容丰富,结构合理,语言简洁,深入浅出,通俗易懂。基础知识与程序实例相结合,示例典型......一起来看看 《Java程序设计》 这本书的介绍吧!