使用右值引用构造string仍会引发内存拷贝吗?
测试代码
#include <iostream> #include <string> #include <string_view> using namespace std; int main() { string a = "abcdef"; cout << (const int*)a.c_str() << endl;//0x64fe20 string_view av{ a }; cout << av << endl;//abcdef string b = std::move(a); cout << (const int*)b.c_str() << endl;//0x64fdf0 cout << av << endl;// bcdef }
可能的输出
0x64fe20 abcdef 0x64fdf0 bcdef
问题描述
使用std::move(a)构造string b的核心目的是避免内存拷贝,但测试发现:a与b的内部字符串缓冲区地址不同,且原本指向a的string_view输出异常(变为" bcdef")。由此疑惑是否发生了至少部分内存拷贝?C未保证缓冲区地址相同,但如何实现无拷贝的移动构造?测试环境为VS2022及GCC(版本遗忘),采用C20标准。
解答
缓冲区地址不同的原因:小字符串优化(SSO)
主流C++标准库的std::string都实现了小字符串优化——对于长度较短的字符串(不同编译器阈值不同,比如VS默认是15字节+终止符),会直接把字符存在std::string对象内部的缓冲区里,不会在堆上分配内存。这种情况下,移动构造std::string时,只能将原对象内部缓冲区的内容复制到新对象的内部缓冲区,因此两者的缓冲区地址必然不同。但这种拷贝是对象内部栈内存的拷贝,开销远小于堆内存拷贝,不属于我们要避免的“内存拷贝”范畴。string_view输出异常的原因
string_view本质是保存原字符串的字符指针和长度,不会持有内存所有权。当原std::string是SSO状态时,移动构造会复制内部缓冲区的内容到新对象,同时原对象的内部缓冲区可能会被修改(比如VS的实现会把原字符串的第一个字符置空),导致string_view指向的原缓冲区内容被破坏,从而输出异常。如果是堆上的长字符串,移动后原对象的堆指针会被置空,此时string_view会指向悬空内存,行为是未定义的,风险更高。实现无拷贝移动构造的场景
当字符串长度超过SSO阈值时,std::string会在堆上分配内存存储字符。此时移动构造仅会将原对象的堆指针、长度、容量等成员变量转移给新对象,原对象的指针会被置空,完全不会拷贝堆上的字符数据,这就是真正的无拷贝移动。你可以用更长的字符串测试,比如:string a = string(20, 'x'); // 长度超过SSO阈值 string b = std::move(a); cout << (const int*)a.c_str() << endl; // 原a的指针已置空,输出空地址或随机值 cout << (const int*)b.c_str() << endl; // 地址与原a的初始地址相同
内容的提问来源于stack exchange,提问作者Zhang

