C++模板编程—学习C++类库的编程基础


课程总目录



一、详解函数模板

模板的意义:对类型也可以进行参数化了

// 也可以用template<class T>,但class容易和类混淆,我们都用typename
template<typename T>	// 模板参数列表
bool compare(T a, T b)	// compare是一个函数模板
{
	cout << "template compare" << endl;
	return a > b;
}

/*
调用点实例化出来的模板函数
bool compare<int>(int a, int b)
{
	return a > b;
}

bool compare<double>(double a, double b)
{
	return a > b;
}
*/

int main()
{
	// 函数的调用点
	compare<int>(10, 20);
	compare<double>(10.5, 20.5);

	// 函数模板实参推演
	compare(20, 20); 	 // 还是用的刚才实例化的compare<int>
	// compare(30, 40.5); // 错误,推演不出来是什么类型
	// 解决方法一:template<typename T, typename E>,a和b用两个类型,各推各的
	// 解决方法二:compare<int>(30, 40.5),double强转成int
}

函数模板:不进行编译,因为类型还不知道

模板函数:在函数调用点,编译器用程序员指定的类型,从原模板实例化一份函数代码出来这就叫做模板函数,这是实例化出来真正需要进行编译的函数,因此站在编译器的角度来看,待编译的函数并没有减少,只是我们编写的代码量减少了。
同时,实例化出来的模板函数在.o文件符号表中产生相应的符号,每个函数名的符号只能出现一次

来看看字符串的情况 (模板的特例化)

// 针对compare函数模板,提供const char*类型的特例化版本
template<>	// 要写上
bool compare(const char* a, const char* b)
{
	cout << "compare<const char*>" << endl;
	return strcmp(a, b) > 0;
}
// 模板特化不需要在函数名后面加上类型参数
// 即别写成compare<const char*>

int main()
{
	// 推演T为const char*,字符串 > 代表的是比较两个常量的地址,要用strcmp才能比较字符串的字典顺序
	// 对于某些类型来说,依赖编译器默认实例化的模板代码,代码处理逻辑是错误的
	// 这时候,就需要我们进行模板的特例化了,这不是编译器提供的,而是程序员提供的
	compare("aaa", "bbb");
	compare<const char*>("aaa", "bbb");
	// 这两种写法都是对的
}

当然,非模板函数(普通函数)优先被调用

//非模板函数 - 普通函数
bool compare(const char* a, const char* b)
{
	cout << "normal compare" << endl;
	return strcmp(a, b) > 0;
}

int main()
{
	// 这时候就调用普通函数了,不调用模板函数了
	compare("aaa", "bbb");
	// 调用模板函数
	compare<const char*>("aaa", "bbb");
}

编译器优先把compare处理成函数名字,没有的话,才去找compare模板特例化,如果没有特例化,才进行模板的实例化

分文件编写

模板代码是不能在一个文件中定义,在另一个文件中使用的,否则链接的时候会出现错误

比如在test.cpp中存放模板代码,在main.cpp中声明,这是不可以的,因为声明产生的符号是*UND*,而在test.cpp中只有模板,模板本身是不编译的,没有模板实例化出来的compare<int>等函数,所以不可以

模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能进行正常的实例化,产生能够被编译器编译的代码。

所以,模板代码都是放在头文件.h当中的,然后在原文件当中直接进行#include包含

模板的非类型参数:

必须是整数类型(整数或者地址/引用都可以)是常量,只能使用,而不能修改

模板不仅可以接受类型参数typename T,还可以接受非类型参数。这些非类型参数可以是整型、指针、引用等。它们在编译时是常量,只能使用,不能修改

示例代码:

template <int N>
class Array {
public:
    int arr[N];
    
    int size() const { return N; }
};

int main() {
    Array<5> myArray; // 创建一个包含5个整数的数组
    cout << "Array size: " << myArray.size() << endl;
    return 0;
}

// 使用模板实现冒泡排序
template <typename T, int N>
void bubbleSort(T* arr) {
	for (int i = N - 1; i >= 1; --i)
	{
		int flag = 0;
		for (int j = 1; j <= i; ++j)
		{
			if (arr[j - 1] > arr[j])
			{
				T temp = arr[j];
				arr[j] = arr[j - 1];
				arr[j - 1] = temp;
				flag = 1;
			}
		}
		if (flag == 0)
			return;
	}
}

int main() {
	int arr[] = { 64, 34, 25, 12, 22, 11, 90 };
	const int size = sizeof(arr) / sizeof(arr[0]);

	// 调用冒泡排序模板函数
	bubbleSort<int, size>(arr);

	cout << "排序后的数组: ";
	for (int i : arr)
		cout << i << " ";
	cout << endl;

	return 0;
}

二、类模板

  • 类模板 → \to 实例化 → \to 模板类
  • 类名称 = 模板名称 + 类型参数列表
  • 为了简化,构造和析构函数不用加<T>,其他出现模板的地方都要加上
  • 类模板可以设置默认类型参数,实例化的时候只用写SeqStack<>就行了
