C++的学习心得和知识总结 第四章(完)

在笔试面试中(单独考察模板的机会不多),模板主要是用在开发库(针对很多类型都是可以使用的)上面。有了模板,我们在将要使用某种类型的时候,只需要用这个类型实例化我们已经写好的模板 就可以了。

文章目录

    • 第一节:理解函数模板
    • 第二节:理解类模板
    • 第三节:实现C++ STL向量容器vector
    • 第四节:理解容器 空间配置器allocator的重要性

第一节:理解函数模板

模板的意义:对类型也可以进行参数化了
int sum(int a, int b){return a+b;}这里的两个形参变量a b,就是为了接受实参的值。他们两个是对实参的值进行了一个参数化。但是实参的类型必须是确定的,因为这里形参定义的时候已经确定了。所以在调用这个sum函数的时候,实参传入的值的类型只能是int。模板的意义:对类型也可以进行参数化了,将会是非常方便。只用写一套代码实现逻辑即可,至于模板最后用什么类型去实例化可以在函数调用点指定类型就行。

函数模板 : 是不进行编译的,因为(形参a b)类型还不知道。(区分函数模板很简单,前面看有没有加template和模板参数列表。)
模板的实例化:是在函数调用点进行实例化(由用户指定的类型,或实参推演出的类型)
模板函数: 才是要被编译器所编译的
有了函数模板,我们只用注重于功能的实现,并不需要管处理的数据的具体类型是什么
模板类型参数:< > 里面可以由typename/class定义 模板 类型参数T (可以定义多个,由,隔开)T用来接收 类型的。
模板非类型参数

模板的实参推演 :可以根据用户传入的实参的类型,来推导出模板类型参数的具体类型

模板的特例化(专用化) 特殊(不是编译器提供的,而是用户提供的)的实例化
函数模板、模板的特例化、非模板函数的重载关系

模板代码是不能在一个文件中定义,在另外一个文件中使用的。模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能够进行正常的实例化,产生能够被编译器编译的代码。所以,模板代码都是放在头文件当中的,然后在源文件当中直接
进行#include包含。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

//只能做整型的比较
bool compare(int a, int b)
{
	return a > b;
}

template<>//定义一个模板参数列表 可以在<>中,定义很多的类型参数名称,或者是非类型参数名称。
template< typename T>//定义了一个类型参数T ,T后面用来接收类型。

template<typename T>//定义一个模板参数列表
//用T类型 定义具体的形参变量
bool compare(T a, T b)
{
	cout << "template compare" << endl;
	return a > b;
}

此时的compare 只是一个函数模板,通过这个模板可以创建出一个具体的函数。当然我们在调用的时候,肯定是希望这个compare是一个 函数名称(而非函数模板名称)。模板名称+参数列表----->函数名称。 我们在定义这个函数模板的时候,需要让用户指定一个类型参数。

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

compare< int >
compare< double > 这才叫函数名称。
在函数调用点,编译器用用户指定的类型(int double),从原模板实例化一份函数代码出来 如下:
模板函数:真真正正需要代码编译(编译器编译)的函数
bool compare< int >(int a, int b) //
{
return a > b;
}
bool compare< double >(double a, double b)
{
return a > b;
}
(对于编译器而言,它所需要编译的函数代码 并没有减少。每一个用户指定的类型的都需要编译,每一个类型都对应一份函数。但从用户编写代码的角度而言:只用写一份作为模板就可以了)
有时候会发现只用函数模板名称也可以调用函数:

// 函数模板实参的推演
	compare(20, 30);

模板的实参推演 :可以根据用户传入的实参的类型,来推导出模板类型参数的具体类型。它会推演出来实参类型 是个int,则此时的函数模板里面的T 就是int了。(但是我们在之前的代码里已经做了 int类型的实例化了,那么实参推演又是int 还会重新再生成一份 int形参类型的函数代码吗?)当然不会,不然的话 会造成函数的重定义,第一次只会在相应的目标文件里面的符号表里面产生函数的相应的符号,这个名字的符号只能出现一次。(用某个类型实例化函数模板产生的函数代码只产生一份,如上面产生的那个。在后面使用的时候直接调用这个函数代码就行,不用产生新的函数)。

