首页 C++内存管理- 14~15
文章
取消

C++内存管理- 14~15

C++内存管理 - 14~15

此笔记需要等待更新和确认。

Static Allocator

如果我们的每一个class都想要分配内存池,那么我们就需要在每个class内部都写自己的内存分配函数。这样会很麻烦。所以我们应该把内存分配函数抽象出来,写入一个新的class。这样的class就叫做static allocator。

QQ图片20220604091838

我们的allocator会被外面的类的对象调用。所以我们不需要再在allocator里面本身维护(储存)那个对象。所以也就不需要union来精简空间。仅需一个指针。

详细说明:查看笔记内存管理12~13。在airplane的例子中。对象airplane里面有一个储存信息的结构体airplaneRep和一个维护内存的union。但是我们每次在外面new的时候new的是整个airplane对象而不是airplaneRep这个局部对象。所以我们需要用union来使用嵌入式指针。

但是这里我们new的是调用allocator的对象而不是allocator本身。因为allocator自己不存有对应的对象。因为allocator只实例化一次,然后类自己通过allocator申请内存空间。所以allocator本身不会存在一个区块既有指针又有对象的情况。因为对象拿到allocator返回的区块指针之后会直接将数据写入。而allocator本身没有对应的数据对象。

每个allocator 对象都是分配器,它体内维护一个freelist,不同的allocator objects维护着不同的freelists。

QQ截图20220604091951

外部用的类使用起来非常简便。重载operator new 和 delete是非常模板化的。所以我们可以用macro 宏来进行进一步简化。

QQ截图20220604094940

注意红色框和蓝色框需要分开。因为蓝色框的部分在class外面,调用的时候需要类名。

重要知识

指针A给指针B赋值的意思是将指针A指向的地址赋给指针B

1
2
3
4
5
6
7
8
9
10
int digit = 5;
int* a = &digit;
int* b = a; //指针a指向的地址赋给指针b 没有新对象产生所以不会调用拷贝构造。
int* c = &*a; //等同于将指针a先解引用(*a = 5),得到指针a指向的的值。然后把这个值取地址(&*a)赋值给指针c。

cout << b << endl; //打印b储存的地址。也就是b指向的地址。 等同于&digit
cout << *b << endl; //解引用b 打印b指向的地址的值。
cout << &b << endl; //打印b自己的地址。
cout << &digit << endl; //打印变量地址。

指针加法

指针直接和数字相加:意思是指针当前指向的地址 + 对应指针类型的大小 * 数字。

举例:

1
2
3
4
5
int * p;
p = (int*)malloc(100); //分配100字节。
cout << p<< endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。此处输出 0x771410
auto y = p + 1; //把p指向的地址偏移4位(因为int是4字节)后赋值给y。
printf("%#x",y); //打印y储存的地址。也就是指向的地址。此处输出 0x771414

也就是说指针直接和数字相加,可以理解为用指针操作数组。加几就是将指针往后移动几位。具体偏移量也就是对应对象的大小*移动位数。

如果想要指定一个具体的偏移量应该怎么办?

首先,可以使用转型将指针转换为对应类型。然后再加数字。

举例:

1
2
3
4
5
6
7
obj* p;
p = (obj*)malloc(sizeof(int) * 10);
cout << p<< endl; //打印p储存的地址。也就是指针指向的地址。是内存首地址。 此处输出 0x771410
auto x = (char*)p + 1; //把p指向的地址偏移1位后赋值给x。因为char是1位。
printf("%#x\n",x); //打印x储存的地址。也就是指向的地址。此处输出 0x771411
auto y = (obj*)p + 1; //把p指向的地址偏移8位后赋值给x。因为obj是指针类型。8位。
printf("%#x\n",y); //打印y储存的地址。也就是指向的地址。此处输出 0x771418

注意:指针类型转换指的是指针转换为对应类型的指针。

此处不要使用cout

