【c++ 篇】学习笔记之模板--什么是c++模板-函数模板-类模板

目录

一、模板解决的问题

二、什么是c++模板

泛型编程:

c++模板的分类

三、函数模板

函数模板概念

函数模板的格式

函数模板的原理

函数模板的实例化

隐式实例化

显示实例化:

模板支持多个模板参数

模板参数的匹配原则

四、类模板

类模板的概念

类模板的格式

类模板的实例化

类模板的分离编译

一、模板解决的问题

        在我们学习c++中,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下。而函数重载规定是名字相同,参数的顺序、类型、数量不同,与返回值无关。仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

        那么,模板的出现,就让这些问题有了解决方案。

        补充:如果底层的实现机制是一样的可以使用模板,否则考虑重载:

模板和重载类似。模板类里面的操作不一样,实现同样的事,实现的规则不同,这种情况就不可以使用模板。

例1:实现加法功能时,字符串类型的相加实现起来是将两个字符串拼接,而整型的相加就是数字相加。两者实现起来的规则不同,就不能使用模板。

例2:排序:用int类型的数字排序直接比较大小即可,给学生排序,创建一个学生的结构体,排序规则是先比较成绩,成绩一样的比较学号。这种函数里面需要实现的排序方式不相同。就不可以使用模板。

二、什么是c++模板

        程序设计中经常会用到一些程序实体:他们的实现和所完成的功能基本相同,不同的仅仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制

        模板——是泛型程序设计的基础(泛型generic type——通用类型之意)

        函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类型的代码。

泛型编程:

        编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

c++模板的分类

1. 函数模板:使用泛型参数的函数

2. 类模板:使用泛型参数的类

三、函数模板

函数模板概念

        函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本

        通过函数模板,可以编写一种通用的函数定义,使其能够适用于多种数据类型,从而提高代码的复用性和灵活性。

函数模板的格式

template
返回值类型 函数名(参数列表)
{
	...
}
注意:template是定义模板关键字,typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

例如:实现交换功能就可以使用函数模板

#include
using namespace std;
template
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}
int main()
{

	int x = 2, y = 3;
	cout << x << " " << y << endl << endl;
	Swap(x, y);
	cout << x << " " << y << endl << endl;
	cout << "-------------------------------" << endl;

	char a = 'a', b = 'b';
	cout << a << " " << b << endl << endl;
	Swap(a, b);
	cout << a << " " << b << endl << endl;
	cout << "-------------------------------" << endl;


	double c = 2.1, d = 3.1;
	cout << c << " " << d << endl << endl;
	Swap(c, d);
	cout << c << " " << d << endl << endl;
	cout << "-------------------------------" << endl;
	return 0;
}

【c++ 篇】学习笔记之模板--什么是c++模板-函数模板-类模板_第1张图片

 通过这个函数模板,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模板可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。

函数模板的原理

上述代码中,他们调用的真的是同一个函数么?

        当然不是,这里三次swap不是调用同一个函数,其实swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用。可以通过查看汇编代码时发现被调用的函数模板生成了两个不同的函数,他们有着不同的地址。

        函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

【c++ 篇】学习笔记之模板--什么是c++模板-函数模板-类模板_第2张图片

         在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用模板函数时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

函数模板的实例化

 用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:

        1. 隐式实例化                              2. 显式实例化

隐式实例化

 隐式实例化:让编译器根据实参推演模板参数的实际类型

template
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2); //编译器推出T是int
	Add(d1, d2); //编译器推出T是double
    return 0;
}

注意:但是我调用的时候如若这样就会出错:

int main()
{
    int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, d1); //err 编译器推不出来
	/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    */
}

对于这种问题,有两种处理方式:

方法一:使用强制转换

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
}

方法二:使用显式实例化

显示实例化:

显示实例化:在函数名后的<>中指定模板参数的实际类型

template
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    //显示实例化
	Add(a1, d1); //double隐式类型转换成int 
	Add(a1, d2); 
    return 0;
}

模板支持多个模板参数

template //两个模板参数
void Func(const K& key, const V& value)
{
	cout << key << ":" << value << endl;
}
int main()
{
	Func(1, 1); //K和V均int
	Func(1, 1.1);//K是int,V是double
	Func(1, 'A'); //多个模板参数也可指定显示实例化不同类型
}

模板参数的匹配原则

原则1:一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
//通用加法函数
template
T Add(T left, T right)
{
	return left + right;
}
int main()
{
	Add(1, 2); //会调用哪个Add函数?
}