compare(30, 40.5);

对于上面那种情况: T是int 还是double?
C++的学习心得和知识总结 第四章(完)_第1张图片

 error C2672:  “compare”: 未找到匹配的重载函数
 error C2782:bool compare(T,T): 模板 参数“T”不明确
 message :  参见“compare”的声明
 message :  可能是“double”
 message :  或    “int”
 error C2784:bool compare(T,T): 未能从“double”为“T”推导 模板 参数
 message :  参见“compare”的声明

原因分析:因为这里依赖于 函数模板实参的推演:使用模板名作为函数名来进行调用函数,编译器肯定要做一个从 实参类型到形参T的推演。
解决方法一:重新定义一个新的模板类型参数,a和b 用不同的模板类型参数。各自推演各自的,但是现在a和b 用的是相同的模板类型参数。却实参的类型不一样,编译器无法T类型到底采用什么类型?
解决方法二:compare(30,40.5); 这样也可以了,只是会牵扯到精度丢失的警告。

假如我们现在:compare("aaa", "bbb"); 确实也可以进行比较,但是在调用这个模板的时候并没有指定参数列表(所以就要使用函数模板实参的推演)推导出T为(常量字符串) const char *。
得到了我们实例化以后的模板函数如下:

bool compare<const char*>(const char* a, const char* b)
{
	return a>b;
}

问题:a>b 只是在比较两个字符串的地址大小。而我们想要的是 让他们比较“字典顺序:阿斯克码的顺序”。实际上应该 调用的是return strcmp(a, b) > 0;
但是 编译器也没办法,模板给的就是 > 比较。编译器在进行模板实例化的时候,只能做到可以根据用户传入的实参的类型(或者实参推演的类型),来推导出模板类型参数的具体类型。并不能够根据这个类型来改变代码。
问题:对于某些类型来说,依赖编译器默认实例化的模板代码,代码处理逻辑是有错误的。比如 字符串类型的compare 。这个时候需要来给模板提供特例化(特殊的模板实例化)。
特殊在:模板的特例化(专用化) 特殊(不是编译器提供的,而是用户提供的)的实例化。template<>//不可省略:表示这个特例化也是从上面的模板来的,先有模板才有特例化。特例化也是从模板实例化来的。虽然这个特例化是自己写的,编译器还是要检查这个特例化符不符合实例化的语法。< >空着是因为T 我们已经知道了

//针对compare函数模板,提供const char *类型的特例化版本
template<>//不可省略:表示这个特例化也是从上面的模板来的,先有模板才有特例化。
bool compare<const char*>(const char* a, const char* b)
{
	return strcmp(a, b) > 0;
}
template<typename T>//定义一个模板参数列表
//用T类型 定义具体的形参变量
bool compare(T a, T b)
{
	cout << "template compare" << endl;
	return a > b;
}

//针对compare函数模板,提供const char *类型的特例化版本
//不可省略:表示这个特例化也是从上面的模板来的,先有模板才有特例化。
template<>
bool compare<const char*>(const char* a, const char* b)
{
	cout << "compare" << endl;
	return strcmp(a, b) > 0;
}
//非模板函数
bool compare(const char* a, const char* b)
{
	cout << "普通的compare" << endl;
	return strcmp(a, b) > 0;
}

如上代码:函数模板、模板的特例化、非模板函数的重载关系
但是这里不符合重载的要求:函数名相同 参数列表不同
这里的函数名:不一样
特例化的:compare
普通的和模板的:compare

此时compare("aaa", "bbb"); 就优先的把compare看做是普通函数 函数名:因为这样最简单(看成模板 还要进行实参推演)。调用函数只给了一个函数名字:编译器为了减少工作 直接调用函数即可。
但是此时compare("aaa", "bbb");//调用特例化版本 绝对不是函数名(只有模板名称后面+ < >),只能调用函数模板,但是发现用户已经通过了 const char * 的特例化版本 直接特例化就行了。
编译器优先把compare处理成函数名字,没有的话,才去找compare模板 然后是编译器自己实例化 还是有用户的针对这个类型的特例化版本。

