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

文章目录

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

模板

泛型编程

概念

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

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

函数模板

常规使用

template:定义模板的关键字

typename:定义模板类型的关键字

<> :指定模板的参数列表

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

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

template
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;

【C++】 模板(泛型编程、函数模板、类模板)_第1张图片

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

在函数体中加上

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

【C++】 模板(泛型编程、函数模板、类模板)_第2张图片

显式指定及默认值

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

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

}

调用函数可知:

【C++】 模板(泛型编程、函数模板、类模板)_第3张图片

确定模板类型:

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

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

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

}

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

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

	fun2();   //double
	fun2();    //float

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

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

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

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

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

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

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

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

多模板参数

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

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

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

	fun4();  //char char*

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

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

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

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

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

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

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

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

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

template
void fun8(M m) {
	cout << typeid(K).name() << endl;
	cout << typeid(T).name() << endl;
	cout << typeid(M).name() << "  " << m << endl;
}

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

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

	fun8('c');   //short int char

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

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

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

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

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

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

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

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

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

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

模板函数的声明和定义

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

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

template
void fun9();

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

void fun9(){
    cout<<__FUNCTION__<

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

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

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

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

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

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

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

【C++】 模板(泛型编程、函数模板、类模板)_第4张图片

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

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

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

template
void fun9();

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

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

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

【C++】 模板(泛型编程、函数模板、类模板)_第5张图片

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

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

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

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


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


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

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

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

【C++】 模板(泛型编程、函数模板、类模板)_第6张图片

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

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

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

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

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

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

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

【C++】 模板(泛型编程、函数模板、类模板)_第7张图片

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

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

//AA.cpp
//显示实例化:
template void fun11();

用函数模板优化冒泡排序

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

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;
}

【C++】 模板(泛型编程、函数模板、类模板)_第8张图片

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

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

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

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

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

两种排序规则函数:

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

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

优化后的冒泡排序:

template
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;
}

运行结果:

【C++】 模板(泛型编程、函数模板、类模板)_第9张图片

类模板

常规使用

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

template
class CTest {
public:
	T m_t;

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

};

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

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

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

CTest tst(1);

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

显式指定及默认值

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

template
CTest<> tst2('a');

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

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

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

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

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

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

多模板参数

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

template
class CTest2 {
public:
	T m_t;

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

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

	CTest2 tst4(12);

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

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

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

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

template
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::fun() {
	cout << __FUNCSIG__ << endl;
}

在主函数中使用这个函数

	CTest2 tst5('c');
	tst5.fun();

【C++】 模板(泛型编程、函数模板、类模板)_第10张图片

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

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

	CTest2 tst6(15);
	tst6.fun();

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

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

template
void CTest2::fun() {
	cout << __FUNCSIG__ << endl;
    cout << typeid(T).name() << "  " << typeid(K).name() << endl;
}

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

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

	CTest2 tst6(15);
	tst6.fun();

【C++】 模板(泛型编程、函数模板、类模板)_第11张图片

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

	template
	void fun2();

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

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

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

使用:

	CTest2 tst7(16);
	tst7.fun2();

【C++】 模板(泛型编程、函数模板、类模板)_第12张图片

嵌套的类模板

我们先来创建一个模板类

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

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

那么我们嵌套有三种方式

1.类和类型都能确定

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

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

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

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

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

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

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

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

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

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

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

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

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

优化链表

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

#include
using namespace std;

template
struct Node {
	T val;
	Node* pNext;

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

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

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

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

	operator bool() {
		return m_pNode;
	}

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

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

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

};

template
class CList {
public:
	Node* m_pHead;
	Node* m_pEnd;
	int   m_nLen;

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

	}
	~CList() {
		Node* 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* pNode = new Node(v);
		if (m_pHead) {   //非空链表
			m_pEnd->pNext = pNode;
		}
		else {//空链表
			m_pHead = pNode;
		}
		m_pEnd = pNode;
		m_nLen++;
	}
	void PopFront() {
		if (m_pHead) {
			Node* 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 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 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;
}

【C++】 模板(泛型编程、函数模板、类模板)_第13张图片

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

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
using namespace std;

template
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 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;
}

【C++】 模板(泛型编程、函数模板、类模板)_第14张图片

你可能感兴趣的:(C++进阶之路,c++,算法,链表,数据结构)