C++17 新特性: std::string_view —— 减少内存分配,让std::string运行得更快
在 C++ 中,字符串操作看似简单,但在性能敏感的场景下,频繁的堆内存分配可能会成为性能瓶颈。C++17 引入了 std::string_view,可以让我们以零拷贝的方式访问字符串数据,从而显著提升性能。本文将通过实际代码演示问题、验证方法和解决方案。
一、原始方法的问题:std::string 会隐式分配内存
在 C++ 中,std::string 会在构造时分配内存。例如:
#include <iostream>
#include <string>
void PrintName(const std::string& name) {
std::cout << "Name: " << name << std::endl;
}
int main() {
std::string name = "Hatsune Miku";
PrintName(name);
}
即便是传入字面量 "Hatsune Miku",std::string 也会在堆上分配内存来存储字符串。
如果我们要拆分名字和姓氏:
std::string firstName = name.substr(0, 7);
std::string lastName = name.substr(8, 4);
PrintName(firstName);
PrintName(lastName);
每次 substr() 都会触发新的堆内存分配。频繁操作时,这种隐式分配可能成为性能瓶颈。
注意:现代 C++ 实现对小字符串使用 SSO(Small String Optimization)。
对于短字符串(通常 < 15~23 字符),std::string 会直接在栈上存储,不会触发堆分配。
所以对于 "Hatsune Miku" 这种短字符串,即使调用 substr(),堆分配计数仍可能是 0。
二、如何验证内存分配:重载 operator new
为了直观观察程序中的堆内存分配,可以重载全局 operator new:
static uint32_t s_AllocCount = 0;
void* operator new(size_t size) {
s_AllocCount++;
std::cout << "Allocating " << size << " bytes\n";
return malloc(size);
}
使用这个方法,我们可以统计堆分配次数:
std::string name = "Hatsune Miku";
std::string firstName = name.substr(0, 7);
std::string lastName = name.substr(8, 4);
PrintName(firstName);
PrintName(lastName);
std::cout << s_AllocCount << " allocations totally\n";
输出示例:
Allocating 16 bytes
Allocating 16 bytes
Allocating 16 bytes
3 allocations totally
可以看到,每个 std::string 构造和 substr() 都触发了一次分配,说明原始方法确实会带来额外开销。
三、std::string_view 的主要特性
-
非拥有所有权:
std::string_view不管理字符串的内存,不负责分配或释放。 -
不可变性:视图中字符串内容不可被修改,保证数据安全性。
-
高效性:无需内存拷贝,传递和操作字符串更加高效。
-
灵活性:可以从多种类型的字符串对象构造,如
std::string、C 字符串等。
四、与 std::string 的区别
| 特性 | std::string | std::string_view |
|---|---|---|
| 所有权管理 | 拥有底层字符串的所有权 | 不拥有底层字符串的所有权 |
| 可变性 | 可修改字符串内容 | 不可修改字符串内容 |
| 内存拷贝 | 通常涉及内存拷贝 | 无需内存拷贝 |
| 构造成本 | 高(涉及内存分配和拷贝) | 低(仅记录指针和长度) |
| 生命周期管理 | 管理自身资源 | 依赖外部字符串的生命周期 |
| 使用场景 | 需要拥有或修改字符串时 | 只需读取或传递字符串时 |
理解这些区别有助于选择合适的数据类型,提高性能并减少不必要的堆分配。
五、使用 std::string_view 避免不必要分配
std::string_view 是一个轻量级对象,只包含一个指针和长度,并不拥有字符串本身。它可以安全地创建字符串“窗口”,而无需在堆上分配内存。
#include <iostream>
#include <string>
#include <string_view>
void PrintName(std::string_view name) {
std::cout << "Name: " << name << std::endl;
}
int main() {
std::string name = "Hatsune Miku";
std::string_view firstName(name.c_str(), 7); // 指向 "Hatsune"
std::string_view lastName(name.c_str() + 8, 4); // 指向 "Miku"
PrintName(firstName);
PrintName(lastName);
std::cout << s_AllocCount << " allocations totally\n";
}
输出结果:
Name: Hatsune
Name: Miku
1 allocations totally
这里仅在
name初始化时发生一次分配,子字符串操作不再分配内存。
如果字符串本身是静态字面量,则可以做到完全零分配:
const char* name = "Hatsune Miku";
std::string_view firstName(name, 7);
std::string_view lastName(name + 8, 4);
PrintName(firstName);
PrintName(lastName);
输出结果:
Name: Hatsune
Name: Miku
0 allocations totally
字符串在静态存储区,
string_view仅是指针 + 长度,整个过程没有堆内存分配。
在这种情况下,程序 没有任何堆内存分配,效率最优。
六、总结
-
问题根源:
std::string拥有自己的内存,每次构造或substr()都会分配内存。 -
验证方法:重载
operator new可以统计堆内存分配次数。 -
解决方案:
std::string_view提供对已有字符串的只读窗口,避免不必要的分配。 -
最佳实践:
-
函数参数尽量使用
std::string_view,减少隐式构造。 -
静态字符串使用
const char*+std::string_view可做到零分配。 -
避免频繁使用
substr(),用string_view截取子串更高效。
-
通过这种方式,我们不仅写出可读性强的代码,也显著减少了内存分配开销,提升程序性能。
更多推荐
所有评论(0)