**函数模板的特点:**在写多文件工程的时候,如果把 函数模板、const char * 的特例化版本、普通函数都放在另一个源文件里面。然后在另一个文件中用的时候,如下:
C++的学习心得和知识总结 第四章(完)_第2张图片
C++的学习心得和知识总结 第四章(完)_第3张图片

// 模板的声明
template< typename T >
//bool compare(T a, T b); // compare *UND*
//bool compare(const char* a, const char* b); // compare *UND*  普通的函数声明

运行如下:

error LNK2019: 无法解析的外部符号 "bool __cdecl compare(int,int)" (??$compare@H@@YA_NHH@Z),该符号在函数 _main 中被引用
error LNK2019: 无法解析的外部符号 "bool __cdecl compare(double,double)" (??$compare@N@@YA_NNN@Z),该符号在函数 _main 中被引用

他们找到了 普通函数和compare< const char * >函数:
C++的学习心得和知识总结 第四章(完)_第4张图片
因为在声明处
template< typename T >
bool compare(T a, T b); // compare * UND *
我们得到了一个用const char * 实例化的 compare * UND * 这么一个符号的引用。在链接的时候要去其他文件中 找到这个符号定义的地方。可以找到的。
可是为什么int 和 double 实例化的符号没找到?
因为模板本身不编译,类型不知道啊。它是在函数调用点进行实例化(由用户指定的类型,或实参推演出的类型) 得到的模板函数才是去编译的。

模板代码是不能在一个文件中定义,在另外一个文件中使用的(这样是找不到这个符号的,因为这个符号 现在是在这个文件中调用的时候,用int double进行实例化的,只能产生这些符号的UND符号 找不到他们在text段定义的地方。在链接的时候 产生错误)。
模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能够进行正常的实例化,产生能够被编译器编译的代码。

所以,模板代码都是放在头文件当中的(直接在源文件当中看见他们的源码),然后在源文件当中直接进行#include(在预编译 把头文件代码在当前源文件里面展开)包含。 模板代码调用之前,是都可以看到模板定义的地方。这样的话,模板才能够进行正常的实例化,产生能够被编译器编译的代码。

但是非要多文件,也是可以的,如下(模板的显式实例化)
C++的学习心得和知识总结 第四章(完)_第5张图片
在 模板定义的那个文件里面,加上如下:

//直接告诉编译器 直接实例化相应的函数(进行指定类型的模板实例化)
template bool compare<int>(int, int);
template bool compare<double>(double, double);

本来是在函数调用点,编译器用用户指定的类型(int double),从原模板实例化一份函数代码出来。现在 编译器不要去看调用点了,直接告诉编译器 直接实例化相应的函数(进行指定类型的模板实例化)。这样就可进行一个分文件的调用了。
但是我们在写模板代码的时候,不可能把所有要使用的类型都去实例化一下。

如果在通用算法(处理任何类型都可以)的时候:可以处理为模板,适用于任何类型。

注:传入 int double ,则typedef int T和typedef double T(这是类型重定义),现在T是一个独立的类型,而不是简单的字符串替换的宏替换。

第二节:理解类模板

本节:函数模板的 模板的非类型参数类模板
类模板的使用实际上是类模板实例化成一个具体的类(而非模板类)。
模板参数列表里面一般都是定义参数,用来初始化类型的。即都是定义模板类型参数。模板类型参数:< > 里面可以由typename/class定义 模板 类型参数T (可以定义多个,由,隔开)T用来接收 类型的。

模板的非类型参数 都是常量,只能使用 不能修改。例如 SIZE。在模板参数列表里面定义非类型参数用来接收非类型参数
而且这个常量只能且必须是整数类型(整型的:int char long short。地址也是整型的:其他类型的指针或者引用也是可以的)。

