C++初阶:模板

目录

一.泛型编程

二.函数模板

2.1.函数模板的概念

2.2.函数模板的格式

2.3.函数模板的原理

2.4.函数模板的实例化

隐式实例化

显示实例化

2.5.模板参数的匹配原则

三.类模板

3.1.类模板的格式

3.2.类模板的实例化

3.3.在类模板外部定义成员函数

四.非类型模板参数

4.1.类型参数

4.2.非类型参数

五.模板特化

5.1.函数模板特化

5.2.类模板特化

全特化

偏特化

六.模板的分离编译

6.1.什么是分离编译

6.2.模板的分离编译

6.3.解决方法

七.模板总结


模板是C++支持参数化多态的工具,它可以实现类型参数化,即把类型定义为参数,真正实现了代码的可重用性,减轻了编程及维护的工作量和难度。模板使类或函数可在编译时定义所需处理和返回的数据类型,一个模板并非一个实实在在的类或函数,仅仅是一个类或函数的描述。模板一般分为函数模板和类模板。

一.泛型编程

如何实现一个通用的交换函数呢?

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. 代码的可维护性比较低,一个出错可能所有的重载均出错。

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢? 答案当然是肯定的,我们可以使用泛型编程解决上述问题,泛型编程允许我们编写与数据类型无关的通用代码,从而提高代码的重用性和可维护性。

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

二.函数模板

函数模板并不是一个可以直接使用的函数,它是可以产生多个函数的模板。使用函数模板的目的就是让这些程序的实现与类型无关,比如定义一个函数模板add(),它既可以实现int类型数据相加,又可以实现double类型数据相加。

2.1.函数模板的概念

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

2.2.函数模板的格式

定义函数模板的语法格式如下所示:

template<typename 形参名,typename 形参名...>
返回值类型 函数名(参数列表)
{
  函数体;
}

上述语法格式中,template是声明模板的关键字,typename是定义形参的关键字,它可以用class代替,在这里class和typename没有区别。<>中的参数称为模板形参,模板形参和函数形参很像,但模板形参不能为空。template下面就是定义的一个函数模板,它与普通的函数定义方式相同,只是参数列表中的数据类型要使用<>中的形参名。定义一个add()函数模板,如下所示:

//定义函数模板
template<typename T>
T add(T t1, T t2)
{
	return t1 + t2;
}

注意:

typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)。

案例:

template<class T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 1, b = 2;
	//Swap(a,b);//调用模板函数
	//库里面包含了swap函数,可以不用自己写
	swap(a, b);//调用的不是模板函数

	double c = 1.11, d = 2.22;
	//Swap(c, d);
	swap(c, d);

	return 0;
}

注意:

  1. 函数std::swap():是C++标准模板库(STL)中的内置函数,该函数交换两个变量的值;
  2. 对函数模板而言不存在add(int,int)这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行;
  3. Swap(a,b)和Swap(c, d)调用的是两个不同的函数,它们对应的函数地址并不相同。

2.3.函数模板的原理

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

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

2.4.函数模板的实例化

函数模板并不是一个函数,它只相当于一个模子,定义一次即可使用不同类型的参数来调用该函数,这样做可以减少代码的书写,提高代码的复用性。但要注意,使用函数模板不会减少最终可执行程序的大小,因为在调用函数模板时,编译器会根据调用时的参数类型进行相应的实例化。所谓实例化,就是用类型参数去替换模板中的模板参数,生成一个具体类型的真正函数。实例化可分为隐式实例化与显示实例化。

隐式实例化

隐式实例化是根据函数调用时传入的数据类型确定模板形参T的类型,模板形参的类型是隐式确定的。编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。生成int类型的函数后,再将实参1和2传入进行运算。

这样,每一次调用时都会根据不同的类型实例化出不同类型的函数,所以最终可执行程序的大小并不会减少,它只是提高了程序员对代码的复用。

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

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

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

	//实参传递给形参,自动推演模板类型
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
	cout << Add(a1, (int)d1) << endl;
	cout << Add((double)a1, d1) << endl;

	return 0;
}

显示实例化

隐式实例化不能为同一个模板形参指定两种不同的类型,如add(1,1.2),这样调用,两种形参类型不一致,编译器便会报错。此时可以用另一种实例化方式来解决这个问题--显示实例化。显示实例化就是显示地指定函数模板中的数据类型,即在函数名后的<>中指定模板参数的实际类型。

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
 
int main(void)
{
	int a = 10;
	double b = 20.0;

	//显式实例化
	Add<int>(a, b);

	return 0;
}

注意:

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

2.5.模板参数的匹配原则

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

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}

//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}

