C++ 内存管理
侯捷老师的课程内容,做了个简单的记录
C++应用程序,使用memory的途径
SGI STL G2.9 用的allocate (SGI) 是直接调用的malloc(),图有些差错
C++ memory primitives
分配 | 释放 | 属性 | 是否重载 |
---|---|---|---|
malloc() | free() | C函数 | 不可 |
new | delete | C++表达式 | 不可 |
::operator new() | ::operator delete() | C++函数 | 可 |
alloctor |
alloctor |
C++标准库 | 可自由设计并以之搭配任何容器 |
new expression
delete expression
在malloc为用户分配内存的时候,除了分配用户本身的内存,还会在内存前后加上两个cookie,来记录分配了多少内存,这样在调用free函数的时候才能准确的回收内存。因此每次调用malloc函数都会产生cookie消耗。
而new操作符中的array new,为了记录需要调用多少次析构函数,会在分配的内存前记录分配了多少个对象。下面的图更好理解一点:
malloc和new的内存分布布局
上图展示了我们在运行代码示例一之后分配的内存,其中61H是malloc设置的cookie,表示malloc总共分配给用户的内存大小;而00481c30地址所指的3,则是new设置的其分配对象的个数。也就是说,每次调用new操作符的时候,都会分配一些额外的内存来存放所分配内存信息。
对于 Complex 是POD类型, delete 调用3次和1次没区别
string类 delete调用少了,会发生泄漏
首先来看下面这段代码:
// 代码示例二
#include
using namespace std;
class Demo
{
private:
int a, b, c;
public:
Demo() : a(0), b(0), c(0)
{
cout << "constructor" << endl;
}
~Demo()
{
cout << "destructor" << endl;
}
};
int main()
{
Demo *p = new Demo[3];
delete[] p;
return 0;
}
/*
输出:
constructor
constructor
constructor
destructor
destructor
destructor
*/
上面的代码示例二展示了array new和array delete的用法,可以看到,其中调用了3次构造函数和3次析构函数。如果我们将delete[] p改为delete p,结果会是什么样呢?
将delete[]改为delete
将delete[] p改为delete p之后,会出现上图的错误,invalid pointer。为什么会出现这种情况呢,前面我们提到过,在使用new操作符分配一个数组时,会在分配的数组前面多分配几个字节(视环境而定侯捷老师说是4个字节,但是在我的环境下面是8个字节),再来看一下前面那个图:
我们在运行了代码示例2之后,实际分配的内存如上图所示(仅仅是为了说明,地址并不准确)。在运行Demo *p =new Demo[3]之后,返回的指针p是0x00481c34,在00481c34之前还有用来存放对象数量的内存,4个字节或者8个字节,图中展示为8个字节。
在调用delete[] p的时候,会调用operator delete[]函数,而传入operator delete[]函数的指针其实是从0x00481c30开始的,并不是从对象真正的地址开始,因为new[]申请的内存是从0x00481c30开始的。
注意﹐開於“placement new”,
或指new§
或指::operator new(size, void*)
C++ 应用程序,分配内存的途径
C++ 容器,分配内存的途径
重载 new() / delete()
我们可以重截class member operator new()﹐寓出多佃版本﹐前提是每一版本的馨明都必须有嗳特的参敷列﹐其中第一参数必须是size_t,其余参数以new所指定的placement arguments爲初值·出现於 new …小括虢内的便是所谓placement arguments 。
我们也可以重戴class member operator delete(),写出多个版本·但它绝不会被delete 调用。只有常new所调用的ctor抛出exception,才会调用这些重载的operator delete()·它只可能被这样调用﹐主要用来归还未能完全创建成功的 object所占用的memory 。
例子
想利用类内重载operator new去接管内存的分配,然后利用内存池的观念【即创建出一大段连续空间的内存,然后将其切割成一小段一小段】,将创建的元素对象放在内存池切分好的各分段小内存片中,这样避免了多次调用new而造成生成多个带有cookie的内存空间。通过内存池的观念,可以生成一大段只带有两个头尾cookie的内存空间,而该一大段内存空间又被切分成每一小段的内存空间,且其中的每一小段内存空间片都可以共享这一整体的cookie信息。
per-class allocator 1
因为为了能将一大段内存空间切分成一小段一小段,然后通过单向链表的形式串接起来,所以必须多引入一个Screen* next指针。但这又会增加class Screen的大小【增加了4字节】。
另外,static Screen* freeStore 和 static const int screenChunk是静态成员变量,是声明class Screen就创建出来的了【而且被其所有类对象共享的】,并不是在创建每一个Screen类对象时才被创建出来,因而并不计算入类对象的大小中【所以生成的每一个类对象其大小为4+4=8】。
#include
#include
namespace jj04
{
//ref. C++Primer 3/e, p.765
//per-class allocator
class Screen {
public:
Screen(int x) : i(x) { };
int get() { return i; }
void* operator new(size_t);
void operator delete(void*, size_t); //(2)
//! void operator delete(void*); //(1) 二擇一. 若(1)(2)並存,會有很奇怪的報錯 (摸不著頭緒)
private:
Screen* next;
static Screen* freeStore;
static const int screenChunk;
private:
int i;
};
Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;
void* Screen::operator new(size_t size)
{
Screen *p;
if (!freeStore) {
//linked list 是空的,所以攫取一大塊 memory
//以下呼叫的是 global operator new
size_t chunk = screenChunk * size;
freeStore = p =
reinterpret_cast(new char[chunk]);
//將分配得來的一大塊 memory 當做 linked list 般小塊小塊串接起來
for (; p != &freeStore[screenChunk-1]; ++p)
p->next = p+1;
p->next = 0;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
//! void Screen::operator delete(void *p) //(1)
void Screen::operator delete(void *p, size_t) //(2)二擇一
{
//將 deleted object 收回插入 free list 前端
(static_cast(p))->next = freeStore;
freeStore = static_cast(p);
}
//-------------
void test_per_class_allocator_1()
{
cout << "\ntest_per_class_allocator_1().......... \n";
cout << sizeof(Screen) << endl; //8
size_t const N = 100;
Screen* p[N];
for (int i=0; i< N; ++i)
p[i] = new Screen(i);
//輸出前 10 個 pointers, 用以比較其間隔
for (int i=0; i< 10; ++i)
cout << p[i] << endl;
for (int i=0; i< N; ++i)
delete p[i];
}
} //namespace
-----------------------------------------------
per-class allocator 2
//----------------------------------------------------
#include
#include
namespace jj05
{
//ref. Effective C++ 2e, item10
//per-class allocator
class Airplane { //支援 customized memory management
private:
struct AirplaneRep {
unsigned long miles;
char type;
};
private:
union {
AirplaneRep rep; //此針對 used object
Airplane* next; //此針對 free list
};
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 已空。配置一塊夠大記憶體,
//令足夠容納 BLOCK_SIZE 個 Airplanes
Airplane* newBlock = static_cast
(::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; //以null結束
// 將 p 設至頭部,將 headOfFreeList 設至
// 下一個可被運用的小區塊。
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
// operator delete 接獲一塊記憶體。
// 如果它的大小正確,就把它加到 free list 的前端
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(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
//-------------
void test_per_class_allocator_2()
{
cout << "\ntest_per_class_allocator_2().......... \n";
cout << sizeof(Airplane) << endl; //8
size_t const N = 100;
Airplane* p[N];
for (int i=0; i< N; ++i)
p[i] = new Airplane;
//隨機測試 object 正常否
p[1]->set(1000,'A');
p[5]->set(2000,'B');
p[9]->set(500000,'C');
cout << p[1] << ' ' << p[1]->getType() << ' ' << p[1]->getMiles() << endl;
cout << p[5] << ' ' << p[5]->getType() << ' ' << p[5]->getMiles() << endl;
cout << p[9] << ' ' << p[9]->getType() << ' ' << p[9]->getMiles() << endl;
//輸出前 10 個 pointers, 用以比較其間隔
for (int i=0; i< 10; ++i)
cout << p[i] << endl;
for (int i=0; i< N; ++i)
delete p[i];
}
} //namespace
//-----
赏你受困於必须爲不同的classes重写一遍几乎相同的member operator new和 member operator delete时﹐应该有方法将一个总是分配特定尺寸之区块的memory allocator概念包装起来﹐使它容易被重愎使用。以下展示一擂作法﹐每固allocator object都是个分配器﹐它体内维护一个ree-lists ;不同的allocator objects维护不同的free-lists 。
将分配特定尺寸区块的memory allocator包装成一个class allocator,
这样每个allocator object都是个分配器,体内维护一个free lists,
不同类(如下面的class Foo,class Goo等) 里面调用生成的各自allocator objects维护不同的free lists。
另外,class Foo或class Goo中,应注意到:
static allocator myAlloc,即myAlloc必须是个静态成员变量,且在类外定义(赋初值)。如果其不是静态成员变量时,则在构建class Foo类对象时,是没办法调用到myAlloc的。因为非静态成员变量只能通过对象调用【但此时Foo对象还没生成又如何调用!!】。而myAlloc又是用来生成Foo类对象的,所以得通过类名调用即应设置为static类型。
class allocator{
private:
struct obj{
struct obj* next;
};
public:
static void* allocate(size_t);
static void deallocate(void*, size_t);
private:
obj* freeStore = nullptr;
const int CHUNK = 5; // 标准库一般设置为20
};
void* allocator::allocate(size_t size){
obj* p;
if(!freeStore){
// linked list为空,则申请一大块
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk); // 这里直接调用malloc进行分配空间
// 将分配的一大块切分成5小段,并串接起来
for(int i = 0; i < (CHUNK - 1); ++i){
p->next = (obj*)((char*)p + size);
p = p->next;
// 上面这两步相当于p->next = p + 1,
// 只不过这里需要适应不同的类下的操作,因而设置成这种形式!!
}
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 type】
// DECLARE_POOL_ALLOC used in class definition
#define DECLARE_POOL_ALLOC()\
public:\
void* operator new(size_t size){
return myAlloc.cllocate(size);
}\
void operator delete(void* p){
myAlloc.deallocate(p, 0);
}\
protected:\
static allocator myAlloc;
// IMPLEMENT_POOL_ALLOC used in class implementation file
#define IMPLEMENT_POOL_ALLOC(class_name)\
allocator class_name::myAlloc;
当operator new没能力爲你分配出你所申请的memory,会抛一倜std::bad_alloc exception 。某些老旧的编译器则是返回0,你仍然可以令编译器那么做︰
设计良好的new handler
设置 new handler —— set_new_handler
=default , =delete
operator new 不能被default(没有默认版本),可用被delete(禁用)