C++中的std::string
字符串字面量
字符串字面量位于字面量池中,字面量池位于程序的常量区中
void show_address(const char* str) {
std::cout << reinterpret_cast<const void*>(str) << std::endl;
}
int main()
{
// 三者位于同一个地址上
show_address("Hello");
show_address("Hello");
show_address("Hello");
// C++中允许对字符串字面量取地址,即&"Hello" 得到的地址与上文相同
}
对于指针和数组,它们代表的含义不同
// pStr指针位于全局区中 指向位于常量区中的字符串字面量
const char* pStr = "Hello";
int main()
{
// strArr位于栈中 将数据从常量区拷贝到函数栈中
char strArr[] = "Hello";
}
std::string的内存分配
C++对std::string
的内部实现有如下约定
- 如果传入的字符串字面量小于某阈值,那么该
std::string
内部在栈上分配内存(即短字符串优化——SSO);如果大于指定的阈值,那么将会根据传入的字符串的尺寸,在堆上开辟相应的空间。不管是短字符串还是长字符串,在使用字符串字面量构建std::string
的时候,都会产生拷贝的操作 - 如果后续对
std::string
采用了“增”操作,那么将会采用double的形式进行扩容(双倍扩容)
在通常情况下,若数据的长度小于等于15(还有一位是'\0'
结束符),那么会采用短字符串优化(这主要取决于不同库的实现)
// MSVC中的实现
// length of internal buffer, [1, 16]:
static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);
std::string的结构
在MSVC-Release-x64的环境下,std::string
的大小是32B
using string = basic_string<char, char_traits<char>, allocator<char>>;
using _Alty = _Rebind_alloc_t<_Alloc, _Elem>;
using _Alty_traits = allocator_traits<_Alty>;
using _Scary_val = _String_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Simple_types<_Elem>,
_String_iter_types<_Elem, typename _Alty_traits::size_type, typename _Alty_traits::difference_type,
typename _Alty_traits::pointer, typename _Alty_traits::const_pointer, _Elem&, const _Elem&>>>;
_Compressed_pair<_Alty, _Scary_val> _Mypair;
std::string
采用std::allocator<char>
作为分配器,由_Compressed_pair
的EBO得,分配器并不会占用内存空间。该分配作用于std::_Is_simple_alloc_v<std::_Rebind_alloc_t<std::allocator<char>, char>>
为true
,因此std::string
的内存布局可以拆解如下
// std::string同一时间只可能是短字符串或长字符串
union _Bxty { // storage for small buffer or pointer to larger one
char _Buf[16];
char* _Ptr;
char _Alias[16]; // TRANSITION, ABI: _Alias is preserved for binary compatibility (especially /clr)
} _Bx;
std::size_t _Mysize = 0; // current length of string
std::size_t _Myres = 0; // current storage reserved for string
-
当
std::string
中记录的是短字符串时,_Buf
代表栈上的字符串,如"Hello World"
是存储在_Buf
数组中 -
当
std::string
中记录的是长字符串时,_Ptr
代表指向堆上数据的指针,可通过该指针访问数据
当我们调用c_str()
时,本质上是在调用如下方法
constexpr const value_type* _Myptr() const noexcept {
const value_type* _Result = _Bx._Buf;
// 判断是否是长字符串
if (_Large_string_engaged()) {
_Result = _Unfancy(_Bx._Ptr);
}
return _Result;
}
constexpr bool _Large_string_engaged() const noexcept {
#if _HAS_CXX20
// 判断当前函数调用是否发生在常量求值场合
if (std::is_constant_evaluated()) {
return true;
}
#endif // _HAS_CXX20
return _BUF_SIZE <= _Myres;
}
SSO与移动
若无特殊说明,本小节建立在MSVC-Release-x64的环境下进行分析,且源码在便于理解的基础上略有删减。std::string
在Debug和Release模式下内存分配的机理不同(Debug模式下无短字符串优化等)
// 如果是MSVC-Debug-x64环境 那么会在堆上分配2次16B的内存
std::string name = "Hello World";
std::string newName = std::move(name);
下面进行源码剖析
constexpr basic_string(basic_string&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t{}, _STD move(_Right._Getal())) // 标签分发
{
// 根据优化等级选择不同的分配器 在Release模式下取得_Fake_allocator 它是空类 不负责任何功能
_Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
// 拿走被移动对象中的数据
_Take_contents(_Right);
}
constexpr void _Take_contents(basic_string& _Right) noexcept {
// assign by stealing _Right's buffer
auto& _My_data = _Mypair._Myval2;
auto& _Right_data = _Right._Mypair._Myval2;
// We need to ask if pointer is safe to memcpy.
// size_type must be an unsigned integral type so memcpy is safe.
// _Elem must be trivial standard-layout, so memcpy is safe.
// We also need to disable memcpy if the user has supplied _Traits, since they can observe traits::assign and similar.
if constexpr (_Can_memcpy_val) {
#if _HAS_CXX20
if (!_STD is_constant_evaluated())
#endif
{
// 该宏判断优化等级 Release模式下_ITERATOR_DEBUG_LEVEL为0
#if _ITERATOR_DEBUG_LEVEL != 0
if (_Right_data._Large_string_engaged()) {
// take ownership of _Right's iterators along with its buffer
_Swap_proxy_and_iterators(_Right);
} else {
_Right_data._Orphan_all();
}
#endif
// memcpy右值字符串中的数据
_Memcpy_val_from(_Right);
// 将右值字符串置回默认状态
_Right._Tidy_init();
return;
}
}
// 下方代码处理 when is unsafe to memcpy 的情况
// Codes...
}
void _Memcpy_val_from(const basic_string& _Right) noexcept {
// 添加偏移量 使memspy正常工作
const auto _My_data_mem =
reinterpret_cast<unsigned char*>(std::addressof(_Mypair._Myval2)) + _Memcpy_val_offset;
const auto _Right_data_mem =
reinterpret_cast<const unsigned char*>(std::addressof(_Right._Mypair._Myval2)) + _Memcpy_val_offset;
// 对数据进行拷贝 Debug和Release模式的不同会导致偏移量不同 但最终拷贝的是同一份数据
::memcpy(_My_data_mem, _Right_data_mem, _Memcpy_val_size);
}
由于MSVC中对将存储数据的结构设计为union
,因此在::memcpy
的时候并不需要考虑是长字符串还是短字符串,直接对数据进行拷贝,然后再读取的时候进行判定即可即可(即上文中提到的_Myptr()
以及_Large_string_engaged()
)
过时的COW
std::string_view与const std::string&
对于std::string
而言,当它从一个原生的c-style-string上构造时,都伴随着内存分配(可能是堆也可能是栈);但对于std::string_view
而言,它内部只维护了一个原生指针和一个长度
const char* _Mydata;
std::size_t _Mysize;
这代表着std::string_view
在构造的时候,只是进行一次浅拷贝,同时进行一次O(n)复杂度的长度求值
constexpr basic_string_view(const char* _Ntcts) noexcept
: _Mydata(_Ntcts), _Mysize(_Traits::length(_Ntcts)) {}
const char* word = "Hello";
std::string_view sv1 = word;
std::string_view sv2 = word;
std::string s = word;
因此在对c-style-string进行操作时,为此构建一个std::string
是一个不值当的操作,我们需要的是一个“视图”,即std::string_view
Example1
std::string extract_part(const std::string& bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
// do something...
}
尽管编译器已经开启了RVO,但上述代码仍然包含了两次std::string
对象的构造,若检测的字符串是长字符串,那么这代表着高额的性能开销
std::string_view extract_part(std::string_view bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
// do something...
}
Problem1
但由于std::string_view
执行的是浅拷贝,所以也伴随着dangling的问题
std::vector<std::string_view> elements;
// 若elem的生命周期短于elements 那么可能会访问到已经被释放的内存
void Save(const std::string& elem) {
elements.push_back(elem);
}
Problem2
std::map<std::string, int> frequencies;
int GetFreqForKeyword(std::string_view keyword) {
// 无法通过编译 不存在std::string_view到std::string的隐式转换
return frequencies.at(keyword);
}
Problem3
class Sink
{
public:
Sink(std::string_view sv) : str(std::move(sv)) {}
private:
std::string str;
};
- 对一个
std::string_view
,而言,std::move
它是无害但无用的 - 以
std::string_view
去构造std::string
,存在sv
在构建时内部指针悬空的风险
总结
- 考虑使用
std::string_view
代替const std::string&
- 函数传参按值传递
std::string_view
即可,不需要pass-by-const-reference,也没有移动操作
手撕简易my_string
class my_string
{
protected:
std::size_t size;
char* pStr;
void init_null_impl() {
size = 0;
pStr = new char[1]{'\0'};
}
void init_impl(const char* newData) {
size = std::strlen(newData);
pStr = new char[size + 1];
strcpy_s(pStr, size + 1, newData);
}
void str_swap(my_string& _another) {
std::swap(pStr, _another.pStr);
std::swap(size, _another.size);
}
public:
my_string() {
init_null_impl();
}
my_string(const char* newData) {
if (newData == nullptr)
init_null_impl();
else
init_impl(newData);
}
my_string(const my_string& _copy) {
if (_copy.pStr == nullptr)
init_null_impl();
else
init_impl(_copy.pStr);
}
my_string(my_string&& _another) : size(_another.size), pStr(_another.pStr) {
_another.init_null_impl();
}
my_string& operator=(my_string _another) {
str_swap(_another);
return *this;
}
~my_string() {
delete[] pStr;
}
char operator[](std::size_t index) const { return pStr[index]; }
const char* c_str() const { return pStr; }
};