int main()
{
	int a = 1, b = 2;
	Add(a, b);//与非模板函数匹配,编译器不需要特化
	Add<int>(a, b);//调用编译器特化的Add版本

	return 0;
}

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

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}

//通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}

int main()
{
	Add(1, 2);//与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0);//模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数

	return 0;
}

原则三:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

如果有不同类型参数,则只允许使用非模板函数,因为模板是不允许自动类型转化的,但普通函数可以进行自动类型转换。

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}

//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}

int main()
{
	Add(1, 5);//与非模板函数匹配,编译器不需要特化
	Add(1, 5.2);//调用非模板函数

	return 0;
}

三.类模板

函数可以定义模板,对于类来说,也可以定义一个类模板,类模板是针对成员数据类型不同的类的抽象,它不是代表一个具体的实际的类,而是一个类型的类,一个类模板可以生成多种具体的类。

3.1.类模板的格式

template<typename 形参名,typename 形参名...>
class 类名
{
   ...
}

类模板中的关键字含义与函数模板相同。需要注意的是,类模板的模板形参不能为空,一旦声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用数据类型的地方都可以使用模板形参名来声明。例如定义一个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();
	//...

	size_t Size()
	{
		return _size;
	}

	T& operator[](size_t pos)
	{
		assert(pos < _size);
		return _pData[pos];
	}

private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};

//注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
	if (_pData)
		delete[] _pData;
	_pData = nullptr;
	_size = _capacity = 0;
}

3.2.类模板的实例化

由于类模板包含类型参数,因此也称为参数化类,如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。定义了类模板后就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类。

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟上一个<>,并在里面表明相应的类型。类模板名字不是真正的类,而实例化的结果才是真正的类。例如对栈类模板进行实例化:

//typedef int STDataType;
template<class T>
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = new T[capacity];
		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		delete[]_a;
		_capacity = _top = 0;
	}

private:
	//STDataType* _a;
	T* _a;
	size_t _top;
	size_t _capacity;
};

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

	Stack<int> st1;//int
	Stack<double> st2;//double

	return 0;
}

注意:

  1. 当类模板有两个模板形参,则在创建对象时,类型之间要用逗号分隔开;
  2. 使用模板时,必须要为模板形参显示指定实参,也就是必须要在<>中指定类型,不存在实参推演过程,这一点与函数模板不同;
  3. 创建类对象时,在类名后<>中指定了模板形参的类型,编译器先根据类型实例化出一个具体的类,然后再创建这个具体实例的对象,这个过程和函数模板一样,都不会减少最终执行程序的代码。

3.3.在类模板外部定义成员函数

在学习的过程中,类模板的成员函数都是在类的内部实现的,类模板的成员函数可以在类模板中定义(inline函数),也可以在类模板外定义(此时成员函数定义前面必须加上template及模板参数),在类模板外部定义成员函数的方法如下所示:

template<模板形参表>
函数返回类型 类名<模板形参名>::函数名(参数列表){ }

template是类模板的声明,在实现成员函数时,也要加上类作用域,而且在类名后要用<>指明类的模板形参。例如有下列类模板的定义:

template<class T1,class T2>
class B
{
public:
	T1 a;
	T2 b;
	T1 func(T1 a, T2& b);
};

上述代码中,如果在类模板外定义类B的成员函数func(),其实现如下所示:

template<class T1,class T2>
T1 B<T1, T2>::func(T1 a, T2& b)
{

}

类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址时,才被实例化。需要注意的是,当在类外面定义类的成员函数时,template后面的模板形参应与要定义的类模板形参一致。

注意:

类模板在实例化时,带有模板形参的成员函数并不会跟着实例化,这些成员函数只有在被调用时才会被实例化。

四.非类型模板参数

模板是C++支持参数化多态的工具,模板的形参有两种类型:类型参数和非类型参数。接下来就针对这两种模板形参进行详细讲解。

4.1.类型参数

在前面我们所涉及的模板参数都是由typename或者class标记,以这两个关键字标记的模板参数就称为类型模板参数,类型模板参数是我们使用模板的主要目的。例如下列模板声明:

template<class T>
T add(T t1,T t2);

其中,T就是一个类型形参,类型形参的名字由用户自行确定,表示的是一个未知类型,模板类型形参可以作为类型说明符用在模板中的任何地方,与内置类型说明符或类型说明符的使用方式完全相同。我们可以为模板定义多个类型模板参数,也可以为类型模板参数指定默认值,示例代码如下所示:

template<class T,class U = int>
class A
{
public:
  void func(T,U);
};

在上述代码中,把U默认设置成为int类型,类模板类型形参和函数的默认参数一样,如果多个形参,则第一个形参设定了默认值之后的所有模板形参都要设定默认值。

