C++内存管理 - 14~15
此笔记需要等待更新和确认。
Static Allocator
如果我们的每一个class都想要分配内存池,那么我们就需要在每个class内部都写自己的内存分配函数。这样会很麻烦。所以我们应该把内存分配函数抽象出来,写入一个新的class。这样的class就叫做static allocator。
我们的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。
外部用的类使用起来非常简便。重载operator new 和 delete是非常模板化的。所以我们可以用macro 宏来进行进一步简化。
注意红色框和蓝色框需要分开。因为蓝色框的部分在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字节。记住,指针类型指的是告诉指针应该读取指针指向的地址之后多大的数据。
怎么理解这个操作呢。我们可以想一下。如果 a
是int*
类型,也就是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
详细解释
为什么要在
allocator
里面单独建立一个obj
类型的结构体? 我们可以看到。在函数allocate
里面,我们使用了obj*
类型的指针。我们这个obj
类型只有一个指针,所以大小为8。我们看3的位置。申请出来的内存转型为obj*
类型后赋值给了obj*
类型的指针p。为什么要这么做?因为我们依旧使用了嵌入式指针的概念。把申请来的内存切为对应数量后,在每一个内存子块的前8个字节中填充为一个指向下一空闲位置的指针。我们必须要保证这个指针刚好读取出来指针首地址+8的位置。因为这一段是指针。所以类型一定要是obj*
来保证刚好使用了8个字节。这也回答了为什么必须要单独建立一个obj
类型的结构体。看2的位置还有两个变量。如果我们指针类型使用allocator类型,这个类型的大小为8+8+4 = 20。这意味着指针要占用分割出来的内存子块的前20个字节。这样做是无意义的。整体顺序是什么?首先看3。我们使用
malloc
向系统申请出对应大小的内存。然后把返回值转换为obj*
类型(前面提到过原因)。之后赋值给freestore
和p
。然后,我们看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*)
的转型。