//template<typename T = int> // 类模板可以设置默认类型参数
template<typename T>
class SeqStack
{
public:
	SeqStack(int size = 10)
		:_pstack(new T[size])
		, _top(0)
		, _size(size)
	{}

	~SeqStack()
	{
		delete[]_pstack;
		_pstack = nullptr;
	}

	SeqStack(const SeqStack<T>& stack)
		:_top(stack._top)
		, _size(stack._size)
	{
		_pstack = new T[_size];
		for (int i = 0; i < _top; i++)
			_pstack[i] = stack._pstack[i];
	}

	SeqStack<T>& operator=(const SeqStack<T>& stack)
	{
		// 防止自赋值
		if (this == &stack)
			return *this;

		delete[]_pstack;

		_top = stack._top;
		_size = stack._size;
		_pstack = new T[_size];
		for (int i = 0; i < _top; i++)
			_pstack[i] = stack._pstack[i];

		return *this;

	}

	void push(const T& val);

	void pop()
	{
		cout << "pop():" << _pstack[_top] << endl;
		if (empty())
			return;
		--_top;
	}

	// 之前说过,对于只需要读的方法,最好写成常方法
	T top() const	// 返回栈顶元素
	{
		if (empty())
			throw "stack is empty";//抛异常也代表函数逻辑结束
		return _pstack[_top - 1];
	}

	bool full() const { return _top == _size; }	// 栈满
	bool empty() const { return _top == 0; }	// 栈空


private:
	T* _pstack;
	int _top;
	int _size;

	// 扩容
	void expand()
	{
		T* ptmp = new T[_size * 2];
		for (int i = 0; i < _top; i++)
			ptmp[i] = _pstack[i];
		delete[] _pstack;
		_pstack = ptmp;
		_size *= 2;
	}
};

// 在类外实现成员方法
// 注意点:1.加类的作用域SeqStack<T>::  2.写template<typename T>
template<typename T>
void SeqStack<T>::push(const T& val)
{
	cout << "push(const T& val):" << val << endl;
	if (full())
		expand();
	_pstack[++_top] = val;
}

三、类模板实践:实现向量容器vector

template<typename T>
class vector
{
public:
	vector(int size = 10)
	{
		_first = new T[size];
		_last = _first;
		_end = _first + size;
	}

	~vector()
	{
		delete[] _first;
		_first = _last = _end = nullptr;
	}

	vector(const vector<T>& vec)
	{
		int size = vec._end - vec._first;
		_first = new T[size];
		int len = vec._last - vec._first;
		for (int i = 0; i < len; ++i)
			_first[i] = vec._first[i];
		_last = _first + len;
		_end = _first + size;
	}

	vector<T>& operator=(const vector<T>& vec)
	{
		// 防止自赋值
		if (this == &vec)
			return *this;

		// 释放本身指向
		delete[] _first;

		// 拷贝
		int size = vec._end - vec._first;
		_first = new T[size];
		int len = vec._last - vec._first;
		for (int i = 0; i < len; ++i)
			_first[i] = vec._first[i];
		_last = _first + len;
		_end = _first + size;
	}

	void push_back(const T& val)	// 向容器末尾添加元素
	{
		if (full())
			expend();
		*_last++ = val;
	}

	void pop_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 expend()	// 容器的二倍扩容
	{
		int size = _end - _first;
		T* ptmp = new T[2 * size];
		for (int i = 0; i < size; ++i)
			ptmp[i] = _first[i];
		delete[] _first;
		_first = ptmp;
		_last = _first + size;
		_end = _first + 2 * size;
	}
};


int main()
{
	vector<int> vec;
	for (int i = 0; i < 20; ++i)
		vec.push_back(i);
	vec.pop_back();	// 弹出19
	while (!vec.empty())
	{
		// 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
		cout << vec.back() << " ";
		vec.pop_back();
	}
	cout << endl;
	return 0;
}

四、理解容器空间配置器allocator的重要性

先来看一下目前会有哪些问题?

template<typename T>
class vector { ... };	// 同上节

class Test
{
public:
	Test() { cout << "Test构造" << endl; }
	~Test() { cout << "~Test析构" << endl; }
	Test(const Test& t) { cout << "Test拷贝构造" << endl; }
};
int main()
{
	vector<Test> vec;
	return 0;
}

运行结果:

Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构

一个空容器,竟然构造了10个Test对象,在vector的构造函数里使用了new,这不仅会开辟空间,还会去调用构造函数去构造对象,这明显是不合理的!

也就是我们需要把内存开辟和对象构造分开处理

vector的析构函数里面使用delete,即delete[] _first;,这会把_first指针指向数组的每一个元素都析构一遍,而我们应该是析构容器中有效的元素(数组可能很长,里面可能只有几个有效元素),然后释放_first指针指向的堆内存

另外我们来看,当我们添加一些对象的时候:

