C++ 基础系列——模板
1. 快速入门
在 C++ 中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。
函数模板
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量 类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
// 所有版本函数功能一样
template<typename T> void Swap(T *a, T *b)
{
T temp = *a;
*a = *b;
*b = temp;
}
template<class T> void Swap(T *a, T *b)
{
T temp = *a;
*a = *b;
*b = temp;
}
template<typename T> void Swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
从整体上看,template<typename T>
被称为模板头。
template
是定义函数模板的关键字,后面紧跟着尖括号包围的类型参数。typename
是用来声明具体的类型参数的关键字,也可以使用 class
关键字替代,它们没有任何区别。
类模板
类模板中定义的类型参数可以用在类声明和类实现中,其目的是将数据的类型参数化。
类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参 数;类型参数不能为空,多个类型参数用逗号隔开。
template<typename T1, typename T2> // 这里不能有分号
class Point{
public:
Point(T1 x, T2 y):m_x(x),m_y(y){}
T1 getX() const;
void setX(T1 x);
T2 getY() const;
void setY(T2 y);
private:
T1 m_x;
T2 m_y;
};
注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。
上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头。
template<typename T1, typename T2> // 模板头
T1 Point<T1, T2>::getX() const{
return m_x;
}
template<typename T1, typename T2> // 模板头
void Point<T1, T2>::setX(T1 x) const{
m_x = x;
}
类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。
使用类模板创建对象时,需要指明具体的数据类型。
Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, chat*> p3(12.4,"东经120度");
// 使用对象指针实例化
Point<float, float> *p1 = new Point<float, float>(10.6, 109.3); 2. Point<string, string> *p = new Point<string, string>("东经 180 度", "北纬 210 度");
2. 函数模板重载
当需要对不同的类型使用同一个算法时,为了避免定义多个功能重复的函数,可以使用模板,然后有些特定的类型需要单独处理,因此需要对函数模板进行重载。举个例子:
template<typename T> void Swap(T *a, T *b)
{
T temp = *a;
*a = *b;
*b = temp;
}
template<typename T> void Swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
对于这两个函数模板,能用来交换很多类型变量的值,但不能交换两个数组。
- 对于函数1,调用函数时传入的是数组指针,即指向第 0 个元素的指针,交换的是第0个元素,而不是整个数组
- 对于方案2,假设传入长度为5的 int 数组,那么 T 的真实类型是 int[5],T temp 会被替换为 int[5] temp,显然错误,另外在赋值时,由于数组名是常量,它的值不允许修改,也会产生错误。
实际上数组也有类型,例如 int a[6],对于数组 a,它的类型是 int[6]。
交换两个数组唯一的办法就是逐个交换所有的数组元素
template<typename T> void Swap(T a[], T b[], int len)
{
T temp;
for(int i = 0; i < len; i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
3. 函数模板的实参推断
类模板创建对象需要显式指明实参具体的类型:
template <typename T1, typename T2> class Point;
Point<int, int> p1(10, 20); //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京 180 度", "北纬 210 度"); //在堆上创建对象
对于函数模板,调用函数时可以不显式地指明实参的具体类型。
template<typename T> void Swap(T &a, T &b);
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);
虽然没有显式指明 T 的具体类型,但编译器会通过实参的类型自动推断出 T 的类型,这就是模板实参推断。
模板实参推断过程中的类型转换
对于非模板函数,调用时实参类型转换包括:
- 算数转换:如 int 转为 float
- 派生类向基类转换
- const 转换,即非 const 转换为 const
- 数组或函数指针转换,如果函数形参不是引用,那么数组名会转换为数组指针,函数名会转换为函数指针。
- 自定义类型转换
对于函数模板,类型转换仅能对 const转换 和数组或函数指针转换,其他都不能应用于函数模板,例如:
// 函数模板
template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);
// 调用形式
int name[20];
Student stu1("张华", 20, 96.5); //创建一个 Student 类型的对象
func1(12.5, 30); //Error
func2(name); //name 的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int
func3(stu1); //非 const 转换为 const,T 的真实类型为 Student
func4(name); //name 的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int *
func5(name); //name 的类型依然为 int [20],不会转换为 int *,所以 T 的真实类型为 int [20]
可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候要注意下面这样的函数模板:
template
void func(T &a, T &b);
如果调用形式为:
int str1[20];
int str2[10];
func(str1, str2);
由于 str1、str2 的类型分别为 int [20] 和 int [10],在函数调用过程中又不会转换为指针,所以编译器不知道 应该将 T 实例化为 int [20] 还是 int [10],导致调用失败。
函数模板显式指明实参类型
实参类型推断失败例子:
template<typename T1, typename T2> void func(T1 a){
T2 b;
}
// 函数模板调用
func(10);
func() 有 T1 和 T2 两个类型参数,但编译器只能从调用中推断出 T1 的类型,所以这种调用会失败。
「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >,里面包含具体的类型。
func<int, int>(10);
显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配,只有尾部的类型的参数可以省略,而且前提是它们可以从传递的函数实参中推断出来。
上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我 们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换。
template<typename T> void func(T a, T b);
// 调用形式
func(10, 23.5); // 错误
func<float>(20, 93.7); // 正确
在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为「T 的类型到底是 int 还是 double」而纠结了,所以可以从容地使用正常的类型转换了。
4. 模板的显式具体化
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)。
函数模板的显式具体化
一个显式具体化的例子:
typedef struct{
string name;
int age;
float score;
} STU;
// 函数模板
template<class T> const T& Max(const T& a, const T& b);
// 函数模板针对 STU 类型的显式具体化声明
template<> const STU& Max<STU>(const STU& a, const STU& b);
// 重载<<
ostream & operator<<(ostream &out, const STU &stu);
int main()
{
int a = 10;
int b = 20;
cout << Max(a,b) << endl;
STU stu1 = {"王明", 16, 95.5};
STU stu2 = {"徐亮", 17, 90.0};
cout << Max(stu1, stu2) << endl;
return 0;
}
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
}
//
template<> const STU& Max<STU>(const STU& a, const STU& b){
return a.score > b.score ? a : b;
}
ostream & operator<<(ostream &out, const STU &stu){
out << stu.name << ", " << stu.age << ", " << stu.score;
return out;
}
运行结果:
20
王明 , 16 , 95.5
Max
Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也 就为空了,所以模板头应该写作 template<>。
另外,Max
template<> const STU& Max(const STU& a, const STU& b);
在 C++ 中,对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本,在调用函数时,显示具体化优先于常规模板,而非模板函数优先于显示具体化和常规模板。
类模板的显式具体化
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y):m_x(x),m_y(y){}
T1 getX() const { return m_x; }
void setX(T1 x){m_x = x;}
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2> //这里要带上模板头
void Point<T1, T2>::display() const{
cout << "x=" << m_x << ", y=" << m_y << endl;
}
//类模板的显示具体化(针对字符串类型的显示具体化)
template<> class Point<string, string>{
public:
Point(string x, string y): m_x(x), m_y(y){ }
string getX() const{ return m_x; }
void setX(string x){ m_x = x; }
string getY() const{ return m_y; }
void setY(string y){ m_y = y; }
void display() const;
private:
string m_x; // x 坐标
string m_y; //y 坐标
};
// 具体化的类模板的成员函数前面不能带模板头。
void Point<string, string>::display() const{
cout << "x=" << m_x << " | y=" << m_y << endl;
}
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<int, string>(10, "东京 180 度") ) -> display();
( new Point<string, string>("东京 180 度", "北纬 210 度") ) -> display();
return 0;
}
运行结果:
x=10, y=20 x=10, y=东京 180 度
x=东京 180 度 | y=北纬 210 度
当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模 板头,而具体化的类模板的成员函数前面不能带模板头。
部分显式具体化
部分显式具体化,指只为一部分类型参数提供实参,部分显式具体化只能用于类模板,不能用于函数模板。
仍然以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|来分隔输出结果,而不管纵坐标 y 是 什么类型
// 类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y):m_x(x),m_y(y){}
T1 getX() const { return m_x; }
void setX(T1 x){m_x = x;}
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2> //这里要带上模板头
void Point<T1, T2>::display() const{
cout << "x=" << m_x << ", y=" << m_y << endl;
}
//类模板的部分显示具体化
template<typename T2> class Point<string, T2>{
public:
Point(string x, T y): m_x(x), m_y(y){ }
string getX() const{ return m_x; }
void setX(string x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
string m_x; // x 坐标
T2 m_y; //y 坐标
};
template<typename T2> // 需要带上模板头
void Point<string, T2>::display() const{
cout << "x=" << m_x << " | y=" << m_y << endl;
}
模板头 template
类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作 template
5. 模板中的非类型参数
模板中除了可以包含类型参数,还可以包含非类型参数,例如:
template<typename T, int N> class Demo{ };
template<class T, int N> void func(T (&arr)[N]);
- T 是一个类型参数,它通过 class 或 typename 关键字指定。
- N 是一个非类型参数,用来传递数据的值,而不是类型,它和普通函数的形参一样,都需要指明具体的类型。类型参数和非类型参数都可以用在函数体或者类体中。
- 当调用一个函数模板或者通过一个类模板创建对象时,非类型参数会被用户提供的、或者编译器推断出的值所取代。
在函数模板中使用非类型参数
通过 Swap 函数交换两个数组的值的原型为:
template
void Swap(T a[], T b[], int len);
形参 len 用来指明要交换的数组的长度,调用 Swap 函数之前必须先通过 sizeof 求得数组长度再传递给它。sizeof
只能通过数组名求得数组长度,不能通过数组指针求得数组长度。
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
T temp;
for(int i=0; i<N; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
Swap(a, b);
T (&a)[N]表明 a 是一个引用,它引用的数据的类型是 T[N],也即一个数组;
编译器会使用数组类型 int 来代替类型参数 T,使用数组长度 5 来代替非类型参数 N。
在类模板中使用非类型参数,以动态数组为例
// 动态数组实现代码
template<typename T, int N>
class Array{
public:
Array();
~Array();
T & operator[](int i); // 重载下标运算符[]
int length() const{ return m_length; } // 获取数组长度
bool capacity(int n); // 改变数组容量
private:
int m_length; // 数组的当前长度
int m_capacity; // 当前内存容量(数组还能容纳元素个数)
T *m_p; // 指向数组内存的指针
};
template<typename T, int N>
Array<T, N>::Array(){
m_p = new T[N];
m_capacity = m_length = N;
}
template<typename T, int N>
Array<T, N>::~Array(){
delete[] m_p;
}
template<typename T, int N>
T & Array<T,N>::operator[](int i){
if(i<0 || i >= m_length)
cout << "Exception: Array index out of bounds!" << endl;
return m_p[i];
}
template<typename T, int N>
bool Array<T, N>::capacity(int n){
if(n>0){ // 增大数组
int len = m_length + n; // 增大后的数组长度
if(len <= m_capacity){ // 现有内存足以容纳增大后的数组
m_length = len;
return true;
}else{ // 现有内存不能容纳增大后的数组
T *pTemp = nw T[m_length + 2 * n * sizeof(T)]; // 增加的内存足以容纳 2*n 个元素
if(pTemp == NULL){ // 内存分配失败
cout << "Exception: Failed to allocate memory!" << endl;
return false;
}else{ // 内存分配成功
memcpy(pTemp, m_p, m_length*sizeof(T));
delete[] m_p;
m_p = pTemp;
m_capacity = m_length = len;
}
}
}else{ // 收缩数组
int len = m_length - abs(n); // 收缩后数组的长度
if( len < 0 ){
cout << "Exception: Array length is too small!" << endl;
return false;
}else{
m_length = len;
return true;
}
}
}
int main(){
Array<int, 5> arr;
// 为数组元素赋值
for(int i = 0, len = arr.length(); i < len; i++)
arr[i] = 2*i;
//第一次打印数组
for(int i=0, len=arr.length(); i<len; i++)
cout<<arr[i]<<" ";
cout<<endl;
// 扩大容量并增加元素
arr.capacity(8);
for(int i=5, len=arr.length(); i<len; i++)
arr[i] = 2*i;
// 打印数组
for(int i=0, len=arr.length(); i<len; i++)
cout<<arr[i]<<" ";
cout << endl;
// 收缩容量
arr.capacity(-4);
for(int i=0, len=arr.length(); i<len; i++)
cout<<arr[i]<<" ";
cout << endl;
return 0;
}
运行结果:
0 2 4 6 8
0 2 4 6 8 10 12 14 16 18 20 22 24
0 2 4 6 8 10 12 14 16
非类型参数的限制
- 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式,如 1+1 可以,但 1+n 不行。
int len; cin >> len; int a[len]; int b[len]; Swap(a,b); // 错误,是变量,不是常量 Array<int, len> arr; // 错误,是变量,不是常量
- 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存 储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。
6. 模板的实例化
模板仅仅是编译器用来生成函数或类的“图纸”,模板不占用内存,最终生成函数或类才占用内存。由模板生成函数或类的过程叫做模板的实例化。
编译器会根据传递给类型参数的实参来生成特定版本的函数或类,并且相同类型只生成一次。
通过类模板创建对象时不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;类的实例化是延迟的、局部的,编译器并不会生成所有的代码。
类模板创建对象时,一般只实例化成员变量和构造函数。
7. 模板应用于多文件编程
不管是函数还是类,声明和定义(实现)的分离是一回事,都是将声明放在头文件,将定义放在源文件中。最终要解决的问题也只有一个,就是把函数调用和函数定义对应起来(找到函数定义的地址,并填充到函数调用处),而保证完成这项工作的就是链接器。
对于模板,一般是将模板的声明和定义都放到头文件中。模板不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”,这个生成过程有三点需要明确:
- 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码;
- 模板的实例化是由编译器完成的,而不是由链接器完成的;
- 在实例化过程中需要知道模板的所有细节,包含声明和定义。
「不能将模板的声明和定义分散到多个文件中」的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的实例。
将函数模板的声明和定义分散到不同的文件
// func.cpp
template<typename T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
void bubble_sort(int arr[], int n){
for(int i=0; i<n-1; i++){
bool isSorted = true;
for(int j=0; j<n-1-i; j++){
if(arr[j] > arr[j+1]){
isSorted = false;
Swap(arr[j], arr[j+1]); //调用 Swap()函数
}
}
if(isSorted)
break;
}
}
// func.h
#ifndef _FUNC_H
#define _FUNC_H
template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);
#endfi
// main.cpp
#include <iostream>
#include "func.h"
int main(){
int n1=10, n2=20;
Swap(n1,n2);
double f1=23.8, f2=92.6;
Swap(f1, f2);
return 0;
}
上面的程序,会产生一个链接错误,意思是无法找到 void Swap<double>(double &, double &)
这个函数。主函数 main() 中共调用了两个版本的 Swap() 函数,它们的原型分别是:
void Swap
(int &, int &);
void Swap(double &, double &);
对于 int 版本,bubble_sort()
定义时使用了模板生成了 int 版本,因此编译生成的文件中有 int 版本函数的定义, 因此在到链接阶段时,链接器会找到 int 版本,调用不会出错。
对于 double 版本,当编译器编译 main.cpp
时发现使用了 double 版本的调用,于是尝试生成 double 版本实例,但由于只有声明没有定义,所以生成失败,此时编译器不会报错,而是对该函数的调用做一个记录,希望链接时在其他文件找到该函数的定义。但在func.cpp 编译后,由于里面没有调用 double 版本的函数,因此编译器不会生成 double 版本函数,所以链接器最终找不到。
将类模板的声明和实现分散到不同的文件
// point.h
#ifndef _POINT_H
#define _POINT_H
template<class T1, class T2>
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
};
#endif
// point.cpp
#include <iostream>
#include "point.h"
using namespace std;
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout << "x=" << m_x << ", y=" << m_y << endl;
}
// main.cpp
#include <iostream>
#include "point.h"
using namespace std;
int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
Point<char*, char*> p2("东京 180 度", "北纬 210 度");
p2.display(); // 报错
return 0;
}
会出现一个链接错误,无法通过 p2 调用 Point<char*, char*>::display() const
这个函数,因为这个函数定义位于 point.cpp 文件中,是模板函数,但 point.cpp 文件中没有对函数进行调用,因此不会生成对应版本的实例。
8.模板的显式实例化
模板的实例化是在调用函数(函数模板)或创建对象(类模板)时由编译器自动完成的,是隐式实例化,同样可以通过指定类型进行显式实例化。
显式实例化的代码必须放在模板定义的原文件中,不能是只有模板声明的头文件中。
显式实例化的优势:模板的声明和定义可以分散到不同的文件中。
函数模板的显式实例化
对于上节函数模板应用到多文件的例子,可在 func.cpp 文件中将模板进行 double 类型的显式实例化:
template void Swap(double &a, double &b);
这样再运行程序不会出现链接错误。
还可在包含了函数调用的 main.cpp 中增加下面代码:
extern template void Swap(double &a, double &b);
extern 关键字告诉编译器,该版本的函数实例在其他文件中,请在链接期间查找,不过可写可不写。
类模板的显式实例化
类模板的显式实例化和函数模板类似。以上节的 Point 类为例,针对 char*类型的显式实例化(定义形式)代码为:
template class Point<char, char>;
相应地,它的声明形式为:
extern template class Point<char, char>;
不管是声明还是定义,都要带上 class 关键字,以表明这是针对类模板的。
显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。
extern template declaration; //实例化声明
template declaration; //实例化定义
显式实例化的缺陷
必须要在模板的定义文件(实现)中对所有使用到的类型进行实例化。这意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。
一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。
9. 类模板与继承详解
类模板和类模板之间、类模板和类之间可以互相继承。它们之间的派生关系有以下四种情况。
1. 类模板从类模板派生
template<class T1, class T2>
class A{
T1 v1;
T2 v2;
};
template<class T1, class T2>
class B : public A<T2, T1>{
T1 v3;
T2 v4;
};
template<class T>
class C : public B<T, T>{
T v5;
};
int main(){
B<int, double> obj1;
C<int> obj2;
return 0;
}
对于 main 中的 B<int, double> obj1;
生成 B<int, double> 类,由于其基类是A,因此会对应生成 A<double, int>
。
对于 main 中的 C<int>
,会生成直接基类B<int, int> 以及基类 A<int ,int>。
2. 类模板从类模板派生
template<class T1, class T2>
class A{
T1 v1;
T2 v2;
};
template<class T>
class B : public A<int, double>{ // A<int ,double> 是一个模板类
T v;
};
int main(){
B<char> obj1; // 会自动生成两个模板类:A<int, double>、B<char>
return 0;
}
3. 类模板从普通类派生
class A{
int v1;
};
template<class T>
class B : public A{
T v;
};
int main(){
B<char> obj1;
return 0;
}
4. 普通类从模板类派生
template<class T>
class A{
T v1;
int n;
};
class B : public A<int>{
double v;
};
int main(){
B obj1;
return 0;
}
10. 类模板与友元
1. 函数、类、类成员函数作为类模板的友元
void func(){}
class A{};
class B{
public:
void func(){};
};
class Tmp1{
friend void function();
friend class A;
friend void B::func();
};
类模板实例化时,除了类型参数被替换外,其他所有内容都原样保留,因此任何从 Tmp1 实例化得到的类都包含上面三条友元声明。
2. 函数模板作为类模板的友元
// 类模板
template<class T1, class T2>
class Pair{
public:
Pair(T1 k, T2 v) : key(k), value(v){};
bool operator<(const Pair<T1, T2> & p) const;
template<class T3, class T4>
friend ostream & operator << (ostream & o, const Pair<T3, T4> & p);
private:
T1 key;
T2 value;
};
// 成员函数模板
template<class T1, class T2>
bool Pair<T1, T2>::operator<(const Pair<T1, T2> & p) const{
return key < p.key;
}
// 函数模板
template <class Tl, class T2>
ostream & operator << (ostream & o, const Pair<T1, T2> & p){
o << "(" << p.key << "," << p.value << ")";
return o;
}
int main(){
Pair<string, int> student("Tom", 29);
Pair<int, double> obj(12, 3.14);
cout << student << " " << obj;
return 0;
}
cout << student << " " << obj;
会生成两个 operator<< 函数,原型分别是:
// Pair<string, int> 类的友元
ostream & operator << (ostream & o, const Pair<string, int> & p);
// Pair<int, double> 类的友元
ostream & operator << (ostream & o, const Pair<int, double> & p);
3. 函数模板作为类的友元
class A{
public:
A(int n): v(n){}
template<class T>
friend void Print(const T & p);
private:
int v;
};
// 当 T 是 A 时,将起到声明为友元的作用
template<class T>
void Print(const T & p)
cout << p.v;
4. 类模板作为类模板的友元
template<class T>
class A{
public:
void func(const T & p)
cout << p.v;
};
template<class T>
class B{
public:
B(T n) : v(n){}
template<class T2>
friend class A; // 把类模板 A 声明为友元
private:
T v;
};
int main(){
B<int> b(5);
A<B<int>> a; // 用 B<int> 替换 A 模板中的 T
a.func(b);
return 0;
}
A<B
11. 类模板中的静态成员
template <class T>
class A{
public:
A(){ count++; }
~A(){ count--; }
A(A &){ count++; }
static void PrintCount(){}
private:
static int count;
};
template<> int A<int>::count=0;
template<> int A<double>::count = 0;
编译器会根据传递给类型参数的实参来生成特定版本的函数或类,并且相同类型只生成一次。
因此对于由类模板生成的相同类型的类,共享同一份静态成员,生成的不同类型的类,不会共享静态成员。