auto x = (char*)p的意思是把指针p转为char*类型的指针。不可以auto x = (char)p。 这样做的意思是直接转换成了char。当然,所有的指针都是8字节。记住,指针类型指的是告诉指针应该读取指针指向的地址之后多大的数据。

怎么理解这个操作呢。我们可以想一下。如果 aint*类型,也就是int类型指针。意思是指针a指向的数据是int,所以需要以4字节为一段进行读取。也就是一次读取四个字节。那么a+1其实是对地址进行了+4操作。因为指针+1的意思是将指针往后移动一个对象位。也就是指针当前指向的地址 + 对应指针类型的大小(此处为4) * 数量(此处为1)。但是我们如果想要对地址+1的话怎么办?我们可以把指针a转换为char*类型也就是char类型指针。而不是int类型指针。这样相当于告诉编译器,指针a指向的数据是char。所以需要以1字节为一段进行读取。也就是一次读取一个字节。所以此时a+1就是对地址进行了+1操作。也就是指针当前指向的地址 + 对应指针类型的大小(此处为1) * 数量(此处为1)。

移动一个对象位也就是移动的偏移量为对象大小。

1
2
3
4
5
int digit = 5;
int* a = &digit;
cout << a << endl;
auto x = (char*)a + 1;
printf("%#x\n",x);

注意!(char*)a + 1往后移动1位的时候,(char*)a的意思是把指针a看成指向char类型的指针(告诉编译器一段是1)。所以(char*)a + 1不是+2,还是+1。因为指针a指向的地址根本没变。仅仅是看成了char类型的指针后+1

详细解释

QQ截图20220606122223

  1. 为什么要在allocator里面单独建立一个obj类型的结构体? 我们可以看到。在函数allocate里面,我们使用了obj*类型的指针。我们这个obj类型只有一个指针,所以大小为8。我们看3的位置。申请出来的内存转型为obj*类型后赋值给了obj*类型的指针p。为什么要这么做?因为我们依旧使用了嵌入式指针的概念。把申请来的内存切为对应数量后,在每一个内存子块的前8个字节中填充为一个指向下一空闲位置的指针。我们必须要保证这个指针刚好读取出来指针首地址+8的位置。因为这一段是指针。所以类型一定要是obj*来保证刚好使用了8个字节。这也回答了为什么必须要单独建立一个obj类型的结构体。看2的位置还有两个变量。如果我们指针类型使用allocator类型,这个类型的大小为8+8+4 = 20。这意味着指针要占用分割出来的内存子块的前20个字节。这样做是无意义的。

  2. 整体顺序是什么?首先看3。我们使用malloc向系统申请出对应大小的内存。然后把返回值转换为obj*类型(前面提到过原因)。之后赋值给freestorep。然后,我们看4。我们把一大块内存分割成对应数量的小块。看5。这里里面的(char*)p + size的意思是把指针后移对应的大小。此处我们应该举个例子。假设我们的size是20个字节。系统一共分配了100个字节。我们要切成五块。所以地址应该是0x01~0x20, 0x21~0x40, 0x41~0x60, 0x61~0x80, 0x81~0x100。(0x00在主流机器上一般对应空指针)。

    我们p一开始指向0x01。我们想让p->next指向0x21应该怎么办。这里利用了之前讲的移动指针的时候指定一个具体的偏移量。我们先把p看成char类型的指针,然后+size。这里是+20。这样的话就是指针p指向的地址后移20(1*20)位。这里如果不进行转型直接+20的话会变成+160(8*20)。因为指针obj*指向的数据的大小是8。

    然后我们就得到了 0x01+20 = 0x21也就是p->next的地址。然后我们需要让从0x21开始的内存的前8个字节为我们储存的指针。也就是我们需要让编译器仅读取0x21开始的内存的前8个字节。也就是让指针p->next类型为obj类型。这样读取指针p->next储存的的数据的时候可以获得正确的数据。所以我们还要进行一个(obj*)的转型。

本文由作者按照 CC BY 4.0 进行授权