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

news2024/12/23 17:27:33

文章目录

    • 模板
      • 泛型编程
        • 概念
      • 函数模板
        • 常规使用
        • 显式指定及默认值
        • 多模板参数
        • 模板函数的声明和定义
        • 用函数模板优化冒泡排序
      • 类模板
        • 常规使用
        • 显式指定及默认值
        • 多模板参数
        • 类中成员函数的定义和声明
        • 嵌套的类模板
          • 1.类和类型都能确定
          • 2.类和类型都不能确定
          • 3.类能确定,类型不确定
        • 优化链表
        • 动态数组

模板

泛型编程

概念

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

在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)]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/562614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity3D下如何实现跨平台低延迟的RTMP、RTSP播放

技术背景 好多开发者&#xff0c;希望我们能探讨下Unity平台RTMP或RTSP直播流数据播放和录制相关的模块&#xff0c;实际上&#xff0c;这块流程我们已经聊过多次&#xff0c;无非就是通过原生的RTMP或者RTSP模块&#xff0c;先从协议层拉取到数据&#xff0c;并解包解码&…

常用的表格检测识别方法——表格结构识别方法(上)

第三章 常用的表格检测识别方法 3.2表格结构识别方法 表格结构识别是表格区域检测之后的任务&#xff0c;其目标是识别出表格的布局结构、层次结构等&#xff0c;将表格视觉信息转换成可重建表格的结构描述信息。这些表格结构描述信息包括&#xff1a;单元格的具体位置、单元格…

子网掩码计算方法

子网掩码是用来划分网络的一种方式&#xff0c;它是一个32位的二进制数&#xff0c;用于将IP地址分成网络地址和主机地址两部分。子网掩码中的1表示网络地址&#xff0c;0表示主机地址。计算子网掩码的方式取决于需要划分的网络数量和主机数量。 以下是一些计算子网掩码的示例…

【LeetCode热题100】打卡第2天:两数相加

两数相加 ⛅前言 大家好&#xff0c;我是知识汲取者&#xff0c;欢迎来到我们的LeetCode热题100刷题专栏&#xff01; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合初识算法与数据结构的新手和想要在短时间内高效提升的人&#xff0c;熟练…

【2】tensorflow基本概念及变量函数

目录 1 tensorflow运行机制 1.1 搭建计算图模型 计算图的概念 计算图的使用 新建计算图 1.2 在会话中执行计算图 会话的启动方式 1.3 指定计算图的运行设备 2 tensorflow数据模型 2.1 认识张量及属性 张量的类型 张量的阶 2.2 张量类型转换和形状变换 张量类型转换 张…

TwinCAT ENI 数据详解

使用倍福TwinCAT工具可以生成ENI&#xff0c;先对ENI的cyclic frame数据进行解释说明 需要提前了解EtherCAT报文格式&#xff0c;可参考下面文章 EtherCAT报文格式详解_ethercat listtype 1_EtherCat技术研究的博客-CSDN博客https://blog.csdn.net/gufuguang/article/details/…

理解HAL_UARTEx_ReceiveToIdle_IT的工作过程

先只看没错误发生, 没开DMA时候的情况 将会面临3种结局, 收满数据时候IDLE正好发生, 数据发多了, 数据已经收满时候IDLE还没发生IDLE发生了数据没收满, 首先: 接收开始 主要的动作是 1. status UART_Start_Receive_IT(huart, pData, Size); 开始中断接口 2.…

【国产虚拟仪器】基于 ZYNQ 的电能质量系统高速数据采集系统设计

随着电网中非线性负荷用户的不断增加 &#xff0c; 电能质量问题日益严重 。 高精度数据采集系统能够为电能质 量分析提供准确的数据支持 &#xff0c; 是解决电能质量问题的关键依据 。 通过对比现有高速采集系统的设计方案 &#xff0c; 主 控电路多以 ARM 微控制器搭配…

1992-2022年经过矫正的夜间灯光数据

夜间灯光数据是我们在各项研究中经常使用的数据&#xff01;我们平时使用的夜间灯光数据主要来源于NPP/VIIRS和DMSP/OLS两种渠道&#xff0c;我们之前也分享过这两种来源的夜间灯光数据&#xff0c;包括&#xff1a; 2012-2021年逐年的NPP/VIIRS夜间灯光数据2012-2021年逐月的…