template<typename T, int SIZE>
//模板类型参数T   和   模板非类型参数
//对于任何类型的数据 都可以做冒泡排序
//这样将不限于整型
template<typename T, int SIZE>
void sort(T* arr)
{
	for (int i = 0; i < SIZE - 1; ++i)
	{
		for (int j = 0; j < SIZE - 1 - i; ++j)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{
	int arr[] = { 12,5,7,89,32,21,35 };
	const int size = sizeof(arr) / sizeof(arr[0]);
	//右边的常量表达式在编译时期已经算过了
	//若是把变量的值给size 则它将退化成为常变量 而非常量
	sort<int, size>(arr);
	for (int val : arr)
	{
		cout << val << " ";
	}
	cout << endl;
	return 0;
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

	// 顺序栈底层数组按2倍的方式扩容
	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;
	}

这里的T* ptmp = new T[_size * 2]; 只能用new,T类型(有可能是 类型,需要调用构造函数的。malloc是不能调用构造函数的,它只能开辟内存。new可以开辟内存,也可以进行初始化的。对于类类型,其初始化就是:调用相应的构造函数)

用模板实现的顺序栈如下:(在任何需要使用栈的地方,存取任意类型的元素都是可以通过相应类型去实例化它即可)

// 类模板
template<typename T = int>
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];
		// 不要用memcpy进行拷贝
		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];
		// 不要用memcopy进行拷贝
		for (int i = 0; i < _top; ++i)
		{
			_pstack[i] = stack._pstack[i];
		}
		return *this;
	}

	void push(const T& val); // 入栈操作  声明

	void pop() // 出栈操作
	{
		if (empty())
			return;
		--_top;
	}
	//只读操作 建议写成const 方法
	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;

	// 顺序栈底层数组按2倍的方式扩容
	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;
	}
};
template<typename T>
void SeqStack<T>::push(const T& val) // 入栈操作
{
	if (full())
		expand();
	_pstack[_top++] = val;
}
int main()
{
	// 类模板的选择性实例化
	// 模板类 class SeqStack{};
	SeqStack<int> s1;
	s1.push(20);
	s1.push(78);
	s1.push(32);
	s1.push(15);
	s1.pop();
	cout << s1.top() << endl;

	SeqStack<> s2;
	return 0;
}

C++的学习心得和知识总结 第四章(完)_第6张图片
在类外实现成员方法:
1 需要在类体内 声明
2 前面需要加类的作用域,但现在SeqStack不是类(而是模板名称)。
模板名称+ < T > 才是类名称 如下:

//再需要类型参数T的时候,只能去重新定义这个  类型参数T
template<typename T>
void SeqStack<T>::push(const T& val) // 入栈操作
{
	if (full())
		expand();
	_pstack[_top++] = val;
}

不管是类模板还是函数模板,所定义的模板参数列表里面的 类型参数只能延续到 类或者函数 左括号{ 到右括号}。

SeqStack<int> s1;

现在用整型实例化了一个 栈。在用整型实例化这个类模板的时候,编译阶段就会用类模板来实例化一份 专门处理整型的 出来。
C++的学习心得和知识总结 第四章(完)_第7张图片
如上:因为 我们在这里只是定义了一个对象,只用到构造函数;return 0; 那用到了析构函数。所以编译器在用类模板来实例化一份 专门处理整型的 模板类出来的时候,里面只有构造函数和析构函数,没有其他方法(因为没用到)。只有在用到这些方法的时候,才会加上这些方法。
这是类模板的选择性实例化:用某一个类型去实例化类模板得到一个模板类的时候,这个模板类里面有多少成员方法?编译器主要看的是在代码里面使用到哪些方法然后才在实例化的过程中给到模板类(没有调用到的方法 就不产生)减少编译器的工作。
C++的学习心得和知识总结 第四章(完)_第8张图片
C++的学习心得和知识总结 第四章(完)_第9张图片
实例化结束之后产生 模板类(从模板实例化的一个类型)
C++的学习心得和知识总结 第四章(完)_第10张图片
类模板还可以加 默认的类型参数:template
使用的时候就可以:SeqStack<> s2; (类型参数有默认值)

第三节:实现C++ STL向量容器vector

C++的学习心得和知识总结 第四章(完)_第11张图片
这个类生成的容器对象内存里面放的都是指针(要指向外部的空间),所以当对象发生浅拷贝的时候。一定会出问题。所以需要提供拷贝构造函数 赋值重载函数。
_first 指向的是vector容器底层在堆上 动态开辟的那个数组的起始地址。
_last 最后一个有效元素的后继位置
_end 整个数组空间的起始位置

