C++基础 | 初识模板和空间配置器Allocater

前言

单纯地直接写一个普通函数适用性比较小,比如int compare(int a,int b),这个函数直接比较了整型变量a和b的大小。但是如果你有了新需求,想比较两个double型数字的大小,那么之前我们讲过重载,很容易想到重载来满足这个需求。但是我们写代码当然追求省,毕竟懒是人生学习的动力。这种情况使用重载会造成不必要的代码冗余,毕竟参数都一样。那么这次来学习一下模板是如何节省代码的。

首先来看重载的情况:

bool compare(int a,int b)
{
	return a>b;
}
bool compare(double a,double b)
{
	return a>b;
}

int main()
{
	compare(1,2);
	compare(1.3,2.5);
	return 0;
}

针对每一个新需求都写一份重载,浪费时间

函数模板

为了省力气,我们希望通过调用一次compare函数,如果想用比较int类型的compare就用,或者想用比较double类型的compare就用。这里我们先用模板重构一下上面的代码。

templete<typename T>       //函数模板在写出来的时候,声明+定义,这样出来的是一个纯正的模板,而不是函数。
bool compare(const T &a,const T &b)//真正要用的时候是需要函数模板实例化出来一个函数的
{
	return a>b;
}

int main()
{
	compare(1,2);         //这样在函数调用点调用的时候,编译器会自动实例化一个函数。
	//compare<int>(1,2);  //并且,如果函数名后没有使用限定<int>的话,会进行模板实参推演来自动识别参数类型
	compare(2.3,2.5);
	return 0;
}

上面解释了模板实例化模板实参推演的概念,而且可以看到,没写模板之前,假如又遇到需要比较intdouble类型数据的大小还需要再写一份重载的代码,起码多了3行,N多字母。但是有了模板,就可以在上面的模板声明中稍作改动,这样写:

templete<typename T,typename W>
bool compare(const T &a,const W &b);
int main()
{
	compare(1,2.4);
}

这样,只需要加一个typename W,两个参数就支持不同数据类型了。

那么现在遇到了一个新需求,要求输入一个数据和一个整形数字10做大小比较。只需要在函数模板参数上加一个非模板参数int com=10,如下:

templete<typename T,int com=10>
bool compare(const T &a)
{
	return a>com;
}

一份好的模板可以省下很多功夫,这也是学习类库的前驱知识。但是这样做存在一个问题:

如果要求比较两个常量字符串的大小,当然这需要比较其字典序的先后。我们看用上面的思路来比较。但是上面的代码就变成了比较俩字符串地址的大小了,是不符合要求的。遇到这种情况,我们就需要用到模板特例化

模板特例化

针对已有模板所不能解决的问题,首先是想着补充这个已有模板的功能,这个思路就是模板特例化。需要模板已有,对其进行补充。
比如上面已经有了一个模板compare,这里直接对其进行补充:

templete<typename T>    
bool compare(const T &a,const T &b);  //先声明好

templete<>      //特例化的模板,实现对字符串大小的比较
bool comapre(const char* a,const char* b)
{
	return strcmp(a,b);
}

中期总结:到此为止,compare模板已经满足了基本的需要,类似的思想可供来开发最浅显的STL容器

用模板来开发一个简易的Vector容器

直接上代码:

template<typename T>
class wVector
{
public:
	wVector(int size = 10)    //构造函数
	{
		_first = new T[size];
		_last = _first;
		_end = _first + size;
		cout << "wVector consted!" << endl;
	}
	~wVector()             //析构函数
	{
		delete[] _first;
		_first = _last = _end = nullptr;
		cout << "~wVector consted!" << endl;
	}
	wVector(const wVector<T>& other)     //拷贝构造函数
	{
		int size = other._end - other._first;
		_first = new T[size];
		int len = other._last - other._first;
		for (int i = 0; i < len; i++)
		{
			_first[i] = other._first[i];
		}
		_last = _first + len;
		_end = _first + size;
	}
	wVector& operator=(const wVector<T>& other)   //赋值重载函数
	{
		if (this == &other)
		{
			return *this;
		}
		delete[] _first;

		int size = other._end - other._first;
		_first = new T[size];
		int len = other._last - other._first;
		for (int i = 0; i < len; i++)
		{
			_first[i] = other._first[i];
		}
		_last = _first + len;
		_end = _first + size;
		return *this;
	}
	void wPush_Back(const T &val)      //以下就是vector的基本操作了
	{
		if (full())
			expand();
		*_last++ = val;
	}
	void wPop_Back()
	{
		if (empty())
			return;
		--_last;
	}
	T back() const
	{
		return *(_last - 1);
	}
	bool full() const 
	{
		return _last == _end;
	}
	bool empty() const
	{
		return _first == _last;
	}
	int size() const
	{
		return _last - _first;
	}

private:
	T* _first;
	T* _last;
	T* _end;

