【C++】 模板(泛型编程、函数模板、类模板)

模板

泛型编程

概念

通过数据类型和算法将算法从数据结构中抽象出来程序写得尽可能通用用不变的代码完成一个可变的算法。屏蔽掉数据和操作数据的细节让算法更为通用让编程者更多地关注算法的结构而不是在算法中处理不同的数据类型总之是不考虑具体数据类型的一种编程方法

在C++中模板为泛型程序设计奠定了关键的基础。使用模板需要用到两个关键字templatetypename写法template template告诉编译器将要定义一个模板<>中的是模板参数列表类似于函数的参数列表关键字typename看作是变量的类型名该变量接收类型作为其值把Type看作是该变量的名称是一个通用的类型。

函数模板

常规使用

template定义模板的关键字

typename定义模板类型的关键字

<> 指定模板的参数列表

在学模板之前如果我们想做一个两数加法那么要在函数的参数列表里表明参数的类型如果想要函数传入的参数不同但函数依然能实现我们只能用函数重载的办法

但是学过模板之后我们可以在函数上边定义一个模板然后用模板类型代替函数参数列表中的类型这样函数就可以根据传入的类型自动匹配类型了

template<typename T>
T add(T a, T b) {
	return a + b;
}
	cout << add(10, 20) << endl;

	double a = 1.1;
	double b = 2.2;
	cout << add(a, b) << endl;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlxMO7Vd-1684902169878)(C++.assets/image-20230521092353748.png)]

我们也可以用typeid来查看当前T的类型

在函数体中加上

	cout << typeid(T).name() << endl;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lyi6toaz-1684902169878)(C++.assets/image-20230521092501250.png)]

显式指定及默认值

并且也可以在模板参数列表中为模板指定默认值然后就可以用模板类型定义对象了

template<typename T = short>
void fun() {
	T t = 0;
	cout << typeid(T).name() << endl;

}

调用函数可知

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vZh3w3Fu-1684902169879)(C++.assets/image-20230521092621269.png)]

确定模板类型

  1. 函数如果带有形参且在形参中使用了模板则可以通过实参自动推导在函数调用时确定
  2. 函数模板可以指定默认的类型

针对于这个模板类型假如说有个极端的情况没有去指定默认的然后也没有传参所以无法自动推导那么此时该怎么办呢。

template<typename T>
void fun2() {
	T t = 1.2;
	cout << typeid(t).name() << "  "<< t << endl;

}

所以这里还有第三种确定模板类型的方式就是在调用函数时可以显示的指定模板类型

在函数名的后面加一个<>这就相当于对应模板的参数列表然后跟函数传参的方式相同在这里可以传递参数到模板的参数列表中也可以形象的理解成形参和实参

	fun2<double>();   //double
	fun2<float>();    //float

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2g94A6kp-1684902169879)(C++.assets/image-20230522124541426.png)]

我们现在学到了三种确定模板类型的方式那么如果这三种类型同时存在的话那么会以谁为准呢先后顺序是什么呢

所以再加一个函数和函数模板用于测试

我们在这里既指定默认值还传递参数并且也显示的指定

template<typename T = char>
void fun3(T t) {
	cout << typeid(t).name() << "  " << t << endl;
}
	fun3<long>(10);   //long
	fun3<>(10);       //int

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n4ZfTN5D-1684902169879)(C++.assets/image-20230522125036393.png)]

测试后我们发现如果有显示指定那么以显示指定为主如果没有显示指定那么以传递参数自动推导为主

所以三种方式之间的优先级为显式指定>实参自动推导>默认类型

多模板参数

接下来要了解的是针对于这个模板来说如果一个不够用我们可以指定多个

template<typename T , typename M>
void fun4() {
	cout << typeid(T).name() << endl;
	cout << typeid(M).name() << endl;
}

我们可以通过显示指定来传递两种类型

	fun4<char,char*>();  //char char*

再来写一个函数然后将模型参数其中一个参数以实参推导的方式传入另一个显示指定

template<typename T, typename M>
void fun5(M m) {
	cout << typeid(T).name() << endl;
	cout << typeid(M).name() << "  " << m << endl;
}
	fun5<long>('a');    //long char

