C++ new & new operator
基于sgi-stl
new operator
new operator 是C++中在堆上构建对象的操作符,其语法如下:
ClassT* ptr = new ClassT;
其调用过程如下:
- 调用operator new, C++中提供了默认的operator new,调用malloc
- 在operator new返回的地址上调用构造函数, 这一步通过placement new实现
- 将operator new 申请的对象转换为ClassT* 并返回
operator new & operator delete
- 函数说明
- 成对出现的, new负责申请,delete负责释放
- 为new operator 分配(释放)内存
- 函数原型
static void* operator new( size_t count )
- 应用 :如通过重载operator new实现内存池: 预先分配一个固定大小的二维数组,调用operator new时,从数组中找一个没有使用的节点出来,使用完成之后再放回去。
构造对象
释放对象
- 代码示例
char m_buffer[4] = {0};
class Test{
public:
Test(int var) : m_testValue(var) {
std::cout << "Test constructor with initialize list called" << std::endl;
}
Test() { std::cout << "Test constructor called" << std::endl; }
~Test() { std::cout << "Test destructor called" << std::endl; }
static void* operator new(size_t count) {
std::cout << "operator new called" << std::endl;
return m_buffer;
}
static void operator delete(void* ptr) {
std::cout << "operator delete called" << std::endl;
}
public:
int m_testValue;
};
int main(int argc, char** argv){
Test* ptr = new Test;
std::cout << "ptr value = " << ptr->m_testValue << std::endl; // 0
Test* ptr2 = new Test(123);
std::cout << "ptr value = " << ptr->m_testValue << std::endl; // 12
delete ptr;
delete ptr2;
}
上述代码中,ptr ptr2实际是一个对象, 都是指向m_buffer
如果在实现过程中, 将m_buffer调整为一系列内存块数组,在每次调用operator new的时候从内存块中取不同的节点,那么就可以实现一个简单的内存池。
但是STL中没有采用这种实现方式,因为这种实现需要各个类重写operator new & operator delete
- operator new[] & operator delete[] 略,operator new & delete 的数组版
在指定位置调用构造函数
- placement new
- 函数说明:
- 这个函数是operator new的另外一个重载版本
- 作用是返回一个内存,便于在该内存出调用构造函数(placement的意思就是从这里来的,将构造函数 放置 在指定内存位置)
- 该函数并不执行内存分配相关事项,仅仅是返回一个指针,具体的内存管理有其他业务实现
- 该函数的调用缺省了第一个参数(见后面代码示例)
- 函数原型:
- 函数说明:
void* operator new(size_t count, void* ptr)
- 应用
通过***placement new***实现内存池比通过***operator new***要方便很多:- 由于placement new 只是在指定位置调用构造函数,故不需要重写(@override)
- placement new 将内存的申请和构造函数的调用分开了,具有较高的灵活性。
通过placement new实现内存池的流程如下:
- 代码示例
char* m_buffer;
void init() {
m_buffer = (char*)malloc(4);
memset(m_buffer, 0, 4);
}
/
class Test{
public:
int m_testValue;
Test(int var) : m_testValue(var) {
std::cout << "Test constructor with initialize list called" << std::endl;
}
Test() {
std::cout << "Test constructor called" << std::endl;
}
~Test() {
std::cout << "Test destructor called" << std::endl;
}
// placement new
static void* operator new(size_t count, void* ptr) {
std::cout << "placement new called" << std::endl;
return ptr;
}
};
/
int main(int argc, char** argv){
init();
// 调用placement new示例
Test* ptr = new(m_buffer) Test;
std::cout << "ptr value = " << ptr->m_testValue << std::endl;
Test* ptr2 = new(m_buffer) Test(12);
std::cout << "ptr value = " << ptr->m_testValue << std::endl;
// placement new对象的析构
ptr2->~Test();
ptr1->~Test();
}
- 由于placement new并不直接申请内存,它只是将构造函数 放置 在指定位置,所以通过placement new构造出来的对象不能调用delete, 只能显式调用析构进行销毁
- STL 中使用了placement new实现内存池的方法:
- 将内存的申请与对象的构造分为两步, stl_alloc & stl_construct, 其中stl_alloc负责内存管理, stl_construct负责调用构造及析构
- stl_alloc:
- 一级构造器, 直接调用malloc & free进行内存管理, 适用于大内存(>128字节的对象的申请)
- 二级构造器, 维护一个数组,数组中的node为一个链表,如下表所示:
- stl_construct:
- 调用构造函数
- 调用析构函数
二级构造器
8字节 链表 | 16字节 链表 | … | 128字节 链表 |
---|---|---|---|
Node11 | Node21 | … | Nodek1 |
Node12 | Node22 | … | Nodek2 |
… | … | … | … |
- 关于STL的一些说明
- STL 在执行insert 或者 push等函数时,会调用operator = 或者 拷贝构造函数,所以STL的插入操作实际上是一个复制对象的过程
- STL插入操作的具体过程如下:
- 调用stl_alloc,申请一段能够容纳当前对象的内存(从一级构造器或者二级构造器)
- 调用stl_construct,通过placement new调用拷贝构造函数(或operator=,要看具体的实现)
- 将申请出来的对象插入到容器本身的数据结构中(如list的双向循环链表、map的rb-tree、unorderd_map的hash_table中)
- STL释放操作过程:
- 调用stl_construct,通过调用析构释放类中申请的对象
- 调用stl_alloc,将对象本身的内存释放(一级构造器或者二级构造器)
- 从容器本身的数据结构中弹出对象
- STL中的数据安全性:
- deep copy & shallow copy,关于深拷贝和浅拷贝主要是在拷贝构造函数或者是operator=中实现,在设计中需要注意
- STL中会保留对象的副本,如果是将指针放入到容器中需要注意不要在外部将指针删除,如:
std::list<Test*> testLst;
将Test* 对象放入容器这里并不会调用Test的拷贝构造(原因是类型是Test* )之后,直接delete这个 Test* 对象将会导致testLst中存放了一个野指针。