目录
1.2 C++ memory primitives(C++内存分配基础工具)
2.2 delete expression(delete表达式)
3.2 array size,in memory block(数组的内存布局)
6.1 重载::operator new/::operator delete
6.2 重载operator new/operator delete
6.3 重载operator new[]/operator delete[]
6.6 basic_string 使用 new(extra) 扩充申请量
7.1.2 v2版本:加上了embedded pointer
7.2.3 marco for static allocator
7.3 global allocator(with multiple free-lists)
本章讲解基础的用于分配内存和销毁内存的”工具“。
天宝当年,DOS 640K内存
锱铢必较
俱往矣,且看今朝
1 内存分配的每一层面
1.1 C++ 应用程序使用内存的方式
- C++应用程序可以通过多种方式调用内存分配的”工具“(即接口)
- CRT指的是C语言运行时库
1.2 C++ memory primitives(C++内存分配基础工具)
1.3 四个层面的基本用法
对上述的 4 个 primitives 的使用示例:
#include <iostream>
#include <complex>
#include <memory> //std::allocator
#include <ext\pool_allocator.h> //欲使用 std::allocator 以外的 allocator, 就得自行 #include <ext/...>
namespace jj01
{
void test_primitives()
{
cout << "\ntest_primitives().......... \n";
void* p1 = malloc(512); //512 bytes
free(p1);
complex<int>* p2 = new complex<int>; //one object
delete p2;
void* p3 = ::operator new(512); //512 bytes
::operator delete(p3);
//以下使用 C++ 標準庫提供的 allocators。
//其接口雖有標準規格,但實現廠商並未完全遵守;下面三者形式略異。
#ifdef _MSC_VER
//以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.
int* p4 = allocator<int>().allocate(3, (int*)0);
allocator<int>().deallocate(p4,3);
#endif
#ifdef __BORLANDC__
//以下兩函數都是 non-static,定要通過 object 調用。以下分配 5 個 ints.
int* p4 = allocator<int>().allocate(5);
allocator<int>().deallocate(p4,5);
#endif
#ifdef __GNUC__
//以下兩函數都是 static,可通過全名調用之。以下分配 512 bytes.
//void* p4 = alloc::allocate(512);
//alloc::deallocate(p4,512);
//以下兩函數都是 non-static,定要通過 object 調用。以下分配 7 個 ints.
void* p4 = allocator<int>().allocate(7);
allocator<int>().deallocate((int*)p4,7);
//以下兩函數都是 non-static,定要通過 object 調用。以下分配 9 個 ints.
void* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9);
__gnu_cxx::__pool_alloc<int>().deallocate((int*)p5,9);
#endif
}
} //namespace
【说明】:
- ::operator new()底层就是调用的malloc,::operator delete()底层就是调用的free。
void* p3 = ::operator new(512); //底层就是调用的malloc
::operator delete(p3); //底层就是调用的free
- allocator之所以用了不同的宏来区分,是因为虽然是标准接口,但在不同的环境下不同的使用方式效率不同。
#ifdef __BORLANDC__
//此处的allocator<int>()是临时对象,该行执行结束后,对象的生命就结束了
//分配5个整数,而不是5个字节,因为指定了放的单元类型
int* p4 = allocator<int>().allocate(5);
allocator<int>().deallocate(p4,5);
#endif
- 较新的GNU版本alloc换成了_pool_alloc:
2 基本构件之一newdelete expression
2.1 new expression(new表达式)
- new 的操作是:分配内存,调用构造函数;
- ::operator new 是全局的函数,而该函数可以重载,如果它被重载了,那么调用的就是重载的函数;而此处的Complex 类并没有重载 operator new,所以调用的就是全局的::operator new;
- 在函数opeartor new中可以看到,调用了malloc;
- 只有编译器才可以直接调用构造函数;如果程序想直接调用构造函数,可以使用 palcement new 的方式:new(p) Complex(1,2)
2.2 delete expression(delete表达式)
- delete 的操作是:先调用析构函数,释放内存;
- 注意:程序可以直接调用析构函数;
- operator delete 函数底层就是调用的free;
2.3 Ctor & Dtor 直接调用
编写程序测试是否能直接调用构造函数和析构函数:
- 通过指针调用构造函数:pstr->string::string("jjhou"); 编译会失败;
- 第二段和第三段代码是自定义的类,测试通过程序直接调用构造函数
- pA->A::A(3); 在VC6中执行会成功,而在GCC中则会执行失败,GCC更加严谨
- A::A(5); 在 VC6 中也执行成功,在GCC中执行失败
- 结论:程序不能直接调用构造函数,可以直接调用析构函数
3 Array new
3.1 array new, array delete
- pca 如果使用的是delete pca,那么编译器会认为 pca 是一个对象,只会调用一次析构函数;
- cookie记录个数;用malloc就会有cookie;
- Complex类中没有指针,所以其实析构函数是没有什么用的,但是好的编程观念或技巧,就是要统一,使用array new 就要用 array delete;
- string 类中有指针,如果使用了array new,就一定要使用array delete,否则类中的指针所指向的空间不会被销毁,就会造成内存泄漏;
- A类一定要写默认构造函数,因为使用array new 的时候是没有办法设置初值的,调用的就是默认构造函数;
- 使用placement new来设置初值:new (tmp++) A(i); 【注:在 tmp 这个地址放置一个对象】
小结:
- array new 和 array delete 如果不配套写的话可能会造成内存泄漏,泄露的不是数组本身,而是类中的指针所指向的内存;
- 编译器在处理array new 的时候是从上往下,而array delete 则是从下往上;
- 注意 placement new 的用法;
3.2 array size,in memory block(数组的内存布局)
下图是VC6的malloc的内存布局:
- 61h 是cookie,代表记录内存的大小60h,1 表示这块内存使用了;
- 此处的int 的析构函数是无意义的,不重要的,所以是否使用array delete 都可以;
对于Demo类:
- 使用array new 的时候,内存中会有一个表示分配的对象个数的数,此处的3
- 使用delete[] p; 的时候,因为有[],底层执行free 的时候发现对象个数是 3,于是就调用 3 次析构函数
4 placement new
- placement new 允许我们将object 建构于已经分配的内存中,所以首先需要有一个指针指向已经分配的内存
- 没有所谓的placement delete,因为placement new根本没分配内存
5 C++分配内存的途径
5.1 C++应用程序,分配内存的途径
- 应用程序使用new和delete表达式,不可改变,不可重载
- 成员函数operator new和operator delete可以重载
- 全局函数operator new和operator delete可以重载但少见,重载影响极大,因此一般很少重载
5.2 C++容器,分配内存的途径
- 将一个元素放到容器中的时候,容器也要new一块空间来构造出来;
6 重载
6.1 重载::operator new/::operator delete
重载全局的operator new/opeartor delete,即类外重载:
6.2 重载operator new/operator delete
在类中重载,重载成员函数operator new/opeartor delete:
6.3 重载operator new[]/operator delete[]
6.4 重载示例
(1)示例,接口:在Foo类中重载,重载成员函数operator new/opeartor delete
(2)如果这样写:这样调用全局的重载函数,绕过了重载的函数,强制使用全局版本。
6.5 重载 new()/delete()
我们可以重载class member operator new(),实现多个版本:
具体例子:
- 第⑤个Foo* p5 = new(100) Foo(1)调用的是void* operator new(size_t size, long extra) 这个函数;其调用的是带有参数的构造函数Foo(int),在该构造函数中抛出了异常,只有在这种情况下(构造函数内抛出异常)对应的operator delete才会被调用;
6.6 basic_string 使用 new(extra) 扩充申请量
- 每次创建字符串的时候都多带了一包东西,所以需要extra。
7 allocator
接下来的几个版本是我们自行开发的小型的内存分配器。
7.1 per-class allocator
7.1.1 v1版本
第一版本的opeartor new 和 operator delete:
- 这就是个小型的分配器(小型内存池),但是只针对于这个类;
- 针对的是VC6编译器中的内存块,测试:
7.1.2 v2版本:加上了embedded pointer
第二版本的operator new:
说明:
union {
AirplaneRep rep;
Airplane* next;
};
其中Airplane* next; 借用同一个东西的前4个字节当成指针来使用,这种方法叫做“embedded pointer”,所有内存管理都用了这种技巧。
struct AirplaneRep {
unsigned long miles; //4字节
char type;//1字节
};
为了内存对齐,struct AirplaneRep 的大小为 8 字节。
for (int i = 1; i < BLOCK_SIZE - 1; i++)
newBlock[i].next = &newBlock[i+1]; //next每次移动8个字节
第二版本的opeartor delete:
- 将收回来的指针放入单向链表的头;但是没有还给操作系统;
- 写了member operator new/delete的间隔是8,从间隔可以看出,对象都是紧紧相连的,没有耗用掉cookie;
- 而使用global opeartor new/delete的,每个对象的前后都有cookie,所以间隔是 16;
- 第二版本相比于第一版的优点:使用了union ,用前4个字节当成指针来使用,即“embedded pointer”方法。
- 但是还是有个小缺点:收回来的指针全部累计起来了,如果能还给操作系统就更好了。
/*************************************************************************
> File Name: 01.per-class_allocator.cpp
> Author: Maureen
> Mail: Maureen@qq.com
> Created Time: Thu Nov 25 09:52:51 2021
************************************************************************/
#include <iostream>
using namespace std;
class Airplane {
private:
struct AirplaneRep {
unsigned long miles;
char type;
};
private:
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) {
//如果大小有误,转交给::opeartor new(). 在继承的时候可能发现大小有误
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane* p = headOfFreeList;
if (p) //若p有效,将list头部下移一个元素
headOfFreeList = p->next;
else {
//free list已空,申请(分配)一大块
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; //结束list
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
//opeartor 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<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
int main()
{
cout << sizeof(Airplane) << endl;
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');
//输出前10个pointers,用于比较其间隔
for (int i = 0; i < 10; ++i)
cout << p[i] << endl;
for (int i = 0; i < N; ++i)
delete p[i];
return 0;
}
//测试环境非侯捷的测试环境,所以结果有很大区别
7.2 static allocator
7.2.1 static allocator的原理:
特点:将内存的动作抽取到单一的class——allocator 中;
7.2.2 static allocator 示例与结果
每次开辟的 5 个元素在内存中都是相邻的,但是这一组元素与另外开辟的 5 个元素不一定是相邻的;
/*************************************************************************
> File Name: 02.static_allocator.cpp
> Author: Maureen
> Mail: Maureen@qq.com
> Created Time: Thu Nov 25 10:28:15 2021
************************************************************************/
#include <iostream>
using namespace std;
class allocator {
private:
//单向链表的节点
struct obj {
struct obj* next; //embedded pointer
};
public:
void* allocate(size_t);
void deallocate(void*, size_t);
private:
obj* freeStore = nullptr;
const int CHUNK = 5;
};
void* allocator::allocate(size_t size) {
obj* p;
if (!freeStore) {
//linked list为空,于是申请一大块
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk);
//将分配得来的一大块当做linked list般,小块小块串接起来
for (int i = 0; i < (CHUNK - 1); ++i) {
p->next = (obj*)((char*)p + size);
p = p->next;
}
p->next = nullptr; //last
}
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(long l): L(l) { }
static void* operator new(size_t size) {
return myAlloc.allocate(size);
}
static void operator delete(void* phead, size_t size) {
return myAlloc.deallocate(phead, size);
}
};
allocator Foo::myAlloc;
class Goo {
public:
complex<double> c;
string str;
static allocator myAlloc;
public:
Goo(const complex<double>& x): c(x) { }
static void* operator new(size_t size) {
return myAlloc.allocate(size);
}
static void operator delete(void* phead, size_t size) {
return myAlloc.deallocate(phead, size);
}
};
allocator Goo::myAlloc;
int main() {
Foo* p[100];
cout << "sizeof(Foo) = " << sizeof(Foo) << endl;
for (int i = 0; i < 23; ++i) {
p[i] = new Foo(i);
cout << p[i] << " " << p[i]->L << endl;
}
for (int i = 0; i < 23; ++i) {
delete p[i];
}
//============
Goo* p[100];
cout << "sizeof(Goo) = " << sizeof(Goo) << endl;
for (int i = 0; i < 17; ++i) {
p[i] = new Goo(complext<double>(i, i));
cout << p[i] << " " << p[i]->c << endl;
}
for (int i = 0; i < 17; ++i) {
delete p[i];
}
return 0;
}
7.2.3 marco for static allocator
因为每个使用allocator的类的几处写法是固定的,于是将它们写成宏:
使用示例与结果:
代码:宏
//DECLARE_POOL_ALLOC -- used in class definition
#define DECLARE_POOL_ALLOC()\
public:\
void* operator new(size_t size) { return myAlloc.allocate(size); }\
void operator delete(void* p) { myAlloc.deallocate(p, 0); }\
protected:\
static allocator myAlloc;
//IMPLEMENT_POOL_ALLOC -- used in class implementation
#define IMPLEMENT_POOL_ALLOC(class_name)\
allocator class_name::myAlloc
Foo
class Foo {
DECLARE_POOL_ALLOC()
public:
long L;
string str;
public:
Foo(long l): L(l) { }
};
IMPLEMENT_POOL_ALLOC(Foo)
Goo
class Goo {
DECLARE_POOL_ALLOC()
public:
complex<double> c;
string str;
public:
Goo(const complex<double>& x): c(x) { }
};
IMPLEMENT_POOL_ALLOC(Goo)
7.3 global allocator(with multiple free-lists)
是static allocator的进阶版,针对所有的class,而非针对单一的class。不是用static变量的方式使用allocator,而是全局的。
7.4 小结
- per-class allocator v1:一般
- per-class allocator v2:加上了embedded pointer
- static allocator:将内存的动作抽取到了单一的class——allocator中
- marco for static allocator:设计一个 marco 宏,简化书写
- global allocator:是static allocator的进阶版,是一个全局的allocator
8 new handler
- _callnewh就会调用到由用户指定的handler;
- 如果调用到了用户指定的handler,说明没有内存可用了,就要在这个handler里让更多的内存可用:查看哪些内存可以释放;
例子:
9 =default,=delete
有默认版本的函数才可以被设置为default,C++中有默认版本的函数:
- 拷贝构造函数
- 拷贝赋值函数
- 析构函数
验证operator new 和 operator delete 是否也能 default 和 delete:
说明:operator new 和 operator delte 都不能设置为default