这里不能用实参推导前面的参数因为显式指定会将其覆盖而不是跳过他给第二个参数指定

我们再来添加一个模板参数我们想来看看这里参数的默认类型是不是跟函数指定默认类型时一样需要从右向左依次指定中间不能有间断

template<typename T, typename M, typename K = float>
void fun6(M m) {
	cout << typeid(T).name() << endl;
	cout << typeid(M).name() << "  " << m << endl;
	cout << typeid(K).name() << endl;
}
	fun6<long>('b');    //long char float

这样是可以的再来一种顺序

template<typename T, typename K = float, typename M>
void fun7(M m) {
	cout << typeid(T).name() << endl;
	cout << typeid(K).name() << endl;
	cout << typeid(M).name() << "  " << m << endl;
}
	fun7<long>('b');     //long float char

我们发现在中间指定默认值也不会出现问题会显示指定第一个参数然后推导第三个参数

那么如果将默认指定放在第一位呢看显示指定是否会覆盖掉默认指定

template<typename K = float, typename T, typename M>
void fun8(M m) {
	cout << typeid(K).name() << endl;
	cout << typeid(T).name() << endl;
	cout << typeid(M).name() << "  " << m << endl;
}

经过测试我们发现显示指定无法跳过K直接传T还是会将第一位先覆盖掉

所以只能显示的指定两个参数才可以

	fun8<short, int>('c');   //short int char

这样才可以但是我们可以证明默认指定类型确实没有强制的顺序要求只要不违背优先级即可

那么为什么它没有强制的顺序要求呢而函数参数就有差别在哪呢

回顾函数传参方式他只有两种我们传递参数是从左往右而指定默认值只有从右往左依次指定才能实现互补

而函数模板的传参方式不只有默认和显示两种它还可以根据实参自动推导那么互补的方式就被自动推导方式打乱了但只要遵循优先级来传递参数就不会出错

但我们通过研究这几个例子我们发现虽然没有强制的传递顺序但是那种顺序比较理想呢

我们可以肯定的是fun8的那种传递方式是最不好的在fun8种我们只想指定T不想指定K但还跳不过去只能又给K指定一下那么K默认的就浪费了没用上

那么最理想的顺序是什么呢如果我们将默认指定放在最右边那么如果我们想通过显示指定去更改默认值那就跳不过中间的实参推导那就需要再将实参推导的也显示指定一下但是在指定的时候如果我们指定的类型跟推导的类型不同那么就可能会发生冲突

以fun6为例下面这种操作就会出现冲突

	long* p = nullptr;
	fun6<int, int, double>(p);   //实参推导 和 显式指定冲突了

所以我们最终得出最理想的顺序是fun7那样的顺序显示指定的放在最前面实参推导的放在最后面然后将默认类型放在中间

模板函数的声明和定义

假如这个函数的声明和定义是分开的情况下那么这个模板该何去何从呢

我们正常声明一个模板函数然后在主函数下方去定义然后调用

template<typename K>
void fun9();

int main(){
    fun9();
    return 0;
}

void fun9(){
    cout<<__FUNCTION__<<endl;
}

我们会发现调试之后出现了这样一个错误error C2783: “void fun9(void)”: 未能为“K”推导 模板 参数

就是说我们的K没有指定所以我们需要在函数的定义和调用的函数名后面都加上个<数据类型>

void fun9<int>() {
	cout << __FUNCSIG__ << endl;
}
fun9<int>();

但是我们又会发现如果我们在调用函数的时候传递的模板参数跟定义时候的不同那么还会出现错误error LNK2019: 无法解析的外部符号 “void __cdecl fun9(void)” (??$fun9@D@@YAXXZ)函数 _main 中引用了该符号我们知道无法解析外部符号的错误原因是只声明未定义造成的

也就是调用模板函数时如果模板参数与定义时的模板参数不同是匹配不到一起去的这里就体现了模板函数的实例化

void fun9<char>() {
	cout << __FUNCSIG__ << endl;
}	
fun9<char>();

所以在这之前我们没有将模板函数的声明定义拆开时系统是根据我们传递的模板参数不同又创建了不同的函数这点我们在写代码阶段是看不出来的要在编译汇编的文件才能体现出来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLFdcbyq-1684902169879)(C++.assets/image-20230522200147480.png)]

