一、内存池的缺陷
作者在上一版本里面介绍了链表实现内存池,其中有一个小缺陷:虽然较少了cookie的内存损耗,但是加入了一个额外的指针,仍然需要占用内存。我们仔细看内存池的设计思想,可以发现一个关键点:
对于一个内存切片,如果放置在freeList中,才会使用指针。如果被用于构造对象,则这个指针毫无用处。
于是可以想到,可以将一块内存区域,即作为指针使用,又用于构造对象。
方案一:共同体,这个东西过于古早了,不过多解释。
方案二:嵌入式指针。
二、指针类型转换
想要把嵌入式指针讲清楚,先要把指针类型转换讲清楚。
在C++里面,64位操作系统下,所有指针都是八字节,表示一个地址。那么为什么指定指针的类型呢?编译器根据指针定位到这个内存地址之后,根据指针类型去解析这个数据。举个例子,假如是一个int类型的指针,定位到这个地址之后,扫描后面的四个字节,去解析这32位二进制代表的int数字是多少。
说一个看起来违背常识的事情,指针之间转换,基本是不被编译器报错的!!但是有可能解析出来一大堆稀奇古怪的东西,所以最好不要这样做。也就是说,给编译器一个地址和数据类型,编译器就可以解析,至于解析出来的是什么东西,由程序员负责
看代码:
#include <iostream>
using namespace std;
class Test
{
public:
int m_i;
int m_j;
};
class A{
public:
int a;
};
int main()
{
Test t ;
t.m_i=1000;
t.m_j=2;
A *a=(A*)&t;
cout<<a->a<<endl;
cout<<t.m_j;
}
输出结果:
1000
2
说明一点:
(1)Test类的字节数大于A类,将Test指针强转为A类型指针后,相当于使用前面的地址,后面的地址也不会被抛弃,只是当前不用。
(2)不考虑cookie的情况下,a->a相当于解析t的前四位字节,恰好前四位也是int类型,就可以解析出来t.m_i对应的值。
这是大结构体的指针转换为指向小结构体的指针,如果反之会怎么样?
看代码:
#include <iostream>
using namespace std;
class Test
{
public:
int m_i;
int m_j;
};
class A{
public:
int a;
};
int main()
{
A a;
a.a =666;
Test * t = (Test *)&a;
cout<<t->m_i<<endl;
cout<<t->m_j;
}
运行结果:
666
-2120222908
第一个成员变量可以正常输出,但是对于第二个成员变量拿到了垃圾数值。但是编译器没有限制这个行为,编译正常,也可以正常退出。
三、嵌入式指针
嵌入式指针的核心思想:在这个结构体不用于存储数据的时候,将前八个字节强转为指针,将多个结构体按照链表进行管理。看代码:
#include <iostream>
using namespace std;
class Test
{
public:
int m_i;
int m_j;
struct Obj // 相当于在类中定义了一个结构体
{
struct Obj* pNext; // 这个pNext是一个嵌入式指针,64位系统下占了八个字节
};
};
int main()
{
Test test1;
cout << "Test size is "<<sizeof(test1) << endl;
Test::Obj* pTemp;
pTemp = (Test::Obj*)&test1; //类型强转,pTemp实际上指向test1的前两八个字节
Test test2;
pTemp->pNext = (Test::Obj*)&test2; //操作以此类推
Test test3;
((Test::Obj*)&test2)->pNext = (Test::Obj*)&test3;
((Test::Obj*)&test3)->pNext = nullptr;
}
注意要点:
(1)struct Obj 定义在Test类里面,但是只是定义,没有创建对象,不会占用空间,所以Test对象的大小是八个字节。
(2)嵌入式指针的一个重要原则:使用嵌入式指针的类的size必须大于指针大小,不然强转会有问题。
(3)示例代码来自侯捷老师的课程,实际上struct Obj 大可不必定义在Test类里面,定义在外面也是可行的。但是考虑到这个struct Obj 只被Test使用,定义在Test中比较规格。