这就产生一个问题:我调用参数是int类型的add函数,调用的是模板函数add,还是专门的add?

        通过反汇编得知,调用的是专属add函数。得出结论:编译器在调用时,有现成的就调用现成的,没有就套用模板。当然,我们也有办法强制让编译器走模板函数,如下:

void Test()
{
    Add(1, 2); // 与非模板函数匹配,编译器不需要特化
    Add(1, 2); // 调用编译器特化的Add版本
}

原则2:对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

四、类模板

类模板的概念

        类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似。类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。

类模板的格式

定义格式:函数模板加在函数上,对于类模板加在类上

template
class 类模板名
{
    // 类内成员定义
};

如果不使用模板,下述代码只能初始化一个具有整型数据的栈,如果想要放一些浮点型的数据进来的话也做不到,代码如下:

// C++ 正常情况下,如果需要不同类型的类(int char double 等)
typedef int DataType;//给int类型起别名,为了方便后续更改类型
class Stack
{
public:
    // 构造函数
    Stack(int capacity = 3)   //初始化列表
        :_array(new DataType[capacity])   // 开辟一个DateType的动态数组,并进行初始化
        , _capacity(capacity)指定栈的容量,默认值为3
        ,_size(0)//栈的元素数量初始为0
    {}
    void Push(DataType data)
    {
        // CheckCapacity();//用于检查容器的当前容量是否足够,并在不足时进行扩容
        _array[_size] = data;//将传入的数据入栈
        _size++;
    }
    // 其他方法...
    ~Stack() //析构函数
    {
        delete[]_array;
        _array = nullptr;
        _size = _capacity = 0;
    }
private:
    DataType* _array;//动态数组,存储栈元素
    int _capacity;//栈的容量(最大元素数)
    int _size;//当前栈的元素数量
};
int main()
{
    Stack s1;
    return 0;
}

优化:

下面就是使用模板去定义的一个类,简称【模板类】,不限制死数据类型,将所有DataType都给为【T】,代码如下:

template
class Stack
{
public:
	// 构造函数
	Stack(int capacity = 4)
		:_a(new T1[capacity])
		,_capacity(capacity)
		,_size(0)
	{}
 
	void Push(T1 data)
	{
		_a[_size] = data;
		_size++;
	}
	// ...其他方法
	// 析构函数
	~Stack()
	{
		delete[]_a;
		_a = nullptr;
		_capacity = _size = 0;
	}
private:
	T1* _a;
	int _capacity;
	int _size;
};
 
int main()
{
	Stack s1;
	Stack s2;
	return 0;
}

类模板的实例化

类模板的实例化与函数模板的实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

可以看到因为我们将这个类定义为了类模板,此时便可以去初始化不同数据类型的栈了,Stack是类名,Stack、Stack这些都是它的类型

int main()
{
	Stack s1;		// int
	Stack s2;	// double
	Stack s3;		// char
	return 0;
}

类模板的分离编译

我们在学习c++类和对象讲到过一个类要声明和定义分离,那对于模板类也同样适用

模板声明:

#include 
#include 
#include 
using namespace std;
 
template
class Stack
{
public:
	// 构造函数
	Stack(int capacity = 4);
 
	//插入函数
	void Push(T1 data);
 
	// 其他方法....
 
	// 析构函数
	~Stack();
private:
	T1* _a;
	int _capacity;
	int _size;
};

类的模板定义:构造函数的定义

// 构造函数
Stack::Stack(int capacity)
	:_a(new T1[capacity])
	,_capacity(capacity)
	,_size(0)
{}

        这样写会报错,说缺少类模板“Stack”的参数列表,因为这个成员函数内部也使用到了模板参数T,那么这个函数也要变为函数模板才行。

        这里要强调一点的是对于普通类来说类名和类型是一样的,像构造函数,它的函数名就是类名;可是对于模板类来说是不一样,类名和类型不一样,这里Stack只是这个模板类的类名罢了,但是我们现在需要的是类型,显示实例化,这个模板类的类型即为Stack

正确写法:

#include "Stack.h"
 
template
// 构造函数
Stack::Stack(int capacity)
	:_a(new T1[capacity])
	,_capacity(capacity)
	,_size(0)
{}
template
// 插入函数
void Stack::Push(T1 data)
{
	_a[_size] = data;
	_size++;
}
// 析构函数
template
Stack::~Stack()
{
	delete[]_a;
	_a = nullptr;
	_capacity = _size = 0;
}

注意:类模板是不支持声明、定义、测试分开写的,会出现链接编译错误。解决方法是将测试和定义写在一起

你可能感兴趣的:(学习,笔记,c++,模板方法模式)