如果我们只定义这个函数但是不去调用比如说将fun9;注释掉那么还会出现错误error C2768: “fun9”: 非法使用显式模板参数

这其实也是实例化的一个特点就是按需实例化意思是你用到什么类型他去给你生成什么类型的函数如果不用就不会生成

那么如果我们想要一个通用的定义声明方式就需要把模板在定义处也写一份

template<typename K>
void fun9();

int main(){
    fun9<int>();
    fun9<char>();
    return 0;
}

template<typename K>
void fun9<K>() {
	cout << __FUNCSIG__ << endl;
	cout << typeid(K).name() << endl;
}

那么这样就是一个比较通用的声明定义方式了运行一下看看编译结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBZUQRZU-1684902169880)(C++.assets/image-20230522201150362.png)]

我们尝试在头文件中去声明源文件中定义

在之前我们定义声明函数的时候定义一定要放在源文件中如果放在头文件并且头文件被多个源文件包含那么就会出现重定义的错误

那我们将这个模板函数的定义声明都放在头文件中会出现上述错误吗我们在头文件中写一个模板函数然后在源文件中去使用这个函数然后在另一个源文件中也去使用这个函数

//AA.h
#pragma once
#include<iostream>
using namespace std;


template<typename T>
void fun10() {
	cout << __FUNCSIG__ << endl;
	cout << typeid(T).name() << endl;
}
//AA.cpp
#include"AA.h"


void testfun() {
	fun10<char>();
}
//main.cpp
#include"AA.h"
int main() {
    fun10<int>();

    void testfun();
	testfun();
	return 0;
}

我们发现运行是没有问题的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qn2MW0ze-1684902169880)(C++.assets/image-20230522202332933.png)]

那如果将定义放在源文件呢

//AA.h
template<typename T>
void fun11();
//AA.cpp
template<typename T>
void fun11() {
	cout << __FUNCSIG__ << endl;
	cout << typeid(T).name() << endl;
}
//main.cpp
#include"AA.h"
int main(){
    fun11<long>();
    return 0;
}

运行后我们发现会出现无法解析外部符号的错误也就是没有定义但是我们在一个源文件中去分开声明定义就是可行的所以我们得出他的实例化是按照编译单元.cpp按需实例化的也就是只能在同一个cpp中去声明定义才可以

那要怎么去解决这个问题呢

我们可以在定义的源文件中去通过别的函数使用以下这个函数这样在源文件中就有实例化了那么就可以使用了

//AA.cpp
void testfun() {
	fun11<long>();
}

这样在主函数中就能使用了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6epWhOfo-1684902169880)(C++.assets/image-20230522203933791.png)]

当然这不是最终的解决方案我们在别的文件中想要使用这个函数但是还要在自己的源文件中先使用一下这样未免有些说不过去

所以最终的解决方案可以是像fun10一样将声明定义写到一起这样是比较完美的另一个是既然在源文件中缺实例化那么我们就将实例化显示的写出来

//AA.cpp
//显示实例化
template void fun11<long>();

用函数模板优化冒泡排序

我们先正常来写一个冒泡排序

void BubbleSort(int * arr,int len) {
    if (!arr) return;
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < len - i - 1; j++) {
			if (arr[j] > arr[j+1]) { //当前的比下一个大则交换
				int temp = arr[j];
				arr[j] = arr[j+1];
				arr[j + 1] = temp;
			}
		}
	}
}

这里解释一下冒泡循环的循环条件外层循环小于数组长度-1是因为如果有十个数那只需要冒泡9次最后一个数自然就放在固定位置了内层循环len-i是每经过一次外层循环就会排号一个数所以len-i就是剩余需要排的数的数量再-1是因为十个数只需要交换九次

然后在主函数中测试一下

int main() {
	int arr[10] = { 6,2,3,0,5,8,9,1,4,7 };

	BubbleSort(arr, 10);
	for (int v : arr) {
		cout << v << "  "; //0  1  2  3  4  5  6  7  8  9
	}
	cout << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RWzMlzQK-1684902169880)(C++.assets/image-20230522214627599.png)]

