模板基础知识讲解,带你快速了解模板的用法

目录

前言

一、泛型编程

 二、函数模板

1.函数模板概念

2.函数模板格式

3.函数模板原理

4.函数模板的实例化

4.1.隐式实例化:

4.2.显示实例化

5.函数参数的匹配规则

扫描二维码关注公众号,回复: 14429364 查看本文章

5.1非模板函数可以和同名的函数模板同时存在

5.2非模板函数和同名函数模板的调用顺序

三、类模板

1.类模板的定义格式

2.类模板的实例化

四、非类型模板参数

五、模板的特化

1、函数模板特化

2、类模板全特化

2.1全特化

2.2偏特化

五、模板不支持分离编译

总结


前言

有一句是这样说的,学习c++要分为四部分来学,一部分是兼容c的语法,一部分是面向对象的思想,一部分是模板,一部分是STL。从这句话中我们就可以看出学习模板是学习c++的重要环节。那么接下里,我将带大家认识模板,希望大家在阅读本文后能明白什么是模板以及模板的用法。


一、泛型编程

c++对比c的一个增强是增加了函数重载功能,在函数参数类型不一样时,我们可以用相同的函数名定义出不同的函数。例如我们想要写一个通用的swap函数时,针对不同的参数类型,我们可以写出如下代码:

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}
void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}
......

使用函数重载虽然可以实现,但会出现以下问题:

1.重载的函数仅仅是类型不同,复用率较低,每当出现新的类型时,就要增加对应的函数

2.代码的可维护性比较低,当一个函数出错时,可能所有重载函数都会出错。

那么,能否给编译器一个模子,当我们给编译器不同类型的参数时,它可以根据这个模子来生成代码呢?

马云曾说过,世界是由懒人创造的为了节省头发,前人已经帮我们想好了解决办法,我们只需要站在他们载好的树下乘凉就好了。

泛型编程:编写和类型无关的通用代码,是代码复用的一种手段,模板就是泛型编程的基础。而模板又分为函数模板和类模板,下面我将一一介绍。

 二、函数模板

1.函数模板概念

函数模板是一个函数家族,函数模板与类型无关,只实现相应的逻辑。在使用时会进行参数化,根据传入参数的类型生成对应类型的函数。

2.函数模板格式

template<tempname T1,tempname T2,typename T3,typename Tn……>

返回值类型 函数名(参数列表){}

typelate <typename T>

void swap(T& a,T& b)
{
    T temp=a;
    a=b;
    b=temp;
}

typename是用来定义模板类型的关键字,也可以用class(不能用struct代替)。

3.函数模板原理

函数模板并不是函数,它只是一个模具,编译器会根据我们传进的参数生成对应的函数。所以模板的本质就是将本来需要我们重复做的事传给了编译器。

在编译器编译阶段,对于模板函数的使用,编译器会根据传入参数的类型推演生成对应的函数并进行调用。

4.函数模板的实例化

函数模板生成模板函数的过程,称为模板的实例化。而模板的实例化又分为隐式实例化和显式实例化。

4.1.隐式实例化:

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

代码如下:

template<class T>
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);
	Add(d1, d2);
    return 0;
}

但下面这个代码则会报错:

template<class T>
T Add(const T left, const T right)
{
	return left + right;
}
	
int main()
  {
    int a = 10;
	double d = 10.0;
    Add(a, d);

	// 此时有两种处理方式:1. 用户自己来强制转化 Add(a, (int)d);
	                        
                        //  2. 使用显式实例化

}

该代码会报错,因为在编译阶段,编译器看到该实例化时,需要推演实际参数类型,而根据a推演出来的为int,根据d推演出来的为double。但模板参数列表中只有一个T,所有编译器无法推演出T的类型,所以会发生报错。

注意:在模板中,编译器一般不会进行类型转换操作,因为一旦出问题,编译器就要背黑锅。

此时有两种处理方式:1.用户自己来强制转化。2.使用显示实例化。

4.2.显示实例化

在函数名后的<>中指定模板参数的实际类型称为显式实例化。

int main(void)
{
    int a = 10;
    double b = 20.0;
    // 显式实例化
    Add<int>(a, b);
    return 0;
}

如果类型不匹配,编译器会进行类型转换,如果转换失败,编译器会报错。

5.函数参数的匹配规则

5.1非模板函数可以和同名的函数模板同时存在

非模板函数可以和同名的函数模板同时存在,并且这个函数模板可以实例化为这个非模板函数。

// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非模板函数匹配,编译器不需要特化
	Add<int>(1, 2); // 调用编译器特化的Add版本
}

5.2非模板函数和同名函数模板的调用顺序

对于非模板函数和同名函数模板,在其他条件都相同的情况下,编译器会优先调用非模板函数。但是如果函数模板能实例化出更好的函数,那么编译器将会调用函数模板。

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

模板函数不允许自动类型转换,而普通函数可以。

三、类模板

1.类模板的定义格式

typelate <class T1,class T2,class T3,class Tn……>
calss 类模板名
{

    //类内成员定义
};

