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
截取子串更高效。
通过这种方式,我们不仅写出可读性强的代码,也显著减少了内存分配开销,提升程序性能。