template<typename T=int>//定义模板类型参数
class MyVector
{
public:
	MyVector(int size = 5)//底层开辟5个元素的空间
	{
		_first = new T[size];
		_last = _first;
		_end = _first + size;
	}
	~MyVector()
	{
		delete[]_first;
		_first = _last = _end = nullptr;
	}
	MyVector(const MyVector<T>& src)
	{
		int size = src._end - src._first;//总的空间 个数
		_first = new T[size];
		_end = _first + size;
		int len = src._last - src._first;//有效长度
		_last = _first + len;
		for (int i = 0; i < len; ++i)
		{
			_first[i] = src._first[i];//把值赋过来
		}
	}
	MyVector<T>& operator=(const MyVector<T>& src)
	{
		if (this == &src)
		{
			return *this;
		}

		delete[]_first;

		int size = src._end - src._first;//总的空间 个数
		_first = new T[size];
		_end = _first + size;
		int len = src._last - src._first;//有效长度
		_last = _first + len;
		for (int i = 0; i < len; ++i)
		{
			_first[i] = src._first[i];//把值赋过来
		}
		return *this;
	}
	void push_back(const T& val)//向容器末尾添加元素
	{
		if (full())
		{
			resize();
		}
		*_last++ = val;
	}
	void pop_back()//向容器末尾删除元素
	{
		if (empty())
		{
			return;
		}
		*_last--;
	}
	T getValueBack()const//返回容器末尾的值
	{
		return *(_last - 1);
	}
	bool full()const 
	{
		return _last == _end;
	}
	bool empty()const
	{
		return _first == _last;
	}
	int cursize()const//元素的个数
	{
		return _last - _first;
	}
private:
	T* _first;//数组起始位置
	T* _last;//最后一个有效元素的 后继
	T* _end;//数组最后一个空间位置的  后继

	void resize()//容器的2倍扩容
	{
		cout << "resize()" << endl;
		int size = cursize() * 2;
		T* newfirst = new T[size];
		for (int i = 0; i < cursize(); ++i)
		{
			newfirst[i] = _first[i];
		}
		delete[]_first;
		_first = newfirst;
		_last = _first + size / 2;
		_end = _first + size;
	}
};

int main()
{
	MyVector<>myvec;
	cout << "当前容器的容量是:" << myvec.cursize() << endl;
	for (int i = 0; i < 12; ++i)
	{
		myvec.push_back(rand() % 25);
	}
	cout << "当前容器的容量是:" << myvec.cursize() << endl;
	while (!myvec.empty())
	{
		cout << myvec.getValueBack() << " ";
		myvec.pop_back();
	}
	cout << endl;
	cout << "当前容器的容量是:" << myvec.cursize() << endl;
	return 0;
}

C++的学习心得和知识总结 第四章(完)_第12张图片
但是这里实现的vector 和STL里面的vector的巨大差别在于:
C++的学习心得和知识总结 第四章(完)_第13张图片
人家的模板类型参数:是默认的allocator 容器的空间配置器。
容器为什么要用allocator ? 不用行吗?

第四节:理解容器 空间配置器allocator的重要性

C++的学习心得和知识总结 第四章(完)_第14张图片
用Test类型 实例化一下这个vector容器。定义了一个容器,里面并没有添加元素。
这一个空的容器,竟然给我构造了5个 Test对象。出了作用域,又析构了5个Test对象。我现在就只是实例化一个这个空的vector容器
C++的学习心得和知识总结 第四章(完)_第15张图片
因为这里调用了 vector的构造函数,里面的new操作 :这里面不仅仅开辟空间,还构造了5个Test对象。 当然 我要这5个Test对象干嘛?若是用户传入了个 10000,那么他只是想 定义一个容器对象。然后就给人家构造了10000个对象,这不是人家想要的。

因此 因此 因此,现在的需求:需要把内存开辟 和 对象的构造分开处理。**在定义容器对象的时候,需要做的只是 底层开辟空间(只是给容器这个数组开辟空间,而不去构造对象)。**此刻要是用new的话,它直接把这两件事做完了。
而且更重要的是在vector的析构函数里面:

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