现在是个升序排序如果想要降序那么直接将函数中的判断条件的>改为<即可

那么此时这个冒泡排序只是针对于整型来排的但是根据泛型编程的思想我们发现别的类型也可以根据这套逻辑进行排序于是我们用模板来给这个函数做个升级优化

那我们可以使用模板将传入的类型变成通用的那么我们可不可以也将升序降序也纳入到通用的进来呢

现在决定升序降序的就是判断条件中的表达式那么我们就可以将这个表达式抽离出来然后通过一个函数封装起来以这个函数作为判断条件但此时还是没有将升序降序结合起来

现在的情况是直接调用在判断条件中写什么规则就按什么规则排所以我们想通过函数指针间接调用不同的函数那就在参数中增加一个函数指针然后就可以通过传递参数的不同来调用不同的排序方式了

两种排序规则函数

template<typename T>
bool rule(T a,T b) {
	return a > b;
}

template<typename T>
bool rule2(T a, T b) {
	return a < b;
}

优化后的冒泡排序

template<typename T>
void BubbleSort(T * arr,int len,bool(*p_fun)(T,T)) {
	if (!arr) return;
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < len - i - 1; j++) {
			if ((*p_fun)(arr[j],arr[j+1])) {
				T temp = arr[j];
				arr[j] = arr[j+1];
				arr[j + 1] = temp;
			}
		}
	}
}

使用方法

int main() {
	int arr[10] = { 6,2,3,0,5,8,9,1,4,7 };
	BubbleSort(arr, 10,&rule);
	for (int v : arr) {
		cout << v << "  "; //0  1  2  3  4  5  6  7  8  9
	}
	cout << endl;

	double arr_d[10] = { 6.2,2.4,3.6,0,5.1,8.3,9.7,1.8,4.4,7.9 };
	BubbleSort(arr_d, 10,&rule2);
	for (double v : arr_d) {
		cout << v << "  "; 
	}
	cout << endl;

	return 0;
}

运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RMK83fb-1684902169881)(C++.assets/image-20230522221758992.png)]

类模板

常规使用

同样是这个模板泛型编程他不但可以用到函数中去也可以用在类中

template<typename T>
class CTest {
public:
	T m_t;

	CTest(T t) :m_t(t) {
		cout << typeid(t).name() << "  " << t << endl;
	}

};

模板类型可以替换类内的任意地方定义的类型包括成员属性成员函数。

类中成员属性若为模板类型那么我们可以定义带参数的构造让调用者去指定初始化值

在定义对象的时候必须使用<>显示的指定模板类型

CTest<int> tst(1);

在使用的时候我们发现吗这种使用方式与链表映射表十分相似

显式指定及默认值

通过上面我们发现类模板也可以显示指定模板类型那我们也测试一下在模板参数中设置默认值

template<typename T = char>
CTest<> tst2('a');

我们发现也可以指定默认值但是与函数模板不同的是在使用的时候<>不可以省略就算有默认值我们不去显式指定也要用<>传递个空

那我们记得在函数模板中还有一种传参方式就是根据实参推导那我们测试一下这里是否能用

	short s = 12;
	CTest<> tst3(s);

经测试发现这种方式不可以行

所以类模板传参方式只有两种

  1. 显式指定
  2. 指定默认的类型

多模板参数

多参数模板我们主要是测试一次有没有强制的顺序规则

template<typename T /*= char*/,typename K = long>
class CTest2 {
public:
	T m_t;

	CTest2(T t) :m_t(t) {
		cout << typeid(t).name() << "  " << t << endl;
		cout << typeid(K).name() << endl;
	}
};

我们写一个模板类然后现在模板中放置两个参数我们先试一下只给左边参数指定默认值那么如果按函数模板的想法就是我们想给第二个参数显式指定就跳不过第一个参数那么我们就是一下显式指定两个参数

	CTest2<int, long> tst4(12);

测试后发现会出现错误那我们在把右边的参数也指定默认值测试发现错误没有了再将左边的默认值注释掉也不会出现错误

所以得出结论多模板参数指定默认的模板类型有顺序要求从右向左依次指定不能有间断