	void expand()
	{
		int size = _end - _first;
		T* ptmp = new T[size * 2];
		for (int i = 0; i < size; i++)
		{
			ptmp[i] = _first[i];
		}
		delete[] _first;
		_first = ptmp;
		_last = _first + size;
		_end = _end + 2*size;
	}
};

代码不需要细读,功能实现上没问题,勉强能用。车虽然破,好歹能开。调用和普通的vector是一致的,不再赘述。这里我们有3个十分严重的情况要指出:
(1) 如果只是用基本数据类型,如int,这样是没有问题的,但是如果需要用到自己实现的数据类型Test,我们看会有什么问题:

class Test
{
public:
	Test()
	{
		cout << "Test constroed!" << endl;
	}
	~Test()
	{
		cout << "~Test constroed!" << endl;
	}
};
int main()
{
	wVector<Test> vec;
	return 0;
}

我们在main函数中实例化了一个vec,下面是事故重现:

wVector事故
可以看到,一个空容器vec的构造和析构函数各执行了一次,但是离谱的是Test的构造和析构函数执行了10次。

我们定位到问题代码的位置:

wVector(int size = 10)    //构造函数
{
	_first = new T[size];    //这里给T类型 new了10个对象
	_last = _first;
	_end = _first + size;
	cout << "wVector consted!" << endl;
}

这句_first = new T[size]T被转换为了Testnew 运算符构造了10个Test对象。为了方便精准定位根本原因,现在我们来画一下vec目前的内存情况图

Vec存储布局
vec这个对象在构造时new出了10个Test对象。梳理一下:new做了两件事,一是申请内存,二是构造对象。一个人干了两件事, 迟早要出事。分离职责事实上我们在这一步需要将申请内存和构造对象分开。这样就可以实现:vec的构造函数中只分配10个Test对象所占的内存,而不去构造其对象

(2)
再来看push_back()的操作,

void wPush_Back(const T &val)     
{
	if (full())
		expand();
	*_last++ = val;
}

(3)
pop_back()的操作也是有问题的。

void wPop_Back()
	{
		if (empty())
			return;
		--_last;
	}

为了解决上面的三个问题,我们需要自己操作内存分配。这里就牵扯到了空间配置器allocator。我们需要将内存分配/释放对象构造/析构分离开,并将其内置到空间配置器这个类中。下面是allocator这个类模板的代码:

template<typename T>
struct Allocator                 //负责:内存开辟/内存释放   对象构造/对象析构
{
public:

	T* allocate(size_t size)  //负责内存开辟
	{
		return (T*)malloc(sizeof(T) * size);
	}

	void deallocate(void* p)  //负责内存释放
	{
		free(p);
	}
	void construct(T* p, const T& val)  //负责对象构造
	{
		new(p)T(val);                 //使用定位new,在指定的内存p上构造T类型对象val
	}

	void destroy(T* p)        //负责对象析构
	{
		p->~T();     //调用p指向对象的析构函数
	}
};

其中使用allocate方法申请内存,使用construct方法在allocate申请的内存上构造对象。用destroy方法调用指针p指向对象的析构函数来析构p指向的对象,再调用deallocate释放p指向的内存。

用Allocator改造Vector容器

前面已经分析目前我们自实现的vector容器的三个问题,以及空间配置器的好处。现在我们来重写一下容器。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值