注意:

可以为类模板设置默认值,但不能为函数模板设置默认值。

4.2.非类型参数

模板的非类型参数也就是内置类型形参,例如定义如下模板:

template<class T,int a>
class A
{
  //...
}

其中a就是非类型的模板形参。非类型模板形参相当于为函数模板或类模板预定一些常量,在生成模板实例时,也要求必须以常量,即编译器已知的值为非类型模板参数赋值。

非类型模板形参只可以是整型,枚举,指针和引用类型,例如double不可以是非类型形参,但double&,double*这样的对象引用或指针是正确的。

相对于常量,非类型模板参数的灵活之处在于:模板中声明的常量,在模板的所有实例中都具有相同的值,而非类型模板参数则对于在不同的模板实例中拥有不同的值来满足不同的需求。例如要定义一个常量数组,如果已知要用到一个大小为10的数组,则可以将数组长度定义为10,代码如下所示:

template<class T>
class Array
{
	static const unsigned size = 10;
	T arr[size];//数组大小为定义好的常量
};

但如果需要多个大小不一的数组时,将数组大小定义为常量便无法满足要求了,此时就可以将数组长度定义为数组类模板的成员变量,在实例化时自动赋值,具体代码如下:

//定义一个模板类型的静态数组
template<class T, size_t N = 10>
class Array
{
public:
	T& operator[](size_t index)
	{
		return _array[index];
	}

	const T& operator[](size_t index) const
	{
		return _array[index];
	}

	size_t size() const
	{
		return _size;
	}

	bool empty() const
	{
		return 0 == _size;
	}
private:
	T _array[N];
	size_t _size;
};

//err
//不支持赋值操作
//template<class T, size_t N = 20>
//void func1(const T& a)
//{
//	N = 10;//err
//}

//err
//不支持double类型,必须为整型
//template<class T, double N = 20.2>
//void func2(const T& a)
//{
//	//N = 10;
//}

//ok
//支持bool类型
template<class T, bool flag = true>
void func3(const T& a)
{
	//N = 10;
}

int main()
{
	Array<int, 10> a1;//10
	Array<double, 20> a2;//20

	//func1(1);//err,N的值不能进行修改左操作数必须为左值
	//func2(2);//err,非类型模板参数的类型不能为double
	func3(1);//ok
	
	return 0;
}

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的;
  2. 调用非类型模板形参的实参必须是一个常量表达式,即必须能在编译时计算出结果;
  3. 任何局部对象,局部变量及它们的地址都不是一个常量表达式,不能用作非类型模板形参的实参,全局指针类型,全局变量,全局对象也不是常量表达式,也不能用作非类型模板形参的实参;
  4. sizeof运算符的运算结果是一个常量表达式,可以用作非类型模板形参的实参;
  5. 非类型模板形参一般不用于函数模板。

五.模板特化

学习模板特化之前,我们先了解什么是特化,所谓特化就是将泛型的东西具体化。模板特化就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,受到特定约束或完全被指定下来。

很多时候我们既需要一个模板能应对各种情景,又需要它的某个特定的类型(比如bool)来进行特别的处理,这种情形下就需要对模板进行特化。比如实现了一个专门用来进行小于比较的函数模板:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

//函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;   // 可以比较,结果正确

	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误

	return 0;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

5.1.函数模板特化

函数模板特化步骤:

  1. 必须要先有一个基础的函数模板;
  2. 关键字template后面接一对空的尖括号<>;
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型;
  4. 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

//函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

//函数模板特化 -- 某些类型特殊化处理
//对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

int main()
{
	cout << Less(1, 2) << endl;   //可以比较,调用函数模板

	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; //可以比较,调用函数模板

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; //调用特化之后的版本,而不走模板生成

	int* p3 = new int(1);
	int* p4 = new int(2);
	cout << Less(p3, p4) << endl; //可以比较,调用函数模板

	return 0;
}

 但是,函数模板特化并没有太多实际的意义。一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。如下所示:

bool Less(Date* left, Date* right)
{
	return *left < *right;
}

该种实现简单明了,代码的可读性高,容易书写。对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

5.2.类模板特化

类模板特化可分为偏特化与全特化,本节我们就来学习一下这两种特化形式。

全特化

全特化就是模板中的模板参数全部被指定为确定的类型,其标志就是产生出完全确定的东西。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

template<class T>
struct Less
{
	bool operator()(const T& l, const T& r) const
	{
		return l < r;
	}
};

//全特化
template<>
struct Less<Date*>
{
	bool operator()(const Date* l, const Date* r) const
	{
		return *l < *r;
	}
};

