(鄙人总结,希望和大家交流,切莫转载,谢谢!)
C++17 使用std::string_view
来观测一段字符数组,减少内存的拷贝,或者说不至于像string
对象移动后,前一个对象放弃那块本属于它的内存。
// 原码:
// TYPEDEFS FOR basic_string_view
using string_view = basic_string_view<char>;
// CLASS TEMPLATE basic_string_view
template <class _Elem, class _Traits>
class basic_string_view {
public:
using const_pointer = const _Elem*;
using size_type = size_t;
...
private:
const_pointer _Mydata;
size_type _Mysize;
}
可以看到string_view
的成员变量只有2个,const_pointer _Mydata
:指向字符串首地址的指针。 size_type _Mysize
:字符串的长度(不含"\0
")
比如下例:使用string
s给string_view
sv初始化
// 例1:
string s{ "Hello" };
string_view sv{ s };
完成sv的初始化后,我们分别监视s和sv:
可以看到sv是对s的buf的观测,即s.data() == sv.data() == sv._Mydata
,且sv._Mysize = s.size()
,至此sv初始化完成,它刻画一个字符串,这个字符串的长度为5(不能说长度为s.size()
),起始地址为s.data()
.
// 例子2 给s拼接一个" World"
string s{ "Hello" };
string_view sv{ s };
s += " World";
cout << s << endl;
cout << sv << endl;
结果显示:
Hello World
Hello
再次监测s和sv
发现s在capacity
范围内(当前环境得出的capacity如上图s的监视图的[capacity]
所示为5),增加了" World",而sv只是刻画访问那个内存的5个字节的字符串,所以sv的输出依旧是"Hello"。
那么,如果s增加的字符数大于原本的capacity,势必会迎来成长,发生内存的搬移,sv依旧访问原本的内存,此时就会得到意料之外的值。比如:
// 例3 突破此环境(MSVC)下capacity为15的上限
s += " World World World World World";
cout << s << endl;
cout << sv << endl;
结果显示:
Hello World World World World World
萯?o
string
跟string_view
都有返回子串的方法:substr(pos, cnt),看看它干了什么事:
_NODISCARD constexpr basic_string_view substr(const size_type _Off = 0, size_type _Count = npos) const {
// return a new basic_string_view moved forward by _Off and trimmed to _Count elements
_Check_offset(_Off);
_Count = _Clamp_suffix_size(_Off, _Count);
return basic_string_view(_Mydata + _Off, _Count);
}
它返回一个临时对象的拷贝,这个临时对象的_Mydata
= 原对象的_Mydata + _Off
, _Mysize
为第二入参_Count
,因此可以知道返回值依旧是指向同一块内存的不同位置的指针,其长度为_Count,因此子串仍旧会出现上述因为string
成长所带来的问题。
string
和string_view
都提供.data()
的方法,返回一个指向字符串的指针。当使用::printf("%s", ptr)
时,或者cout << ptr << endl
来打印字符串时,稍微注意string_view
的输出
// 例4 sv_sub的内部指针指向sv的第3个字符的地址
string s{ "Hello" };
string_view sv{ s };
string_view sv_sub = sv.substr(2, 2);
cout << sv_sub << endl;
::printf("%s", sv_sub.data());
结果显示:
ll
llo
这里printf
的输出并不能达到只打印子串所观测的2个字符,它会一直打到字符串结束为止。稍微注意一下即可。
又由于string_view
避免了字符串对象在非移动赋值时的内存申请,和N个字符的拷贝,因此从效率上来说是相当高的,这里做一个对比。
// 例5 添加ctime头文件用于获取tick
#include <ctime>
...
const long _LOOP_MILLION = 1000000; // 循环一百万次
/* 在ctime中有这样一个定义,tick的差值除以1000就等于时间差(毫秒级)
// The number of clock ticks per second
#define CLOCKS_PER_SEC ((clock_t)1000)
*/
clock_t startTime, endTime;
startTime = clock();
for (long i{}; i < _LOOP_MILLION; ++i)
{
string sub(std::move(s.substr())); // string子串涉及内存拷贝,然后移动给string sub
}
endTime = clock();
cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
startTime = clock();
for (long i{}; i < _LOOP_MILLION; ++i)
{
string_view sub(move(sv.substr())); // string_view子串不涉及内存拷贝,构造string_view sub时传入地址和长度
}
endTime = clock();
cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
结果显示:
The run time is: 3.975s
The run time is: 0.174s
所以在设计优秀的算法之外,优秀的编程方式也很重要。