类中成员函数的定义和声明

我们在类中声明一个函数那这个成员函数在类外要怎么定义呢

template<typename T /*= char*/,typename K = long>
class CTest2 {
public:
	T m_t;

	CTest2(T t) :m_t(t) {
		cout << typeid(t).name() << "  " << t << endl;
		cout << typeid(K).name() << endl;
	}

	void fun();
};

如果没加模板的话我们在类外定义只需要在函数名前加类名作用域即可 但是对于类模板来说我们定义的时候要在类名后面加上<>指定模板类型

void CTest2<char,int>::fun() {
	cout << __FUNCSIG__ << endl;
}

在主函数中使用这个函数

	CTest2<char, int> tst5('c');
	tst5.fun();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fivLGOH9-1684902169881)(C++.assets/image-20230523123817498.png)]

所以这样在类外去定义是可以的不过还不是一个通用的和函数模板实例化那里一样现在只是针对一种模板的定义和实现

就是如果我们在使用的时候模板类型对应不上定义函数的模板类型那应该会出现错误

	CTest2<int, int> tst6(15);
	tst6.fun();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PM5jI05b-1684902169881)(C++.assets/image-20230523124122027.png)]

所以我们要把模板类成员函数的定义改为通用的这里同函数模板那里差不多

template<typename T /*= char*/, typename K/* = long*/>
void CTest2<T, K>::fun() {
	cout << __FUNCSIG__ << endl;
    cout << typeid(T).name() << "  " << typeid(K).name() << endl;
}

把模板拿过来然后用模板参数替换掉类名后面的参数并且将模板参数中的默认值去掉参考函数的声明和定义在声明时指定默认值定义不指定

这样我们之前因为模板类型不同而找不到定义的问题就不会出现了

	CTest2<int, int> tst6(15);
	tst6.fun();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QkKUvXu9-1684902169881)(C++.assets/image-20230523124856631.png)]

还有一种情况我们再来一个函数这个模板类成员函数也有自己的模板

	template<typename M>
	void fun2();

那对于他来说在类外定义该怎么办呢

template<typename T , typename K >
template<typename M>
void CTest2<T,K>::fun2(){
	cout << __FUNCSIG__ << endl;
	cout << typeid(T).name() << "  " << typeid(K).name() << "  " << typeid(M).name() << endl;
}

注意如果函数模板和类模板同时存在先类模板后函数模板顺序不能调换模板也不能合并

使用

	CTest2<short, double> tst7(16);
	tst7.fun2<int*>();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dW1cyWA-1684902169882)(C++.assets/image-20230523125732593.png)]

嵌套的类模板

我们先来创建一个模板类

template<typename T>
class A{
public:
	T m_t;
	A():m_t(0){}
};

这里我们设置一个模板用来决定成员属性的类型

那么我们嵌套有三种方式

1.类和类型都能确定

这种方式就是我们不用在这个外层嵌套类上设置模板了直接就在成员属性创建一个模板类的成员属性就可以了然后构造函数中要传递一个这个模板类的对象来给成员属性初始化

class B {  //类和类型都能确定
public:
	A<int> m_a;
	B(A<int> a):m_a(a){}
};

使用方法就是定义一个A模板类类型的变量然后将这个变量传入构造函数中就可以了

	A<int> aa;
	B b(aa);
	cout << b.m_a.m_t << "  " << typeid(b.m_a.m_t).name() << endl;  //0  int
2.类和类型都不能确定

这种方式就是要把内层模板类变成一个通用的我们既不规定成员属性是哪个类的也不规定向那个类模板中传入什么类型

template<typename T>
class C {    //类和类型都不能确定
public:
	T m_a;
	C(T a) :m_a(a) {}
};

使用方法还是先定义一个A模板类类型的对象不过像构造函数传参时要向模板参数传入整个A模板类的类型就是用这个类型替换掉C类中的T

	A<long> a2;
	C<A<long>> c(a2);
	cout << c.m_a.m_t << "  " << typeid(c.m_a.m_t).name() << endl;  //0  long
3.类能确定类型不确定

就是能确定成员属性要定义为哪个类的但是往内层类模板传入的参数通用