int main()
{
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less<Date>()(d1, d2) << endl;//调用类模板

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less<Date*>()(p1, p2) << endl;//调用全特化

	return 0;
}

偏特化

偏特化就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。例如定义一个类模板,如下所示:

template<class T1, class T2>
class Data
{
public:
	Data() 
	{ 
		cout << "Data<T1, T2>" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

偏特化有以下两种表现方式:

表现一:部分特化。将模板参数列表中的一部分参数特化。

//将模板参数列表中的一部分参数特化。
template <class T1>
class Data<T1, int>
{
public:
	Data() 
	{ 
		cout << "Data<T1, int>" << endl; 
	}
private:
	T1 _d1;
	int _d2;
};

表现二:参数更进一步的限制。偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

//基础版本
template<class T1, class T2>
class Data
{
public:
	Data() 
	{ 
		cout << "Data<T1, T2>" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

//将模板参数列表中的一部分参数特化。
template <class T1>
class Data<T1, int>
{
public:
	Data() 
	{ 
		cout << "Data<T1, int>" << endl; 
	}
private:
	T1 _d1;
	int _d2;
};

//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data<T1*, T2*>
{
public:
	Data() 
	{ 
		cout << "Data<T1*, T2*>" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1=T1(), const T2& d2=T2())
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}
private:
	const T1& _d1;
	const T2& _d2;
};

//引用和指针混搭
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
	Data(const T1& d1 = T1())
	{
		cout << "Data<T1&, T2*>" << endl;
	}
};

int main()
{
	Data<int, int> d1;//调用特化的Data<T1, int>
	Data<int, char> d2;//调用基础模板

	Data<int*, int> d3;//调用特化的Data<T1, int>
	Data<double, int> d4;//调用特化的Data<T1, int>

	Data<int*, int*> d5;//调用特化的指针版本
	Data<int&, int&> d6;//调用特化的引用版本

	Data<int&, int*> d7;//调用引用和指针混搭版本

	return 0;
}

注意:

  1. 函数模板并不支持偏特化,但它支持全特化;
  2. 只有特化是无法正常使用的,必须包含原模板,是基于原模板进行的。

六.模板的分离编译

6.1.什么是分离编译

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

6.2.模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

//Func.h

#include<iostream>
#include<array>
#include<vector>
using namespace std;

template<class T>
T Add(const T& left, const T& right);

void func();



//Func.cpp

#include"Func.h"

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

void func()
{
	cout << "void func()" << endl;
}



//test.cpp

#include"Func.h"

int main()
{
	Add(1, 2);
	Add(1.0, 2.0);

	func();

	return 0;
}

运行结果:

分析:

C/C++程序运行,一般要经历以下步骤:预处理-->编译-->汇编-->链接:

  • 起始:func.h  func.cpp  test.cpp
  • 预处理:头文件的展开/宏替换/条件编译/去掉注释,生成func.i  test.i
  • 编译:检查语法,生成汇编代码,生成func.s  test.s
  • 汇编:将汇编代码转换成二进制机器码,生成func.o  test.o
  • 链接:合并生成可执行文件,生成a.out

在Func.cpp中,编译器没有看到对Add模板函数的实例化,因此不会生成具体的加法函数;在test.cpp中调用的Add<int>和Add<double>,编译器在链接时才会找其地址,但是这两个函数没有实例化没有生成具体的代码,因此链接时报错。

6.3.解决方法

1.模板定义的位置显式实例化。这种方法不实用,不推荐使用。

//Func.h

#include<iostream>
#include<array>
#include<vector>
using namespace std;

template<class T>
T Add(const T& left, const T& right);

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

void func();



//Func.cpp

#include"Func.h"

void func()
{
	cout << "void func()" << endl;
}

//模板定义的位置显式实例化。这种方法不实用,不推荐使用。
template
double Add<double>(const double& left, const double& right);

template
int Add<int>(const int& left, const int& right);



//test.cpp

#include"Func.h"

int main()
{
	Add(1, 2);
	Add(1.0, 2.0);

	func();

	return 0;
}

2.将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用。

//Func.h

#include<iostream>
#include<array>
#include<vector>
using namespace std;

template<class T>
T Add(const T& left, const T& right);

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

void func();



//Func.cpp

#include"Func.h"

void func()
{
	cout << "void func()" << endl;
}



//test.cpp

#include"Func.h"

int main()
{
	Add(1, 2);
	Add(1.0, 2.0);

	func();

	return 0;
}

七.模板总结

优点:

  1. 模板复用代码,节省了资源和加速迭代开发,C++的标准模板库(STL)因此而产生;
  2. 增强了代码的灵活性。

缺陷:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长;
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值