Loading

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中的 STU 表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位 置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。

Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也 就为空了,所以模板头应该写作 template<>。

另外,Max中的 STU 是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够 逆推出 T 的具体类型。简写后的函数声明为:

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中声明的是没有被具体化的类型参数;类名 Point<string, T2>列出了所 有类型参数,包括未被具体化的和已经被具体化的。

类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作 template class Point,编译器就不知道 string 代表的是第一个类型参数,还是第二个类型参数。

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 可以,但 1+n 不行。
    int len;
    cin >> len;
    int a[len];
    int b[len];
    Swap(a,b);   // 错误,是变量,不是常量
    
    Array<int, len> arr;     // 错误,是变量,不是常量
    
  2. 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存 储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。

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> 类是 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;

编译器会根据传递给类型参数的实参来生成特定版本的函数或类,并且相同类型只生成一次。

因此对于由类模板生成的相同类型的类,共享同一份静态成员,生成的不同类型的类,不会共享静态成员。

posted @ 2021-10-21 19:09  锦瑟,无端  阅读(258)  评论(0编辑  收藏  举报