C++ 调整std::string的大小时,字符缓冲区会发生什么变化?
背景: <>为了更好地了解C++中的内存管理,我最近决定编写自己的内存管理库。 目前,该库除了一个基本池分配器之外,没有多少其他组成,整个池分配器很小,可以内联到下面:C++ 调整std::string的大小时,字符缓冲区会发生什么变化?,c++,string,memory,memory-management,C++,String,Memory,Memory Management,背景: 为了更好地了解C++中的内存管理,我最近决定编写自己的内存管理库。 目前,该库除了一个基本池分配器之外,没有多少其他组成,整个池分配器很小,可以内联到下面: PoolAllocator::PoolAllocator (const AllocatorConfig& config) { this->nextAddress = 0; this->size = config.AllocatorSize; this->memoryArray = n
PoolAllocator::PoolAllocator (const AllocatorConfig& config)
{
this->nextAddress = 0;
this->size = config.AllocatorSize;
this->memoryArray = new byte[this->size];
}
PoolAllocator::~PoolAllocator ()
{
delete[] this->memoryArray;
}
void * PoolAllocator::Allocate (size_t size)
{
void* pointer = &(this->memoryArray[this->nextAddress]);
this->nextAddress += (unsigned long)size;
return pointer;
}
void PoolAllocator::Delete (void * object)
{
this->nextAddress -= sizeof (object);
}
然后,我通过替换全局新的来使用分配器(如果它存在,否则它只使用malloc,这是构造分配器本身时所调用的)
问题:
在测试此类时,我编写了一个简单的应用程序来分配std::string对象,如下所示:
#include <string>
#include <iostream>
#include "PoolAllocator.hpp"
#include "AllocatorConfig.hpp"
PoolAllocator *allocator;
int main (int numArgs, char *args[])
{
AllocatorConfig config = AllocatorConfig ();
config.AllocatorSize = 2048;
allocator = new PoolAllocator (config);
std::string *s = new std::string();
std::cout << "String object size: " << sizeof (*s) << std::endl;
*s = "test";
char *charBuffer = &((*s)[0]);
std::cout << "First allocate offset: " << (int)((int)&((*s)[0]) - (int)s) << std::endl;
std::cout << "Original cstring: " << charBuffer << std::endl;
*s = "thisIsALongStringToTestTheAddress";
std::cout << "Second allocate offset: " << (int)((int)&((*s)[0]) - (int)s) << std::endl;
std::cout << "Original cstring: " << charBuffer << std::endl;
delete allocator;
}
void * operator new(size_t size)
{
if (allocator)
{
return allocator->Allocate(size);
}
else
{
return malloc (size);
}
}
考虑到std::string在内部指向的string原语需要是连续的,而且std::string实际上无法知道它可以简单地扩展到相邻的连续空间,这一点一开始似乎相当合理
但是
在替换全局delete
和delete[]
并附加调试器时,我从未看到任何调用任何调试器的证据。我也试着在免费,但也没用,所以
问题:
std::string对那个内存做了什么?我本以为它会调用delete[]
,这样我的分配器就可以收回它,但据我所知,它根本没有释放它。它只是将堆喷洒到那个位置并一直保持到它死去,还是这里发生了什么我没有注意到的事情?您看到的是短字符串优化,其中字符串对象直接在其自己的分配中存储非常小的字符串,因此在重新分配时没有什么可取消分配的
我们进行了类似的测试,结果如下:
declaration
assignment of 'test'
assignment of 'thisIsALongStringToTestTheAddress'
allocate(34) = 0xb7ac30
assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'
allocate(133) = 0xb7ac60
deallocate(0xb7ac30, 34)
scope end
deallocate(0xb7ac60, 133)
要解释这一点:
- 分配
“测试”
时,没有分配
- 分配较长的字符串时,会有一个分配
- 当分配更长的字符串以强制重新分配时,会有另一个分配(用于新内容),然后是(旧内容的)解除分配,这是我们通常期望的顺序(因此,如果分配失败,我们可以在不破坏旧数据的情况下中止操作)
- 最后,当字符串被销毁时,我们看到一个释放
但是等等,为什么您没有看到分配的较长字符串的任何释放?嗯,你永远不会删除s代码>
,因此您的程序终止时会出现泄漏的分配,操作系统将自行清理。添加删除s代码>在删除分配器之前
并且您应该在操作符delete
中看到释放的分配
测试的源代码:
#include <iostream>
#include <string>
template <typename T, typename Allocator = std::allocator<T>>
class AllocatorProxy
{
private:
Allocator proxy;
public:
using pointer = typename Allocator::pointer;
using const_pointer = typename Allocator::const_pointer;
using value_type = typename Allocator::value_type;
using size_type = typename Allocator::size_type;
using difference_type = typename Allocator::difference_type;
pointer allocate(size_type n)
{
pointer p = proxy.allocate(n);
std::cout << "allocate(" << n << ") = " << static_cast<void *>(p) << '\n';
return p;
}
pointer allocate(size_type n, void const *l)
{
pointer p = proxy.allocate(n, l);
std::cout << "allocate(" << n << ", " << l << ") = " << static_cast<void *>(p) << '\n';
return p;
}
void deallocate(pointer p, size_type n) noexcept
{
std::cout << "deallocate(" << static_cast<void *>(p) << ", " << n << ")\n";
proxy.deallocate(p, n);
}
size_type max_size()
{
return proxy.max_size();
}
};
using astring = std::basic_string<char, std::char_traits<char>, AllocatorProxy<char>>;
int main() {
std::cout << "declaration\n";
astring str;
std::cout << "assignment of 'test'\n";
str.assign("test");
std::cout << "assignment of 'thisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddress");
std::cout << "assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress");
std::cout << "scope end\n";
return 0;
}
#包括
#包括
模板
类分配器代理
{
私人:
分配器代理;
公众:
使用指针=类型名分配器::指针;
使用常量指针=类型名分配器::常量指针;
使用value\u type=typename分配器::value\u type;
使用size\u type=typename分配器::size\u type;
使用差异类型=类型名称分配器::差异类型;
指针分配(大小\类型n)
{
指针p=代理分配(n);
std::cout您看到的是短字符串优化,其中字符串对象直接在其自己的分配中存储非常小的字符串,因此在重新分配时没有什么可取消分配的
我们进行了类似的测试,结果如下:
declaration
assignment of 'test'
assignment of 'thisIsALongStringToTestTheAddress'
allocate(34) = 0xb7ac30
assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'
allocate(133) = 0xb7ac60
deallocate(0xb7ac30, 34)
scope end
deallocate(0xb7ac60, 133)
要解释这一点:
- 分配
“测试”
时,没有分配
- 分配较长的字符串时,会有一个分配
- 当分配更长的字符串以强制重新分配时,会有另一个分配(用于新内容),然后是(旧内容的)解除分配,这是我们通常期望的顺序(因此,如果分配失败,我们可以在不破坏旧数据的情况下中止操作)
- 最后,当字符串被销毁时,我们看到一个释放
但是,等等,为什么您没有看到分配的较长字符串的任何解除分配?嗯,您从未删除s;
,因此您的程序终止时泄漏了一个分配,操作系统将自行清理。在删除分配器;
之前添加删除分配器;
,您应该看到分配器在操作符中释放时删除
测试的源代码:
#include <iostream>
#include <string>
template <typename T, typename Allocator = std::allocator<T>>
class AllocatorProxy
{
private:
Allocator proxy;
public:
using pointer = typename Allocator::pointer;
using const_pointer = typename Allocator::const_pointer;
using value_type = typename Allocator::value_type;
using size_type = typename Allocator::size_type;
using difference_type = typename Allocator::difference_type;
pointer allocate(size_type n)
{
pointer p = proxy.allocate(n);
std::cout << "allocate(" << n << ") = " << static_cast<void *>(p) << '\n';
return p;
}
pointer allocate(size_type n, void const *l)
{
pointer p = proxy.allocate(n, l);
std::cout << "allocate(" << n << ", " << l << ") = " << static_cast<void *>(p) << '\n';
return p;
}
void deallocate(pointer p, size_type n) noexcept
{
std::cout << "deallocate(" << static_cast<void *>(p) << ", " << n << ")\n";
proxy.deallocate(p, n);
}
size_type max_size()
{
return proxy.max_size();
}
};
using astring = std::basic_string<char, std::char_traits<char>, AllocatorProxy<char>>;
int main() {
std::cout << "declaration\n";
astring str;
std::cout << "assignment of 'test'\n";
str.assign("test");
std::cout << "assignment of 'thisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddress");
std::cout << "assignment of 'thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress'\n";
str.assign("thisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddressthisIsALongStringToTestTheAddress");
std::cout << "scope end\n";
return 0;
}
#包括
#包括
模板
类分配器代理
{
私人:
分配器代理;
公众:
使用指针=类型名分配器::指针;
使用常量指针=类型名分配器::常量指针;
使用value\u type=typename分配器::value\u type;
使用size\u type=typename分配器::size\u type;
使用差异类型=类型名称分配器::差异类型;
指针分配(大小\类型n)
{
指针p=代理分配(n);
std::cout*s=“test”
--如果您希望内存被分配,可能不是由于短字符串优化。*s=“test”
--如果您希望内存被分配,可能不是因为短字符串优化。非常有趣!感谢您的深入响应。我想部分让我困惑的是,短字符串缓冲区是单独分配的一部分,而不是初始新分配的一部分。短字符串是初始字符串alloca的一部分不过,这就是重点!是的,你是对的。我回去了,在std::string的VS 2017实现中达到了顶峰,并再次调试,我认为短字符串存储区域的单独分配实际上是一个类型为_Container_proxy的对象,我还没有完全了解它的用途另一方面,string literal在std::string对象的内存中,距离开始只有4个字节