一、C/C++的字符串
C风格的字符串性能高,但是使用不方便。能够高效的处理字符串是每种高级语言都应该具备的能力。C++有多种处理字符串的方式。
const char* constStr = "Hello World"; //指向常量字符串的指针
char charArr[] = "Hello World"; //字符数组
std::string str = "Hello World"; //std::string
std::string_view view = "Hello World"; //std::string_view
常量字符串
常量字符串也称静态字符串,是编译时就已经存放在静态区的常量,只能读取,不能修改。
用constStr指向一个字符串,指针会指向静态区,修改会导致段错误。
同时,常量字符串是一个左值!
字符串数组
charArr是一个局部变量,类型是char*。在栈上分配一块空间,长度为strlen(“Hello World”) + 1,因为要保存数组最后的’\0’。
string对象
str是一个局部变量,类型是string。在栈上分配一块空间用于存储string对象,在str内部,会有一个指针指向在堆上申请的一块空间,长度为strlen(“Hello World”) + 1。
这里面会发生两次字符串遍历,一是用strlen求出字符串的长度,二是将字符串的内容拷贝到堆上申请的空间。在空间占用上,栈上和堆上都有开销。
string_view对象
只是单纯设置静态字符串的起始指针和偏移量。创建string_view对象的时候,只在栈上分配string_view对象的空间,没有动态空间的分配,也没有对字符串的遍历。
一般是用strlen获取字符串的长度,但是实际上字符串在编译的时候长度就已经确定了,因此编译器是可以直接得知长度的。
二、string的缺点
在数据传递中减少拷贝是提高性能的办法。体现在指针和引用。在C++中若传递的字符串仅仅可读,const string&
是很常用的方式,但是仍有一些缺点。
不可避免的深拷贝
字符串字面值、字符数组、字符串指针的传递依然要数据拷贝
这三个类型在传入时会隐式转换,即深拷贝生成临时的string对象。在字符串内容比较大的情况下(文件),此时会引起耗时的内存分配和数据拷贝,降低效率。虽然可以通过右值引用来提高效率,但是不是从根本上解决问题,因为都要构造string对象。
substr 的复杂度
string().substr()用于返回字符串的子串,但是是返回一个新的子串对象,既要遍历源字符串,又要构造新对象。实际上可以不提取这部分,而是只访问。
三、std::string_view
C++17引入了std::string_view,用于获取一个字符串的视图,不是创建一个新的字符串对象,而是只拥有查看字符串的功能。string_view给在房间里的你提供一扇窗户,透过窗户,你可以看到外面的景色,但你只能眼观。只有身处其中,你才拥有了对风景的触感。
string_view相比string性能要高很多,因为每个string都有一份字符串的深拷贝,而string_view只是记录了对应字符串的指针和偏移位置。
构造函数
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支持用一个常量字符串来赋值,是因为string类重载了string到string_view的赋值运算符重载。
operator std::basic_string_view<charT, Traits>() const noexcept;
因此实际上是先拿字符串构造一个string临时对象,再用string_view观察这个对象。
使用方式
#include <iostream>
#include <string_view>
const char* constStr = "Hello World";
std::string_view view1(constStr);
std::string_view view2(constStr, 5);
std::string_view view3("Hello\0World");
std::cout << view1 << std::endl; // Hello World
std::cout << view2 << std::endl; // Hello
std::cout << view3 << std::endl; // Hello
std::string_view GetStringView()
{
std::string name = "Hello";
return std::string_view(name);
// Warning 因为name出了作用于就被销毁了。
}
-
可以用字符串或string对象来构造string_view。
-
string_view通常用于函数参数类型,可用来取代
const char* 和 const string&
,可以避免不必要的内存分配。 -
string_view的接口是 string 的子集,只包含读取字符串内容的部分。
-
string_view().substr()的返回值类型是string_view对象,不分配内存;string::substr()的返回值类型是string对象,会分配内存。
因为string_view的接口是 string 的子集,所以在很多场景下可以使用string_view代替string。
比如生成一个string对象后,如果不会修改它,那么可以把对象转换成为string_view,后续操作全部使用string_view。
注意:string_view是没有c_str()接口的。c_str()函数的语义在于返回一个C风格的字符串,而string_view管理的字符串可能只是字符串的子串。
但是有data()接口。对于std::string,data()与c_str()的作用相同。string_view的data()接口是返回它所保存的数据指针。在使用data()的时候需要注意长度,比如cout << sv.data();与 cout << sv;
的结果可能是不一样的,前者可能会多输出一部分字符。
原理
std::string_view提供一个字符串的视图,可以通过这个对象观测字符串,但不允许修改。由于它只读的特性,它不是拥有这个字符串对象,而是与其共享这一空间,因此构造string_view对象没有拷贝。
string_view类的成员变量只包含两个:字符串指针和字符串长度。字符串指针可能是指向某个字符串,也可能指向某个字符串的子串。
string_view比较适用于处理大文件,把文件里的东西全读到内存,方便观察,不需要创建新的string对象存放它们,效率高。
注意事项
- std::string_view管理的只是指针,使用时需要检查指针
- 如果对常量字符串是O(1)的操作,相比直接使const char*,string_view是没有性能优势的。但是相比const string&,string_view少了拷贝。
- string_view并不掌管其指向的内容。如果源字符串析构了还去使用,就会出现问题。