注意,这样定义出来的仅仅是一个类的模板,而不是具体的类。

2.类模板的实例化

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

例如,下面是一个动态顺序表的模板:

// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>

class Vector
{
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{}
	// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	void PushBack(const T& data);
		void PopBack();
		// ...
	
private:
	T* _pData;
	size_t _size;
	size_t _capacity;

}

现在进行类的实例化:

// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

注意:当类模板定义在类外面时,需要加模板参数列表。

template<class T>
class Vector
{
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{}
	// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	

private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表

template <class T>
Vector<T>::~Vector()
{
	if (_pData)
		delete[] _pData;
	_size = _capacity = 0;
}

四、非类型模板参数

模板参数分为类型模板参数和非类型模板参数

类型模板参数:出现在模板参数列表中,跟在tempneme或者class之后的参数类型名称。

非类型模板参数:用一个常量作为模板参数,在类(函数)模板中可以当成常量使用。

template<class T = int, size_t N = 8>
class Array
{
public:
	void f()
	{
		//N = 1000;  不行,N是常量,不能更改,否则会报错
	}
private:
	T _a[N];
};

 注意:

  • 只有整形可以做非类型模板参数。
  • 非类型的模板参数必须在编译器就能确定结果。

五、模板的特化

1、函数模板特化

通常情况下,使用模板可以实现与类型无关的通用逻辑,但在特定情况下,模板实例化出的类型会导致逻辑错误。

以比较两个值是否相等的函数为例:

template<class T>
bool IsEqual(const T& left, const T& right)
{
	return left == right;
}

int main()
{
	cout << IsEqual(1, 2) << endl;//逻辑正确
	cout << IsEqual(1.1, 1.1) << endl;//逻辑正确

	const char p1[] = "hello";
	const char p2[] = "hello";
	cout << IsEqual(p1, p2) << endl;//逻辑错误

	const char* p3 = "hello";
	const char* p4 = "hello";
	cout << IsEqual(p3, p4) << endl;//逻辑错误

	return 0;
}

这个函数模板在大多数情况下是没有问题的,但当比较的对象是字符串或数组时就会出现问题。因为我们想要比较字符串和数组时,编译器会把模板参数实例化成指针,导致逻辑错误。 这个时候就要用到模板的特化。

template<class T>
bool IsEqual(const T& left, const T& right)
{
	return left == right;
}

bool IsEqual(const char* left, const char* right)
{
	return !strcmp(left, right);
}

我们在模板下面特化指针类型。编译器在调用函数时会找更为匹配的,所以在参数是指针时会调用我们特化出的函数,不会再去使用模板实例化。

正规的函数模板特化像下面这样写:

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
    return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
    return *left < *right;
}

2、类模板全特化

特化分为全特化和偏特化,函数模板和类模板一样,也分为全特化和偏特化,这里以类模板举例。

2.1全特化

全特化即是将模板参数列表中的参数全部特化。

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
//特化类模板
template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	int _d1;
	char _d2;
}

在类型匹配时,编译器优先调用特化的类。 

2.2偏特化

偏特化有以下两种表现方式,分别是部分特化对模板参数进行进一步限制

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
// 将第二个参数特化为int --偏特化
template <class T1>
class Data < T1, int >
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
};
// 偏特化 对参数更近一步限制,指定如何模板参数是指针,就走我
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};

int main()
{
	Data<int, double> d1; //走第一个
	Data<double, int> d2; //走第二个
	Data<double*, int*> d3; //走第三个
    return 0;
}

五、模板不支持分离编译

分离编译:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

模板一般不支持分离编译:如果模板函数进行分离编译,那么定义在.cpp中,声明在.h文件中,其它文件包含头文件时只会包含模板函数的声明。这样做对一般函数是没有任何问题的,但是对模板函数就会发生链接错误,这是为什么呢?

程序构建主要分为四个阶段,预处理,编译,汇编,链接。在前三个过程中,为了提升效率,每个文件之间是不做交流的,等到最后一个阶段才会链接到一起。如果主函数文件中调用的函数定义在了其它文件中,在编译阶段主函数里是不会有这个函数的的定义的,只会暂时保存这个函数的函数名。而在这个函数定义的文件中,编译器会生成其的地址,并和其对应的函数名一起保存到一张符号表里。在链接阶段,主函数就可以根据函数名从符号表里找到对应的地址,从而找到该函数的定义。

但是模板函数有一个特点,只有调用的时候才会实例化,而在编译之前文件与文件之间又是不会交互的,所以这就很尴尬了。这个函数在主函数的文件中被调用,所以只能在主函数中实例化,而主函数中只有声明没有定义,实例化了也没什么用。有定义的文件中由于模板没有被实例化,所以也就不能生成相应的地址存到符号表中去,导致链接的时候找不到这个函数,所以就会发生报错。


总结

通过本篇文章,相信大家已经对模板有了一定认识。如果觉得有收获不妨点一个赞来支持博主。后续我也将继续带来模板的进阶知识,关注我可以第一时间看到哦,感谢您的阅读,您的支持就是对我最大的鼓励。