相当于把first指针指向的每一个元素都 当做有效的Test对象 去析构一遍了,如下图所示:数组里面并非都是对象,它不一定是满的。(正确的做法是:数组可能很长,但是对象并没有满。应该是在容器不需要的时候,把其中有效的对象给析构了,而非从头到尾都 当做有效的Test对象 去析构一遍。最后再把数组的内存给释放掉。)但是这里的 delete做了两件事:1 把数组的的每一个元素都 当做有效的Test对象 去析构一遍 2 释放数组内存(_first指向的堆内存)。
C++的学习心得和知识总结 第四章(完)_第16张图片
C++的学习心得和知识总结 第四章(完)_第17张图片

int main()
{
	Test t1, t2, t3;
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	//MyVector<>myvec;//定义了一个容器,里面并没有添加元素
	MyVector<Test>myvec;//用Test类型 实例化一下这个vector容器
	myvec.push_back(t1);
	myvec.push_back(t2);
	myvec.push_back(t3);
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	myvec.pop_back();
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	return 0;
}

此时的push_back 在逻辑上的实现为:按正常来说,容器只有内存(没有在内存上构造对象),然后push_back t1 t2 t3, 是在容器的底层内存上构造一个新对象和t1 t2 t3的值一样。但是现在在生成容器的时候,在内存上构造对象(满了都)。
C++的学习心得和知识总结 第四章(完)_第18张图片
在push_back t1 t2 t3的时候,相当于 在底层t1 t2 t3这3个给底层的这些Test对象 赋值 操作。这明显 逻辑上是错的。

正确的是:在生成容器的时候,容器只有内存(没有在内存上构造对象)。在push_back t1 t2 t3的时候,直接用的是 拷贝构造函数。

现在很麻烦的一点是:这个Test对象有可能还占有了外部的资源。
C++的学习心得和知识总结 第四章(完)_第19张图片
但是我们的 pop_back 函数只是:做了一个自减。 下次添加对象的时候,直接把值写在了原来的空间里面。可是原来被减掉的那个Test对象,还占有着外部资源呢 (其指针被覆盖了)造成了内存泄漏。所以pop_back的时候,并没有将那个对象给析构掉。

	void pop_back()//向容器末尾删除元素
	{
		if (empty())
		{
			return;
		}
		_last--;
	}

而且也不应该使用delete去析构它:delete首先会析构它,然后还会free这个数组这块的堆内存。

正确的做法是:从容器里面删除一个元素,只是析构这个对象。并不能释放数组的堆内存(因为这是容器的内存)。
因此 此时的pop_back只应该做一件事:只需要析构对象。把对象的析构和内存(容器的堆内存)的释放分离开。

所以在容器的开发过程中:涉及内存管理,我们是不可能用 new 或者 delete的。
现在核心的问题:我们使用了new 和 delete。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
总的运行截图如下:
C++的学习心得和知识总结 第四章(完)_第20张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
容器的空间配置器allocator 做四件事情 内存开辟/内存释放 对象构造/对象析构

总的运行截图如下:
C++的学习心得和知识总结 第四章(完)_第21张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
运行代码如下:

// 定义容器的空间配置器,和C++标准库的allocator实现一样
//默认的内存管理都是free malloc(也可以采用内存池的方式)
template<typename T>
struct Allocator
{
	T* allocate(size_t size) // 只负责内存开辟
	{
		return (T*)malloc(sizeof(T) * size);
	}
	void deallocate(void* p) // 只负责内存释放
	{
		free(p);
	}
	void construct(T* p, const T& val) // 只负责对象构造
	{
		//在我们指定的地址里面构造一个值为val的对象
		//在一个已经存在 开辟好的内存上去构造一个值为val的对象
		new (p) T(val); // 定位new    T类型的拷贝构造
	}
	void destroy(T* p) // 只负责对象析构
	{
		p->~T(); // ~T()代表了T类型的析构函数
	}
};

/*
容器底层内存开辟,内存释放,对象构造和析构,
都通过allocator空间配置器来实现
*/

