侯捷 c++内存管理学习总结笔记。
在C++中,有几种常用的内存分配工具可以帮助进行动态内存管理。
从c++应用程序自上而下,通常会有这样的几种分配内存的方式,当然最终都是直接或间接的调用系统的API。
new
和delete
:new操作符用于在堆上分配内存,delete操作符用于释放先前分配的内存。它们是最基本的内存分配工具,在C++中非常常见。
例如,使用new操作符分配单个对象的内存:
string* str= new string("123");
使用delete操作符释放内存:
delete str;
从源码来看string* str= new string("123");
,这句代码的动作分为三步:
operator new
是一个全局函数,后面会介绍。
1 void* mem = operator new(sizeof(string)); //alloc
2 string* str= static_cast<string*>(mem); //cast
3 pStr->string::string("123");//ctor
出于好奇,我们能否直接使用第三步呢?
class A
{
public:
int id;
A() : id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }
A(int i) : id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }
~A() { cout << "dtor. this=" << this << " id=" << id << endl; }
};
void test_call_ctor_directly()
{
string* pstr = new string;
cout << "str= " << *pstr << endl;
//! pstr->string::string("jjhou");
//[Error] 'class std::basic_string' has no member named 'string'
//! pstr->~string(); //crash -- crash正确, crash 只因上一行被注释
cout << "str= " << *pstr << endl;
//------------
A* pA = new A(1); //ctor. this=000307A8 id=1
cout << pA->id << endl; //1
//! pA->A::A(3); //in VC6 : ctor. this=000307A8 id=3
//in GCC : [Error] cannot call constructor 'A::A' directly
//! A::A(5); //in VC6 : ctor. this=0013FF60 id=5
// dtor. this=0013FF60
//in GCC : [Error] cannot call constructor 'A::A' directly
// [Note] for a function-style cast, remove the redundant '::A'
cout << pA->id << endl; //in VC6 : 3
//in GCC : 1
delete pA; //dtor. this=000307A8
}
从测试用例来看,直接使用第三步这种方式调用构造,在某些编译器标准中是可行的。
new[]
和delete[]
:与new和delete类似,new[]操作符用于在堆上分配数组的内存,delete[]操作符用于释放先前分配的数组内存。
例如,使用new[]操作符分配一个整型数组的内存:
int* arr = new int[10];
使用delete[]操作符释放数组内存:
delete[] arr;
当然这里需要注意的是析构的顺序和构造是相反的,构造是从从下标0 1 2开始, 而析构则是从下标2 1 0开始。
operator new
是 C++ 中的一个操作符和函数,用于在堆上分配内存。它是一个全局的分配函数,可以根据需要进行重载和自定义。
operator new
的基本语法如下:
void* operator new (std::size_t size);
这会分配 size
字节大小的内存,并返回指向分配内存的指针。如果分配失败,operator new
会抛出一个 std::bad_alloc
异常。
以下是一些常见用法:
int* num = new int; // 使用 new 运算符分配一个整数对象的内存
上述代码等效于以下使用 operator new
的操作:
int* num = static_cast<int*>(operator new(sizeof(int))); // 手动调用 operator new 分配内存
需要注意的是,使用 operator new
进行内存分配后,必须手动调用相应的析构函数来销毁对象,并使用 operator delete
进行内存释放。
delete num; // 释放之前由 new 运算符分配的整数对象的内存
对于数组的分配和释放,可以使用 operator new[]
和 operator delete[]
。
int* arr = new int[10]; // 使用 new 运算符分配一个包含 10 个整数的数组的内存
delete[] arr; // 释放之前由 new 运算符分配的整数数组的内存
同样,上述代码等效于以下使用 operator new[]
和 operator delete[]
的操作:
int* arr = static_cast<int*>(operator new[](10 * sizeof(int))); // 手动调用 operator new[] 分配内存
operator delete[](arr); // 手动调用 operator delete[] 释放内存
需要注意的是,operator new
和 operator new[]
通常在内部被 new
和 new[]
运算符隐式调用,我们可以通过重载它们来实现自定义的内存分配行为或内存池的使用。
这里补充一个问题:
operator new源码
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{ // try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{ // report no memory
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
operator new
函数来分配内存。当operator new
函数无法分配足够的内存时,会抛出一个std::bad_alloc
异常。如果你希望接管这个异常并进行自定义的处理,可以使用std::set_new_handler
函数来注册一个自定义的new-handler
。
下面是一个示例,展示了如何使用std::set_new_handler
来接管new
操作符的异常处理:
#include
#include
// 自定义的new-handler函数
void customNewHandler()
{
std::cout << "Allocation failed! Custom new-handler called." << std::endl;
// 这里可以进行一些处理,如释放一些内存资源或者记录日志等
throw std::bad_alloc(); // 抛出std::bad_alloc异常
}
int main()
{
std::set_new_handler(customNewHandler); // 注册自定义的new-handler
try
{
int* ptr = operator new int[1000000000000]; // 尝试分配一个非常大的内存块
// 在正常情况下,当内存分配成功时,这里将会执行
operator delete[] ptr;
}
catch (const std::bad_alloc& e)
{
std::cerr << "Caught exception: " << e.what() << std::endl;
// 这里可以进行进一步的处理,如恢复内存状态或者终止程序等
}
return 0;
}
在上述示例中,我们定义了一个名为customNewHandler
的自定义new-handler
函数。我们通过调用std::set_new_handler
将其注册为全局的new-handler
。当new
操作无法分配内存时,会自动调用该函数。
请注意,这种接管new-handler
的方式只会在使用new
操作符分配内存时生效,对于直接使用malloc
之类的函数是不起作用的。此外,应当谨慎使用自定义的new-handler
,确保它能够正确处理内存分配失败的情况,并适当进行异常处理。
“placement new”
是 C++ 中的一个特殊用法,它允许你在提供的内存地址上构造一个对象,通常情况下,使用 “new”
运算符会在堆上分配内存,并在分配的内存上构造对象。而 “placement new”
则允许你在给定的内存地址上进行对象的构造,而无需分配额外的内存。
下面是 “placement new”
的使用方式示例:
#include
class MyClass {
public:
MyClass(int val) : value(val) {
std::cout << "Constructor called for value: " << value << std::endl;
}
~MyClass() {
std::cout << "Destructor called for value: " << value << std::endl;
}
private:
int value;
};
int main() {
// 分配一块内存
void* memory = operator new(sizeof(MyClass));
// 使用 placement new 在给定的内存地址上构造对象
MyClass* obj = new (memory) MyClass(42);
// 使用对象
std::cout << "Value of the object: " << obj->getValue() << std::endl;
// 销毁对象,但不会释放内存
obj->~MyClass();
// 释放内存
operator delete(memory);
return 0;
}
在上述代码中,我们首先通过 operator new
分配了一块内存,然后使用 “placement new”
在该内存上构造了一个 MyClass 对象,并进行使用。最后,通过显式调用析构函数 obj->~MyClass()
销毁对象,然后用 operator delete
释放内存。
从源码中去看这句代码
// 分配一块内存
void* buf = operator new(sizeof(MyClass));
// 使用 placement new 在给定的内存地址上构造对象
MyClass* obj = new (buf) MyClass(42);
MyClass* obj = new (buf) MyClass(42);
可以转换为
void* mem = operator new(sizeof(MyClass),buf);
obj = static_cast<MyClass*>(mem);
obj->MyClass::MyClass(42);
这里发现operator new(与前面相比多了buf这个参数,这个函数的定义operator new(size_t,void * loc){return loc;}
也能找得到。这里`operator new直接return loc是合理的,没有分配内存,因为loc是已经分配好的。
所以不存在placement delete,因为placement new 没有分配内存。当然,有时候我们又想把placement new
对应的operator delete
称为placement delete,这都是自己对术语的理解。
这里补充下这两个operator new区别:
在 C++ 标准中,operator new
是负责动态分配内存的函数,其函数签名为 void* operator new(std::size_t)
, 在分配内存时会调用这个函数。而 operator new(size_t, void* loc)
的函数签名是用于 placement new
的,它用于在指定的内存位置构造对象,而不是进行内存分配。所以,这两个函数具有不同的功能和用途,并且无法直接对 operator new(size_t, void* loc)
进行重载。
需要注意的是,使用 “placement new”
时需要手动管理对象的生命周期,包括显式调用析构函数和释放内存。因此,这种用法更加底层和高级,需要确保正确的使用方式以避免内存泄漏和未定义行为。
补充
array new + placement new 示例
class A
{
public:
int id;
A() : id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }
A(int i) : id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }
~A() { cout << "dtor. this=" << this << " id=" << id << endl; }
};
void main()
{
A* buf = (A*)(new char[sizeof(A)*size]);
A* tmp = buf;
cout << "buf=" << buf << " tmp=" << tmp << endl;
for(int i = 0; i < size; ++i)
new (tmp++) A(i); //3次 ctor
cout << "buf=" << buf << " tmp=" << tmp << endl;
//! delete [] buf; //crash. why?
//因为这其实是 char array,看到 delete [] buf;编译器会企图唤起多次 A::~A.
// 但 array memory 布局中找不到与 array 元素个數相关的信息,
// -- 整个格局都错乱 (从我对 VC 的认识而言),於是崩潰。
delete buf; //dtor just one time, ~[0]
cout << "\n\n";
}
从我的理解来看,buf这里是一块内存,假如size是3,我们基于这块内存去创建了A(0)对象,基于buf+1这块内存创建了A(1)对象,基于buf+2这块内存创建了A(2)对象,所以这里并不矛盾,delete[]和new[]要配对,但前提是他们的操作对象是同一个,说白了我们并没有使用new A[3],自然不能delete []. 即使我们间接通过placement new使得这三个对象是连续的。
所以这里我们实际上只开辟了buf指向这个内存,delete buf回收即可。但delete只是释放了A(0),A(1)对象和A(2)对象还在吗?当然是没有了,一切都是基于内存创建的,内存没了,自然就没了。
malloc和free:malloc函数用于在堆上分配指定大小的内存,free函数用于释放先前分配的内存。这是C语言中常用的内存分配工具,在C++中也可以使用。
例如,使用malloc函数分配内存:
int* num = (int*)malloc(sizeof(int));
使用free函数释放内存:
free(num);
需要注意的是,使用malloc分配的内存需要强制类型转换为目标指针类型。
malloc
是C语言中用于动态内存分配的函数,它的底层实现原理涉及操作系统和C运行时库的内存管理。
内存分配方式:malloc
通过调用操作系统的系统调用(如brk
或mmap
)来获取一段连续的虚拟内存空间。这段内存空间通常是以页为单位进行分配的,一般大小为4KB或更大。操作系统会将这段连续的虚拟内存映射到进程的地址空间。
内存块管理:为了管理已分配和未分配的内存块,C运行时库会维护一个数据结构(如堆或链表)。这个数据结构用于跟踪可用的内存块和已分配的内存块。当执行malloc
时,C运行时库会在内存块中找到一个适合大小的空闲块。
内存对齐:为了满足特定类型的内存对齐要求,malloc
分配的内存块通常会进行对齐处理。对齐是指将内存地址调整为特定的倍数,以保证数据在内存中的存储和访问效率。通常情况下,对齐方式和大小由编译器和操作系统决定。
内存分配策略:malloc
采用了一些内存分配策略来提高性能和空间利用率。例如,它可能会使用不同的内存分区或堆分配算法,如“首次适应”、“最佳适应”或“worst-fit”。这些算法的目标是在满足分配请求的情况下,尽可能地减少内存碎片和提高分配效率。
总体来说,malloc
的底层实现是由操作系统和C运行时库共同完成的。操作系统负责分配虚拟内存,而C运行时库负责管理已分配和未分配的内存块。malloc
函数的目标是提供简单而高效的内存分配功能,以便程序员能够灵活地进行动态内存管理。后续章节会详细介绍。
在msvc标准中, std::allocator:std::allocator是C++标准库中提供的内存分配器。它是一个泛型类模板,可以用于分配任何类型的内存。
使用std::allocator分配内存:
std::allocator<int> alloc;
int* num = alloc.allocate(1);
使用std::allocator释放内存:
alloc.deallocate(num, 1);
std::allocator提供了更高级的内存管理功能,例如构造和销毁对象,可以在需要时自动调用相应的构造函数和析构函数。当然不同的平台可能接口有所不同,例如 gnu和msvc就有所差异。
在gnu中:
void* p= alloc::allocate(512);
alloc::deallocate(p,512);
除了上述工具,还可以使用自定义的内存分配器来管理内存,通过重载new和delete运算符,或实现自己的内存池等高级技术来实现。这些自定义的工具可以根据特定应用的需求和场景来提供更灵活和高效的内存管理方式。后续章节会详细介绍。