1.new() new是用于创建对象的函数,它通过调用operator new()来完成空间的分配,通过parameter new将对象在已分配的空间中创建。
2.operator new() 全局函数,它返回一个void*型的指针,该指针指向分配到的内存,operator new()通过调用malloc获得内存。
3.parameter new 它的使用方式为new(pointer)className(parameter),该方法没有分配空间,它的pointer参数指向一段已经分配的内存,并调用构造函数(有参或无参)在pointer指向的内存中创建对象。
由该过程可以看出,内存分配最终是由malloc完成的, 构造函数是通过调用parameter new 来调用的。
我们通过实际创建一个对象来加深对调用过程的理解:
当创建一个对象时,我们一般使用如下格式:
Foo* f = new Foo(3, "asdf");
该语句将通过new方法获得对象,new方法中包括如下操作(即可用如下操作代替new来创建对象):
//1通过operator new开辟空间获得一个指针,此时指针类型为void*
//operator new相当于一个函数,它会调用malloc来开辟空间
void* p = operator new(sizeof(Foo));
//2将获得的void*类型指针转换为可以使用的Foo*类型
Foo* f = (Foo*)p;
//3通过new(指针) structureFunction() 调用构造函数完成构造
new(f)Foo(2, "qwe");
其中operator new过程将调用malloc,malloc通过sizeof(Foo)分配合适大小的空间,并将地址返回。
当然除了new,我们还有new[], 它将进行多次操作,即每次都通过operator new获得空间,再通过parameter new在空间中创建对象。值得注意的是,在new[]获得的空间中,额外有两个叫做cookie的空间(分别位于分配到的空间的首尾),cookie的作用是:当调用delete[]时,可以通过cookie知道该调用多少次析构函数。
1 delete delete过程中,将首先调用析构函数完成对象在销毁之前的一些工作,比如需要对该对象所指向的那些空间进行销毁,然后再调用operator delete对对象本身空间进行销毁。
2 operator delete() 全局函数, operator delete将调用free完成空间释放
同样的,我们通过实际销毁一个对象来加深对调用过程的理解:
对象销毁时,我们一般使用如下格式:
delete f;
该语句通过delete方法完成对象的销毁,其操作分为以下两步(即可用如下操作代替delete来销毁对象):
f->~Foo();
// //2调用free释放本身所占用的内存
operator delete(f);
首先调用析构函数对完成对象销毁前的善后工作,再调用operator delete释放本身的空间。
而对于使用new[]创建的对象,对应使用delete[], 如下所示:
Foo * f3 = new Foo[4];
delete[] f3;
其中的delete[]将多次调用delete完成数组对象的销毁过程,到底调用多少次?这就是cookie存在的意义,前面已经提到,cookie就是在用new[]创建对象时,额外添加的用于标识数组对象大小的内存空间,cookie将帮助delete[],告诉它需要调用多少次delete,以正确释放分配到的所有空间。这里常常被提到的一点是,对于使用new[]创建的对象,必须使用delete[]进行销毁,否则将有可能产生内存泄漏的麻烦。应该说,麻烦不在于对象的内存是否会泄漏(因为对象本身内存的销毁有cookie帮助),而在于数组对象中的对象的指针指向的空间,在使用delete[]时,将多次调用析构函数,析构函数将对对象(数组对象中的每个对象)中的指针指向的空间进行释放。如果通过delete释放new[]对象,将产生的结果是,delete只调用了一次析构函数,即数组对象中只有一个对象的指针指向的空间被释放,其他对象的指针指向的空间未被释放,然后就将数组对象本身释放掉了,那些“其他对象的指针指向的空间”就是内存泄漏的东西。
那也就是说,是不是只要对象中没有指针(或引用类型、其他对象类型),就可以通过delete释放new[]创建的对象?的确如此,因为不需要调用析构函数做善后处理,这样是不会产生内存泄漏的,但这样将使代码“不规范”,我们寻求的是一种通用的代码格式,而不是“对于new[]产生的没有指针的对象使用delete,对于new[]产生的有指针的对象使用delete[]”这种分不同情况的,难以把握的格式。
我们已经知道,new的过程将调用operator new获得空间,再调用parameter new 在空间中创建对象。在这个里,每次new都会调用operator new和parameter new ,而每次调用operator new都会调用一次malloc来申请内存。有没有什么方法可以批量申请内存?这样不必每次都调用malloc申请内存,只需要调用parameter new 创建对象即可。这是可行的,可以通过重载operator new实现。重载operator new,接管内存分配函数,这就是内存管理的内容。重载operator new,使用内存池,不仅使malloc的调用次数减少,还去掉了cookie,减少了内存碎片,是具有很好的时间和空间效益的。
我们通过如下实例,介绍内存管理的过程:
#include
#include
using namespace std;
class Screen {
public:
Screen(int x) :i(x) {
};
int get() {
return i; }
//operator new和operator delete默认为static,这里可以不写static
static void* operator new(size_t);
static void operator delete(void*, size_t);
private:
Screen* next;//用于形成链
int i;
private:
static Screen* freeStore;//用于指向内存池(一条链表)
static const int screenChunk;
};
Screen* Screen::freeStore = 0;
const int Screen :: screenChunk = 24;//默认为内存池分配24个单位
//重写operator new方法,该方法在new一个对象的过程中被调用,如果对operator new进行重载,第一个参数必须为size_t
//调用过程中,如果freeStore为空,为其开辟空间,然后将让指针指向当前空间链的第一个元素,并将空闲指针后移
void* Screen::operator new(size_t size) {
Screen* p;
if (!freeStore) {
//linked list 是空的,所以申请一大块空间
size_t chunk = screenChunk * size;//大小为24(screenChunk)块
//获取一大块空间
freeStore = p = reinterpret_cast<Screen*>(new char[chunk]);
//将一大块分割为每一片,将每一片当作linked list 串接起来
//这里p是一个Screen对象,每次加1获得下一个Screen的首地址
for (; p != &freeStore[screenChunk - 1]; ++p)
p->next = p + 1;
p->next = 0; //最后一个Screen对象的next 指向0
}
//取下一个空间给刚创建的对象
p = freeStore;
freeStore = freeStore->next;
return p;
}
void Screen::operator delete(void* p, size_t) {
//将deleted object 插回free list 前端
(static_cast<Screen*>(p))->next = freeStore;
freeStore = static_cast<Screen*>(p);
}
//使用我们的operator new 和operator delete每个间隔为8为一个int和一个指针的内存,表明没有cookie
// 但如果将我们的operator new 和operator delete等相关代码注释掉,系统调用默认operator new,获得的地址将是不连续的
int main() {
cout << sizeof(Screen) << endl;
const int N = 100;
Screen* p[N];
for (int i = 0; i < N; i++)
p[i] = new Screen();
//输出结果,地址之间间距为8,表明不存在cookie
for (int i = 0; i < 10; i++)
cout << p[i] << endl;
for (int i = 0; i < N; i++)
delete p[i];
return 0;
}
我们在Screen 类中通过重写operator new和operator delete完成了简单的内存管理,以后使用new创建Screen 对象和使用delete销毁Screen 对象时,在本该调用全局函数operator new和operator delete的地方将调用如上重写的operator new和operator delete。通过main测试用例,在输出结果中每个地址距离为8,正好为类成员中int和Screen*的大小,表明每个对象以及没有前后cookie了(但是整个单链表前后有cookie,即24个连续对象的连续内存的前后有cookie)。
实例1中,虽然去掉了cookie,但是却因为使用了指针导致内存有效利用率降低。以下实例将对这一点做出改善:
#include
using namespace std;
class Airplane {
private :
//这个结构体将因内存对齐而每个元素占8bit
struct AirplaneRep {
unsigned long miles;//4bit
char type;//1bit
};
private:
//通过union ,内存块在空闲队列中时,将其看作Airplane*,取出来使用时,则将其看作AirplaneRep
union {
AirplaneRep rep;
Airplane* next;
};
public://部分无关函数
unsigned long getMiles() {
return rep.miles; }
char getType() {
return rep.type; }
void set(unsigned long m, char t) {
rep.miles = m; rep.type = t;
}
public:
static void* operator new(size_t size);
static void operator delete(void* deadObject, size_t size);
private:
static const int BLOCK_SIZE;
static Airplane* headOfFreeList;
};
Airplane* Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;
void* Airplane:: operator new(size_t size) {
//如果大小有误,交给全局operator new。发生继承时可能会出错
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane* p = headOfFreeList;
//如果p有效,就把list后移一个元素
if (p)
headOfFreeList = p->next;
else {
//free list 已空,申请一大块空间,这里使用全局operator new来申请
Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));
//将这一块空间串成free list
//但跳过第#0,因为它被用作本次分配。
for (int i = 1; i < BLOCK_SIZE - 1; ++i)
newBlock[i].next = &newBlock[i + 1];
newBlock[BLOCK_SIZE - 1].next = 0;//最后一个指向空
//p指向#0
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
void Airplane::operator delete(void* deadObject, size_t size) {
if (deadObject == 0)return;
if (size != sizeof(Airplane)) {
::operator delete(deadObject);
return;
}
Airplane* carcass = static_cast<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
int main() {
cout << sizeof(Airplane) << endl;
const int N = 100;
Airplane* p[N];
for (int i = 0; i < N; i++)
p[i] = new Airplane();
//输出结果,地址之间间距为8,表明不存在cookie
for (int i = 0; i < 10; i++)
cout << p[i] << endl;
for (int i = 0; i < N; i++)
delete p[i];
return 0;
}
主要通过使用union做了改进,将next和对象分别作为union的一项,使得在空闲队列中和在使用时同一段内存具有不同的含义。其他部分基本相同,只是分配空间从实例1中的new改为了全局operator new 。
在实际运用中,我们当然不应该在每个class中都重写operator new和operator delete ,为此,我们可以创建一个类,专门用于内存管理,在需要使用该内存管理的类中,只需要定义一个该类的静态成员即可。如下所示:
#include
using namespace std;
class allocator {
private :
struct obj {
struct obj* next;
};
public:
void* allocate(size_t);
void deallocate(void*, size_t);
private :
obj* freeStore = nullptr;
const int CHUNK = 5;
};
//::allocator 明确使用当前自定义的allocator
void* ::allocator::allocate(size_t size) {
obj* p;
if (!freeStore) {
//当linked list为空时
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk);
//将分配的一大块用作listed list
for (int i = 0; i < (CHUNK - 1); ++i) {
p->next = (obj*)((char*)p + size);
p = p->next;
}
p->next = nullptr;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
void ::allocator::deallocate(void* p, size_t) {
//将p回收到free list 的前端
((obj*)p)->next = freeStore;
freeStore = (obj*)p;
}
//这样其他类就可以使用
class Foo {
public:
long L;
string str;
static ::allocator myAlloc;
public:
Foo() {
}
Foo(long l) :L(l) {
}
static void* operator new (size_t size) {
return myAlloc.allocate(size);
}
static void operator delete(void* pdead, size_t size) {
return myAlloc.deallocate(pdead, size);
}
};
::allocator Foo::myAlloc;
//从结果可以看出,每五个元素的空间是连续的
int main() {
cout << sizeof(Foo) << endl;
const int N = 100;
Foo* p[N];
for (int i = 0; i < N; i++)
p[i] = new Foo();
//输出结果,地址之间间距为8,表明不存在cookie
for (int i = 0; i < 20; i++)
cout << p[i] << endl;
for (int i = 0; i < N; i++)
delete p[i];
return 0;
}
这部分代码中,我们使用allocator 作为管理内存的类,而在实际运用中,通过Foo类中那样使用该allocator 即可,由于标准库中已经有allocator 类,所以我们在使用自己的allocator 时需要在前面加上::