template<typename T=int,typename Alloc=Allocator<T>>//定义模板类型参数
class MyVector
{
public:
	MyVector(int size = 5)//底层开辟5个元素的空间
	{
		//需要把内存开辟和对象的构造分开处理
		//_first = new T[size];

		_first = _allocator.allocate(size);
		_last = _first;
		_end = _first + size;
	}
	~MyVector()
	{
		//析构容器里面有效的元素,再去释放_first指向的堆内存
		//delete[]_first;

		for (T* p = _first; p != _last; ++p)
		{
			//把_first指针指向的数组的有效元素进行析构操作
			_allocator.destroy(p);
		}
		_allocator.deallocate(_first);// 释放堆上的数组内存
		_first = _last = _end = nullptr;
	}
	MyVector(const MyVector<T>& src)
	{
		int size = src._end - src._first;//总的空间 个数
		//_first = new T[size];

		_first = _allocator.allocate(size);
		_end = _first + size;
		int len = src._last - src._first;//有效长度
		_last = _first + len;
		for (int i = 0; i < len; ++i)
		{
			//_first[i] = src._first[i];//把值赋过来

			_allocator.construct(_first + i, src._first[i]);
		}
	}
	MyVector<T>& operator=(const MyVector<T>& src)
	{
		if (this == &src)
		{
			return *this;
		}

		//delete[]_first;

		for (T* p = _first; p != _last; ++p)
		{
		//把_first指针指向的数组的有效元素进行析构操作
			_allocator.destory(p);
		}
		_allocator.deallocate(_first);// 释放堆上的数组内存

		int size = src._end - src._first;//总的空间 个数
		//_first = new T[size];

		_first = _allocator.allocate(size);
		_end = _first + size;
		int len = src._last - src._first;//有效长度
		_last = _first + len;
		for (int i = 0; i < len; ++i)
		{
			//_first[i] = src._first[i];//把值赋过来

			_allocator.construct(_first + i, src._first[i]);
		}
		return *this;
	}
	void push_back(const T& val)//向容器末尾添加元素
	{
		if (full())
		{
			resize();
		}
		//*_last= val;
		//_last++;

		//_last指针指向的内存构造一个值为val的对象
		_allocator.construct(_last, val);
		_last++;
	}
	void pop_back()//向容器末尾删除元素
	{
		if (empty())
		{
			return;
		}
		//_last--;// 不仅要把_last指针--,还需要析构删除的元素
		--_last;
		_allocator.destroy(_last);

	}
	T getValueBack()const//返回容器末尾的值
	{
		return *(_last - 1);
	}
	bool full()const 
	{
		return _last == _end;
	}
	bool empty()const
	{
		return _first == _last;
	}
	int cursize()const//元素的个数
	{
		return _last - _first;
	}
private:
	T* _first;//数组起始位置
	T* _last;//最后一个有效元素的 后继
	T* _end;//数组最后一个空间位置的  后继
	Alloc _allocator;//定义的容器 空间配置器对象

	void resize()//容器的2倍扩容
	{
		cout << "resize()" << endl;
		int size = cursize() * 2;
		//T* newfirst = new T[size];

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

		for (T* p = _first; p != _last; ++p)
		{
		//把_first指针指向的数组的有效元素进行析构操作
			_allocator.destroy(p);
		}
		_allocator.deallocate(_first);// 释放堆上的数组内存

		_first = newfirst;
		_last = _first + size / 2;
		_end = _first + size;
	}
};
class Test
{
public:
	Test() { cout << "Test()" << endl; }
	~Test() { cout << "~Test()" << endl; }
	Test(const Test&) { cout << "Test(const Test&)" << endl; }
};
int main()
{
	Test t1, t2, t3;
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	//MyVector<>myvec;//定义了一个容器,里面并没有添加元素
	MyVector<Test>myvec;//用Test类型 实例化一下这个vector容器
	myvec.push_back(t1);
	myvec.push_back(t2);
	myvec.push_back(t3);
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	myvec.pop_back();
	cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
	return 0
}

但凡是容器,没有空间配置器是绝对不行的。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

你可能感兴趣的:(C++的学习心得和知识总结)