template<typename T>
class D {  //类能确定类型通用
public:
	A<T> m_a;
	D(A<T> a) :m_a(a) {}
};

使用方法先定义一个A类对象然后要在D类模板参数中传入跟A类模板参数一样的类型并将对象传入构造函数

	A<double> a3;
	D<double> d(a3);
	cout << d.m_a.m_t << "  " << typeid(d.m_a.m_t).name() << endl;  //0  double

总之就是我们会通过给外层模板类传递参数来决定内层模板类成员属性的类型

优化链表

记得之前我们用链表封装过一次迭代器那个链表的节点只能装整型那现在学过模板之后我们来对其优化一下

#include<iostream>
using namespace std;

template<typename T>
struct Node {
	T val;
	Node* pNext;

	Node(T v):val(v),pNext(nullptr){   //构造函数初始化
	}
};

template<typename T>
class CIterator {
private:
	Node<T>* m_pNode;
public:
	CIterator():m_pNode(nullptr){}
	CIterator(Node<T>* pNode) :m_pNode(pNode) {}

	Node<T>* operator=(Node<T>* pNode) {
		m_pNode = pNode;
		return m_pNode;
	}

	bool operator!=(Node<T>* pNode) {
		return m_pNode != pNode;
	}
	bool operator==(Node<T>* pNode) {
		return m_pNode == pNode;
	}

	operator bool() {
		return m_pNode;
	}

	T& operator*() {
		return m_pNode->val;
	}

	//左++
	Node<T>* operator++() {
		m_pNode = m_pNode->pNext;
		return m_pNode;
	}

	Node<T>* operator++(int) {
		Node<T>* pTemp = m_pNode;  //先标记一下
		m_pNode = m_pNode->pNext;  //后去移动
		return pTemp;
	}

};

template<typename M>
class CList {
public:
	Node<M>* m_pHead;
	Node<M>* m_pEnd;
	int   m_nLen;

public:
	CList():m_pHead(nullptr),m_pEnd(nullptr),m_nLen(0){

	}
	~CList() {
		Node<M>* pNode = nullptr;
		while (m_pHead) {   //如果链表不为空循环
			pNode = m_pHead;   //标记头
			m_pHead = m_pHead->pNext;  //头向后移动
			delete pNode;   //删除标记的
		}
		m_pHead = nullptr;
		m_pEnd = nullptr;
		m_nLen = 0;
		pNode = nullptr;
	}

	void PushBack(M v) {
		Node<M>* pNode = new Node<M>(v);
		if (m_pHead) {   //非空链表
			m_pEnd->pNext = pNode;
		}
		else {//空链表
			m_pHead = pNode;
		}
		m_pEnd = pNode;
		m_nLen++;
	}
	void PopFront() {
		if (m_pHead) {
			Node<M>* pNode = m_pHead;   //标记头也是将来要删除的
			if (m_pHead == m_pEnd) {  //1个节点
				m_pHead = m_pEnd = nullptr;
			}
			else {   //多个节点
				m_pHead = m_pHead->pNext;  //向后移动
			}

			delete pNode;   //删除标记的
			pNode = nullptr;
			m_nLen--;
		}
	}

	void ShowList() {
		//迭代器
		CIterator<M> ite(m_pHead);     //Node* pNode = m_pHead;  //初始化构造函数
		//ite = m_pHead;              //pNode = m_pHead;   //operator=
		while (ite != nullptr) { //operator!=  operator==  operator bool
			//cout << pNode->val << "    ";   //*pNode   operator*
			cout << *ite << "   ";
			//pNode = pNode->pNext;   //operator++  operator(int)
			++ite;
		}
		cout << endl;
	}

	int GetLength() { return m_nLen; }
};

我们先来尝试一下传入long类型的结点

int main() {
	CList<long> lst;
	lst.PushBack(1);
	lst.PushBack(2);
	lst.PushBack(3);
	lst.PushBack(4);

	cout << lst.GetLength() << endl;

	lst.ShowList();

	lst.PopFront();
	lst.PopFront();

	cout << lst.GetLength() << endl;
	lst.ShowList();
    	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EYcErv18-1684902169882)(C++.assets/image-20230523211501452.png)]