int main()
{
	Test t1, t2, t3;
	cout << "--------------------" << endl;
	vector<Test> vec;
	vec.push_back(t1);
	vec.push_back(t2);
	vec.push_back(t3);
	cout << "--------------------" << endl;
	vec.pop_back();
	cout << "--------------------" << endl;

	return 0;
}

运行结果:

Test构造
Test构造
Test构造
--------------------
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
-------------------- # 这里pop_back没析构,明显不行
--------------------
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构

现在的逻辑vector底层用了new,相当于容器中每一个位置已经放了一个Test()对象,我们现在push_back(t1)是用t1去给容器里的的Test()对象赋值;

正确的逻辑:容器中生成的只有内存,而没有对象,push_back的时候应该是在已有的内存上面进行拷贝构造,

同时,容器中的对象很有可能占用外部资源,而pop_back现在的逻辑只是--_last;,下一次再添加对象的时候覆盖当前内存,指向外部资源的指针就丢失了,因此在pop_back的时候应该析构当前的对象,但是注意不能使用delete,因为delete不仅会调用对象的析构函数,而且还做一个free操作,把当前的内存释放掉,在这里面我们正确的需求是只析构对象,而不释放容器内部的空间

也就是我们需要把对象的析构和内存释放分开处理

那么,我们想要解决以上的问题,这就需要容器的空间配置器allocator

容器的空间配置器allocator做的四件事:

  • 内存开辟/内存释放
  • 对象构造/对象析构

那我们现在来实现一个自己的空间配置器

// 定义容器的空间配置器,和C++标准库的allocator实现一样
template<typename T>
class 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
		// 在指定内存上构造一个值为val的对象,这会调用T的拷贝构造
	}
	void destruct(T* p)			// 负责对象析构
	{
		p->~T(); // ~T()代表了T类型的析构函数
	}
};

添加到vector中:

// 注意不要只写Allocator,这是模板名称,要是类名称才对
template<typename T, typename Alloc = Allocator<T>>
class vector
{
public:
	vector(int size = 10)
	{
		// 需要把内存开辟和对象构造分开处理
		// _first = new T[size];
		_first = _allocator.allocate(size);

		_last = _first;
		_end = _first + size;
	}

	~vector()
	{
		// 析构容器中有效的元素,然后释放_first指针指向的堆内存
		// delete[] _first;
		for (T* p = _first; p != _last; ++p)
			_allocator.destruct(p);		// 析构有效元素
		_allocator.deallocate(_first);	// 释放堆上的数组内存

		_first = _last = _end = nullptr;
	}

	vector(const vector<T>& vec)
	{
		int size = vec._end - vec._first;

		// _first = new T[size];
		_first = _allocator.allocate(size);

		int len = vec._last - vec._first;
		for (int i = 0; i < len; ++i)
		{
			// _first[i] = vec._first[i];
			_allocator.construct(_first + i, vec._first[i]);
		}

		_last = _first + len;
		_end = _first + size;
	}

	vector<T>& operator=(const vector<T>& vec)
	{
		if (this == &vec)
			return *this;

		// delete[] _first;
		// 和析构~vector()一样
		for (T* p = _first; p != _last; ++p)
			_allocator.destruct(p);		// 析构有效元素
		_allocator.deallocate(_first);	// 释放堆上的数组内存

		// 和拷贝构造一样
		int size = vec._end - vec._first;
		// _first = new T[size];
		_first = _allocator.allocate(size);
		int len = vec._last - vec._first;
		for (int i = 0; i < len; ++i)
		{
			// _first[i] = vec._first[i];
			_allocator.construct(_first + i, vec._first[i]);
		}
		_last = _first + len;
		_end = _first + size;
	}

	void push_back(const T& val)
	{
		if (full())
			expend();
		// *_last++ = val;
		// 现在要在_last指针指向的内存构造一个值为val的对象
		_allocator.construct(_last, val);
		++_last;
	}

	void pop_back()
	{
		if (empty())
			return;
		// --_last;
		// 不仅要--_last,还需要析构删除的元素
		--_last;
		_allocator.destruct(_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;
	Alloc _allocator; // 定义容器的空间配置器对象

	void expend()
	{
		int size = _end - _first;

		//T* ptmp = new T[2 * size];
		T* ptmp = _allocator.allocate(2 * size);
		for (int i = 0; i < size; ++i)
		{
			// ptmp[i] = _first[i];
			_allocator.construct(ptmp + i, _first[i]);
		}

		//delete[] _first;
		for (T* p = _first; p != _last; ++p)
			_allocator.destruct(p);		// 析构有效元素
		_allocator.deallocate(_first);	// 释放堆上的数组内存

		_first = ptmp;
		_last = _first + size;
		_end = _first + 2 * size;
	}
};

使用&未使用allocator的对比:
在这里插入图片描述
可以看到,使用空间配置器之后才是正确的逻辑!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GeniusAng丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值