MySQL查询性能优化之索引覆盖、索引下推、索引潜水、索引合并

索引覆盖 什么是索引覆盖 select的数据列只用从索引中就能够取得&#xff0c;不必读取数据行&#xff0c;换句话说查询列要被所建的索引覆盖。 如何实现索引覆盖&#xff1f; 最常见的方法就是&#xff1a;将被查询的字段&#xff0c;建立到联合索引&#xff08;如果只有一…

python---变量(2)

此处&#xff0c;首次使用“”对a进行设置值&#xff0c;也就是对a的初始化。 后续位置对a使用“”&#xff0c;实际上是对a赋值。 因此两行代码得到的结果显然是不同的&#xff01; 变量的种类 1.整数-int-根据数据大小自动扩容 python中的变量类型不需要显示声明&#…

关于 arduino 中的 constrain(x, a, b)函数

当我们需要将一个变量的值限制在某个范围内时&#xff0c;可以使用 constrain(x, a, b) 函数来实现。该函数可以将参数 x 的值限制在区间 [a, b] 之间&#xff0c;如果 x 小于 a&#xff0c;则返回 a&#xff0c;如果 x 大于 b&#xff0c;则返回 b&#xff0c;否则返回 x。下面…

第五篇、基于Arduino uno,获取超声波(HC04)传感器的距离数据——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;显示的是一个距离值&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;虽然超声波传感器形态各异&#xff0c;但是原理和代码都是适用的。 2、连线 说明&#xff1a;只…

材料力学-剪力和弯矩方向规定及关系

剪力和弯矩的方向规定方法 对水平梁的某一指定截面来说&#xff0c; 剪力&#xff1a;在它左侧的向上外力&#xff0c;或右侧的向下外力&#xff0c;将产生正的剪力&#xff1b;反之&#xff0c;即产生负的剪力。 自己的记法&#xff08;可以不按我的来&#xff09;&#xff1…

ChatGPT:你真的了解网络安全吗?浅谈网络安全攻击防御进行时之网络安全新总结

ChatGPT&#xff1a;你真的了解网络安全吗&#xff1f;浅谈网络安全攻击防御进行时 网络安全新总结 ChatGPT&#xff08;全名&#xff1a;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;美国OpenAI 研发的聊天机器人程序&#xff0c;是人工智能技术驱动的自…

ChatGPT在数据分析中的应用

最近&#xff0c;机器学习和人工智能技术在数据分析领域中发挥着越来越大的作用。而chatgpt正是这个领域最受欢迎的仿人聊天 AI 。但是&#xff0c;对于许多数据科学家和分析师来说&#xff0c;chatgpt并不是他们首选的工具。相反&#xff0c;pandas、sk-learn是数据科学家的最…

一起来聊聊ERP

聊聊ERP 哈喽&#xff0c;哈喽&#xff0c;大家好&#xff01;今天开始&#xff0c;我们就来讲ERP了。 什么是ERP ERP是Enterprise Resource Planning 的缩写&#xff0c;中文含义是企业资源计划。它代表了当前在全球范围内应用最广泛、最有效的一种企业管理方法&#xff0c;…

JVM系列-第12章-垃圾回收器

垃圾回收器 GC 分类与性能指标 垃圾回收器概述 垃圾收集器没有在规范中进行过多的规定&#xff0c;可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中&#xff0c;因此Java发展至今已经衍生了众多的GC版本。 从不同角度分析垃圾收集器&#xff0c;…

gym不渲染画面的解决方案(gym版本号0.26.2)

确认gym版本号 我安装了新版gym&#xff0c;版本号是0.26.2&#xff0c;不渲染画面的原因是&#xff0c;新版gym需要在初始化env时新增一个实参render_mode‘human’&#xff0c;并且不需要主动调用render方法&#xff0c;官方文档入门教程如下 import gym import numpy as n…

FreeRTOS学习之路,以STM32F103C8T6为实验MCU(第一章——FreeRTOS的基本框架)

学习之路主要为FreeRTOS操作系统在STM32F103&#xff08;STM32F103C8T6&#xff09;上的运用&#xff0c;采用的是标准库编程的方式&#xff0c;使用的IDE为KEIL5。 注意&#xff01;&#xff01;&#xff01;本学习之路可以通过购买STM32最小系统板以及部分配件的方式进行学习…