我们再创建一个自创类来看看能否传入

class A {
public:
	char c;
	A():c('a'){}
	A(char cc):c(cc){}
};

但是我们现在*ite无法直接接到对象的成员属性我们需要重载一下<<操作符

ostream& operator<<(ostream& os, A& a) {
	os << a.c;
	return os;
}

并且要在重载*操作符时返回值加上引用不然会出现浅拷贝问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhGMBU0g-1684902169882)(C++.assets/image-20230523212110499.png)]

动态数组

什么是动态数组呢正常数组我们都知道那个可以理解为静态的静态的就是一旦确定了长度之后在运行过程中长度就是不可变的所以动态数组就是长度可变的他的底层也是由静态数组实现的只不过有重新分配的功能

数组中有容量和使用量使用量永远是小于容量的那么容量如果发生变化数组就会重新分配空间还有就是我们对于数组的增加和删除一般都是在尾部进行的

#include<iostream>
using namespace std;

template<typename T>
class CDynamicArray {
public:
	T* m_pArr;
	int m_size;  //使用量
	int m_capacity;  //容量
public:
	CDynamicArray(int capa = 0) :m_size(0), m_capacity(capa), m_pArr(capa > 0 ? new T[capa]() : nullptr) {}
	~CDynamicArray() {
		if (m_pArr) {
			delete[]m_pArr;
		}
		m_pArr = nullptr;
		m_size = m_capacity = 0;
	}

	void PushBack(T t) {
		if (m_size < m_capacity) {  //容器没满
			m_pArr[m_size++] = t;
		}
		else {  //满了 扩容
			int oldSize = m_size++;
			//计算新的容量
			m_capacity = m_size >(m_capacity + m_capacity / 2) ? m_size : (m_capacity + m_capacity / 2);
			
			T* pTemp = new T[m_capacity]();  //申请新空间
			for (int i = 0; i < oldSize; i++) {   //依次拷贝旧值
				pTemp[i] = m_pArr[i];
			}

			delete[]m_pArr;  //删除旧的空间
			m_pArr = pTemp;  //接手新的空间


			m_pArr[oldSize] = t;  //添加新值

		}
	}
	void PopBack() {
		if (m_pArr && m_size > 0) {
			m_size--;  //把他认为是删除并没有真正删除空间在之后赋值会自动将旧值覆盖掉
		}
	}
	int GetSize() { return m_size; }

	int GetCapacity() { return m_capacity; }

	T& operator[] (int index) {  //重载[]操作符使类外可以像正常使用数组一样通过下标找到元素
		return m_pArr[index];
	}

	T* begin() {
		return &m_pArr[0];
	}

	T* end() {
		return &m_pArr[m_size];
	}
};

其中包含的方法有构造析构、尾部添加、尾步删除、获取长度、获取容量重载[]操作符使类外可以正常通过下标查询元素然为了能够使增强的范围for能够正常遍历数组我们加了个begin和end的方法

我们在添加元素时如果使用量大于等于容量那么会以1.5倍扩容

测试

int main() {
	CDynamicArray<int> arr;
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;
	arr.PushBack(1);
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;
	arr.PushBack(2);
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;
	arr.PushBack(3);
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;
	arr.PushBack(4);
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;
	arr.PushBack(5);
	cout << arr.GetSize() << "  " << arr.GetCapacity() << endl;

	for (int i = 0; i < arr.GetSize(); i++) {
		cout << arr[i] << "  ";
	}cout << endl;

	arr.PopBack();
	arr.PopBack();
	arr.PopBack();

	for (int i = 0; i < arr.GetSize(); i++) {
		cout << arr[i] << "  ";
	}cout << endl;

	//int arr1[10] = { 0 };
	//for (int v : arr1) {  //正常数组支持增强的范围for遍历

	//}
	arr.PushBack(50);
	arr.PushBack(60);

	for (int v : arr) {  //动态数组要想也支持用增强的范围for来遍历就要加上begin、end函数
		cout << v << "  ";
	}cout << endl;


	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BF6oIGsZ-1684902169882)(C++.assets/image-20230524122212152.png)]

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“【C++】 模板(泛型编程、函数模板、类模板)” 的相关文章