18.4 C++内存高级话题-嵌入式指针概念及范例、内存池改进版

18.1 C++内存高级话题-new、delete的进一步认识
18.2 C++内存高级话题-new内存分配细节探秘与重载类内operator new、delete
18.3 C++内存高级话题-内存池概念、代码实现和详细分析
18.4 C++内存高级话题-嵌入式指针概念及范例、内存池改进版
18.5 C++内存高级话题-重载全局new/delete、定位new及重载

4.嵌入式指针概念及范例、内存池改进版

  4.1 嵌入式指针

(1)嵌入式指针概念

    嵌入式指针,英文名字叫作embedded pointer,是一个挺巧妙的小东西,即便读者不使用,但读别人代码时可能会遇到,所以应该知道这方面的知识。
    嵌入式指针其实也是一个指针,为什么多了“嵌入式”三个字,相信学习完本节内容后,读者就清楚了。
    嵌入式指针常用于内存池的代码实现中,上一节中讲解了内存池,写了一段代码实现了内存池,回忆一下第一次分配内存时,内存池大概的样子如图所示。
    在上一节的实现代码中,为了让空闲的内存块能够正确地分配出去,在类A中引入了一个成员变量next,如下:

A * next;

    读者都知道,这是一个指针,在当前Visual Studio 的x86平台下,这个指针是占4字节的,每new一个类A对象,都会有这么一个4字节的next指针出现,这个多出来的4字节仔细分析一下,是属于内存空间的浪费。
    通过编码能够把这4字节省下来,这就需要用到本节所讲的“嵌入式指针”技术。当然,该技术要成功地使用需要一个前提条件,不过这个前提条件比较容易满足,稍后会谈。
    “嵌入式指针”的工作原理就是:借用类A对象所占的内存空间的前4字节(代替上面谈到的next指针),这4字节专门用来链住这些空闲的内存块。当一个空闲的内存块分配出去之后,这前4字节的内容就不需要了(因为上一节笔者曾经说过:对于已经分配出去的内存块的next指针指向什么已经没有实际意义),即便这4字节的内容被具体的对象内的数据所覆盖,也无所谓。
    既然谈到这里,读者肯定就想到了,“嵌入式指针”技术要成功地使用需要一个前提条件:
    那就是这个类A的sizeof(new一个该类对象时所占用的内存字节数)必须要不少于4字节。当然,在实际的项目中,一般来讲,一个类对象(或者说一个类)的sizeof肯定会超过4字节,不巧的是,在上一节的范例中类A的sizeof值正好是4字节,而这4字节恰好是next成员变量所占的4字节。现在的主要目的是要把next取消掉,那本类这个sizeof(A)的4字节,这next一取消掉,4-4变成0了,其实不是0,而是1字节(任何一个类或者类对象的sizeof都不可能是0字节,最少都是1字节,哪怕是一个空类。笔者会在《C++新经典:对象模型》书籍中专门论述这方面的知识,这里就不多谈)。
    现在的情形下,如果拿掉类A中的next成员,就导致sizeof(A)不够4字节,没法使用“嵌入式指针”技术了。为了演示嵌入式指针技术,笔者向类A中随便增加两个public修饰的int成员变量,则sizeof(A)立即变成8,这个大小足够演示“嵌入式指针”技术了:

public:
	int m_i;
	int m_j;

    引入“嵌入式指针”技术后,图18.14就演变成了图18.18:每个对象块中都借用该块前面的4字节来保存这个嵌入式指针值(这个值用来指向下一个空闲块)。
    为方便查看,在图18.18中将指向箭头绘制在对象块的左侧(因为是借用块首的4字节来当嵌入式指针用),其实与图18.14中将指向箭头绘制在对象块右侧的效果是一样的。
请添加图片描述

(2)嵌入式指针演示代码

    嵌入式指针的理论已经讲得比较详细。这里看一看嵌入式指针的实现代码。这里写一个类,名字叫作TestEP,为了保证该类的sizeof值不小于4,这里给该类两个int类型的成员变量,这样该类的sizeof值至少是8,即超过了4,可以安全地在其中使用嵌入式指针。类TestEP定义如下:

class TestEP
{
public:
	int m_i;
	int m_j; 
public:
	struct obj
	{
		struct obj* next; //next就是个嵌入式指针
	};
};

    这种代码,如果读者是第一次看到,可能会很不习惯。都有哪些不习惯呢?
    (1)比如说类里突然多了一个结构的定义,其实,跟在类外定义这个结构没什么区别,只不过如果把这个结构定义在类TestEP外面的话,外界要用这个obj结构名时直接写成obj,如果定义在类TestEP里面,外界要用obj类名时就需要写成TestEP::obj,所以这个嵌入式指针只不过是嵌到类里面的一个类(结构)。这就是“嵌入式”这三个字的由来。为什么obj这个结构要嵌入到类TestEP里面,显然是因为只想在类TestEP里面使用这个obj结构,而不希望在TestEP类外面也能用obj这个名字,所以放到里面来(而且一般都用private修饰)。
    (2)structobj*next;。
    乍一看上去这种结构成员定义也可能没反应过来。其实这不过是一个指针变量,名字叫next。这个指针变量指向什么呢?只不过是指向一个obj结构。这就是一个链表,自己是一个obj结构对象,那么把自己这个对象的next指针指向另外一个obj结构对象,最终就是把多个自己这种类型的对象通过next指针(像一个铁链)串起来,如图18.19所示。
其实图18.19的感觉和图18.13是完全一样的。
    有了上面这两点的认知,写几行测试代码看一看嵌入式指针是怎样使用的。在main主函数中写入如下代码:

{
	TestEP mytest;
	cout << sizeof(mytest) << endl; //8
	TestEP::obj* ptmp;              //定义一个指针
	ptmp = (TestEP::obj*) & mytest; //把对象mytest的首地址给了这个指针,这个指针指向对象mytest首地址
	ptmp->next = nullptr;           //前四个字节给成了00 00 00 00
}

    根据上面这几行代码,绘制一下对象的结构数据图,如图18.20所示(注意,是ptmp指向了mytest的首地址,而ptmp->next代表的是mytest首地址开始的4字节的内容)。
在这里插入图片描述
在这里插入图片描述
    通过设置断点并跟踪调试观察可以注意到,ptmp->next=nullptr;对应着把mytest对象内存地址的前4字节清0。所以说这里的ptmp->next占用的是对象mytest的前4字节,这一点千万不要理解错。这就是前面探讨的借用对象的前4字节保存嵌入式指针指向的内容。
    上面的测试代码是让这个嵌入式指针指向空了,如果想让它指向下一个内存池中内存块的地址,那最终不就把上一节讲的next指针省下来了嘛!(这里也涉及一个类对象在内存中的布局问题,笔者会在《C++新经典:对象模型》书籍中详细介绍这方面的知识)。这就是嵌入式指针的工作原理。

  4.2 内存池代码的改进

    有了“嵌入式指针”概念后,笔者希望对内存池进行改进,应用“嵌入式指针”这个技术来作为块与块之间的链,同时上一节的内存池是只针对一个类(类A)而写的,如果应用到别的类如类B中,还得在类B中写一堆代码,很不方便。为了把内存池技术更好地应用到其他类中,这里单独为内存池技术的使用写一个类。
    注意下面的实现代码,尤其注意“嵌入式指针”技术是如何应用到内存池的设计中来,因为这个类的大多数代码内容和上一节很类似,所以相信读者都能够懂。

