C++的std::string是C++标准库中使用最广泛的特性之一,例如google的chromium,std::string对内存管理器的调用次数占到了内存管理器被调用的总次数的一半
前面也提到过,只要涉及内存被频繁分配/复制的地方,就有优化的可用武之地
字符串是动态分配的
C++的std::string实际上是一个char的数组,其大小是固定的,如果调用+
运算,新字符串的大小超过原字符串的大小,就会发生以下操作
- 申请一块内存
- 将新字符串复制到内存中
- 释放掉原有字符串
如果是以下的代码,就会发生频繁的内存分配
1 | std::string str = "a"; |
字符串是值
在赋值语句和表达式中,字符串的行为和值一样,例如你使用str1=str2+str3+str4
,那么str2+str3
的结果会保存在一个临时变量中,然后用其和str4
相加,这时又会产生一个临时变量,然后用于赋值给str1
消除临时变量
我们可以分析一下这段代码的情况
1 | str = str + "b"; |
这段代码调用的是operator+,其函数源码如下
1 | template <class _CharT, class _Traits, class _Alloc> |
首先,我们看到的是其返回值,是basic_string<_CharT,_Traits,_Alloc>
,也就说,其会存在一次复制,将内部用于保存新字符串的临时变量复制出去
临时字符串的构建是使用append
进行构建的,append
的部分源码如下
1 | basic_string& append(const basic_string& __s) |
然后我们再看看operator+=
1 | basic_string& operator+=(const basic_string& __s) { return append(__s); } |
看返回值,返回的是引用,减少了一次复制,然后新字符串的构建也是使用的append
,因此不用多说,自然是operator+=
更胜一筹
因此可以使用str+="b"
替换之前的str=str+"b"
减少内存的重新分配
无论是+=/+
都会使得字符串不断增长,假设,std::string
的内存管理如std::vector
一般,也就是预先分配较大块的内存,例如两倍,那么如果append的字符串不断增长,例如
1 | a = "a"; |
的情况,那么频繁的内存分配和复制依旧可能会发生,此时,有一种策略则是预先分配空间,然后减少内存的分配和复制,这种策略对于很多动态分配的容器都是有用的
1 | str.reserve(length); |
减少对参数字符串的复制
这个在很多书籍都有提到过,通过使用引用传递,消除参数的复制,不过值得注意的是,这种策略对于对象才起作用,如果是对于一般的基本类型,这种策略的效果甚微
使用迭代器消除指针解引用
std::string
的实质是字符数组,因此也会支持下标访问,例如str[i]
,其源码如下
1 | const_reference operator[](size_type __n) const |
我们可以通过迭代器直接进行操作,而不必进行这次多余的函数调用(不过某些时候会导致可读性下降)
消除对返回值的复制
我们可以将接收返回值的变量的引用传入,直接改变,而不是用返回值对其进行赋值
不过,这种策略只适合对结果字符串进行频繁操作的情况
使用字符数组代替字符串
这种策略用于对性能要求极高的情况下,使用C风格的字符串函数来手动编写函数,进行手动的内存管理,不过该种策略难度较高,而且不易使用,但是却能带来显著的性能提升(前提是所有函数都使用得当的情况下)
使用更好的算法
对于某些情况我们可以通过改进算法来达到更高的效率
例如,从字符串中删除某些字符,下面有几种方法,我们逐一分析
1 | //1 |
下面是分析:
代码1,频繁地进行字符复制
代码2,将字符复制改为字符串复制,减少了内存复制的频率
代码3,在代码2中,存在以下函数
1 | basic_string substr(size_type __pos = 0, size_type __n = npos) const { |
这个会造成一次新字符串的复制
将其改为append
将会消除这次复制
代码4,对同一个字符串进行操作,只涉及到少量的内存复制,除了返回字符串涉及到内存复制,其他操作都不会涉及,性能优于前三者
1 |
|
总结
上面介绍到优化方式需要对代码进行推敲后使用,切不可盲目乱用,盲目优化不可取,但是一些策略是可以通用的
例如传递参数的时候使用引用传递,减少临时变量的产生,以及使用更好的算法