//专门的内存池类
class myallocator   //必须保证使用本类的类 sizeof()不少于4字节,否则崩溃报错
{
public:
	//分配内存接口
	void* allocate(size_t size)
	{
		obj* tmplink;
		if (m_FreePosi == nullptr)
		{
			//为空,我要申请内存,要申请一大块内存
			size_t realsize = m_sTrunkCout * size; //申请m_sTrunkCout这么多倍的内存
			m_FreePosi = (obj*)malloc(realsize);
			tmplink = m_FreePosi;

			//把分配出来的这一大块内存(5小块),彼此用链起来,供后续使用
			for (int i = 0; i < m_sTrunkCout - 1; ++i) //0--3
			{
				tmplink->next = (obj*)((char*)tmplink + size);
				tmplink = tmplink->next;
			} //end for
			tmplink->next = nullptr;
		} //end if
		tmplink = m_FreePosi;
		m_FreePosi = m_FreePosi->next;
		return tmplink;
	}
	//释放内存接口
	void deallocate(void* phead)
	{
		((obj*)phead)->next = m_FreePosi;
		m_FreePosi = (obj*)phead;
	}
private:
	//写在类内的结构,这样只让其在类内使用
	struct obj
	{
		struct obj* next; //这个next就是个嵌入式指针
	};
	int m_sTrunkCout = 5;//一次分配5倍的该类内存作为内存池子的大小
	obj* m_FreePosi = nullptr;
};

    有了这个专用的内存池类或者说是内存分配类,怎样用起来?改造上一节讲解的类A中的代码。完整的类A代码现在如下:

class A
{
public:
	//必须保证sizeof(A)凑够4个字节的,这里两个int成员8字节了,所以使用类myallocator毫无问题
	int m_i;
	int m_j;
public:
	static myallocator myalloc; //静态成员变量,跟着类A走
	static void* operator new(size_t size)
	{
		return myalloc.allocate(size);
	}
	static void operator delete(void* phead)
	{
		return myalloc.deallocate(phead);
	}
};

    上面的代码中定义了一个静态成员变量myalloc,然后直接改造了一下类A中的operator new和operator delete成员函数,整体比较简单。现在在main主函数中写入如下的测试代码:

int main()
{
	cout << sizeof(myallocator) << endl;
	A* mypa[100];
	for (int i = 0; i < 15; ++i)
	{
		mypa[i] = new A();
		printf("%p\n", mypa[i]);
	}
	for (int i = 0; i < 15; ++i)
	{
		delete mypa[i];
	}
}

在这里插入图片描述
    通过上面的结果不难看到,每5个分配的内存地址都是挨着的(间隔8字节),这说明内存池机制在发挥作用(因为内存池是一次分配5块内存,显然这5块内存地址是挨在一起的,因为这5块内存实际上是一次分配出来的一大块内存)。
    当然,如果觉得在类A中加入的代码还是有点多,可以用宏来简化,分别定义两个宏。如下:

#define DECLARE_POOL_ALLOC()\
public:\
	static void *operator new(size_t size)\
	{\
		return myalloc.allocate(size);\
	}\
	static void operator delete(void *phead)\
	{\
		return myalloc.deallocate(phead);\
	}\
	static myallocator myalloc;
#define IMPLEMENT_POOL_ALLOC(classname)\
myallocator classname::myalloc;

    这样,整个类A的定义写成下面的样子即可:

//myallocator A::myalloc; //在类A之外定义一下这个静态成员变量
class A
{
public:
	A()
	{
		cout << "类A的构造函数执行了" << endl;
	}
	~A()
	{
		cout << "类A的析构函数执行了" << endl;
	}
	DECLARE_POOL_ALLOC();
public:
	//必须保证sizeof(A)凑够4个字节的,这里两个int成员8字节了,所以使用类myallocator毫无问题
	int m_i;
	int m_j;
};
IMPLEMENT_POOL_ALLOC(A)

    其他的代码不需要改变。
    本节主要是引入嵌入式指针的概念并希望读者知道,在设计类内内存池时往往都用到嵌入式指针技术来节省4字节的额外空间。另外,本节实现了一个独立的内存池类myallocator,这样所有的类都可以用这个myallocator类来分配内存了。同时,以后读者看到嵌入式指针的用法也就能够很自然地认识了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值