C++泛型编程之函数模板&类模板

在C++的门口绕来绕去,今天终于决定要搞起泛型编程了,毕竟也是C++的重要特性了。

本篇博客参考来自:

https://www.cnblogs.com/lifexy/p/8761325.html

https://www.cnblogs.com/lifexy/p/8781525.html


C++泛型编程之函数模板

前言

当我们想写个Swap()交换函数时,通常这样写:

void Swap(int& a, int& b)
{
    int c = a;
    a = b;
    b = c;
}

但是这个函数仅仅只能支持int类型,如果我们想实现交换double,float,string等等时,就还需要从新去构造Swap()重载函数,这样不但重复劳动,容易出错,而且还带来很大的维护和调试工作量。更糟的是,还会增加可执行文件的大小。所以C++引入了泛型编程概念

在C++里,通过函数模板类模板来实现泛型编程

函数模板

  • 一种特殊的函数,可通过不同类型进行调用
  • 函数模板是C++中重要的代码复用方式
  • 通过template关键字来声明使用模板
  • 通过typename关键字来定义模板类型

比如:

template <typename T>       //声明使用模板,并定义T是一个模板类型

void Swap(T& a, T& b)           //紧接着使用T
{
    T c = a;
    a = b;
    b = c;
}

当我们使用int类型参数来调用上面的Swap()时,则T就会自动转换为int类型.

函数模板的使用

  • 分为自动调用显示调用

例如,我们写了一个Swap函数模板,然后在main()函数里写入:

#include <iostream>
using namespace std;


template <typename T> //声明使用模板,并定义T是一个模板类型

void TemplateSwap(T &a, T &b)
{
    T c = a;
    a = b;
    b = c;
}

int main()
{
    int a = 0;
    int b = 1;
    TemplateSwap(a, b); //自动调用,编译器根据a和b的类型来推导

    float c = 0;
    float d = 1;

    TemplateSwap<float>(c, d); //显示调用,告诉编译器,调用的参数是float类型

    cout << a << " " << b << endl;
    cout << c << " " << d << endl;

    system("pause");
    return 0;
}

初探函数模板

写两个函数模板,一个用来排序数组,一个用来打印数组,代码如下:

#include <iostream>
#include <string>
using namespace std;

template <typename T>
void Sort(T a[],int len)
{
    for(int i=0;i<len;i++)
    {
        for(int j=0;j<i;j++)
        {
            if(a[i]<a[j])
            {
                T t = a[i];
                a[i] = a[j];
                a[j] = t;
            }
        }
    }
}

template <typename T>
void Println(T a[],int len)
{
    for(int i=0;i<len;i++)
    {
        cout << a[i] << ", ";
    }
    cout << endl;
}
int main()
{
    int a[5] = { 5,3,2,4,1 };
    Sort(a, 5); //自动调用,编译器根据a和5的类型来推导

    Println<int>(a, 5); //显示调用,告诉编译器,调用的参数是int类型

    string s[5] = { "Java","C++","Pascal","Ruby","Basic" };
    Sort(s, 5);
    Println(s, 5);

    system("pause");
    return 0;
}

突然的发问:上面这个排序是冒泡排序吗?---答曰,不是。记住冒泡排序每趟都有有一个最大(最小)值冒出来。

深入理解函数模板

为什么函数模板能够执行不同的类型参数?

答:

  • 其实编译器对函数模板进行了两次编译
  • 第一次编译时,首先去检查函数模板本身有没有语法错误
  • 第二次编译时,会去找调用函数模板的代码,然后通过代码的真正参数,来生成真正的函数
  • 所以函数模板,其实只是一个模具,当我们调用它时,编译器就会给我们生成真正的函数.

试验函数模板是否生成真正的函数

通过两个不同类型的函数指针指向函数模板,然后打印指针地址是否一致,代码如下:

#include <iostream>
using namespace std;

template <typename T>
void Swap(T& a,T&b)
{
    T c = a;
    a = b;
    b = c;
}

int main()
{
    void(*Fpii)(int&, int&);
    Fpii = Swap;

    void(*Fpff)(float&, float&);
    Fpii = Swap;

    cout << reinterpret_cast<void *>(Fpii) << endl;
    cout << reinterpret_cast<void *>(Fpff) << endl;
    //cout << reinterpret_cast<void *>(Swap) << endl;

    system("pause");
    return 0;
}

可以发现两个不同类型的函数指针,指向同一个函数模板,打印的地址却都不一样,显然编译器默默帮我们生成了两个不同的真正函数。

关于reinterpret_cast的解释看这个博客,由于这算是高级特性,可以暂时先不管它,不影响我们认识泛型。

多参函数模板

在我们之前小节学的函数模板都是单参数的, 其实函数模板可以定义任意多个不同的类型参数,例如:

template <typename T1,typename T2,typename T3>       
T1 Add(T2 a,T3 b)
{
    return static_cast<T1>(a+b);      
}

注意:

  • 工程中一般都将返回值参数作为第一个模板类型
  • 如果返回值参数作为了模板类型,则必须需要指定返回值模板类型.因为编译器无法推导出返回值类型
  • 可以从左向右部分指定类型参数 

接下来开始试验多参数函数模板

#include <iostream>
using namespace std;

template<typename T1,typename T2,typename T3>
T1 Add(T2 a,T3 b)
{
    return static_cast<T1>(a + b);
}
int main()
{
    //int a = Add(1, 1.5);

    int a = Add<int>(1, 1.5);
    cout << a << endl;

    float b = Add<float, int, float>(1, 1.5);
    cout << b << endl;

    system("pause");
    return 0;
}

重载函数模板

  • 函数模板可以像普通函数一样被重载
  • 函数模板不接受隐式转换
  • 当有函数模板,以及普通重载函数时,编译器会优先考虑普通函数
  • 如果普通函数的参数无法匹配,编译器会尝试进行隐式转换,若转换成功,便调用普通函数
  • 若转换失败,编译器便调用函数模板
  • 可以通过空模板实参列表来限定编译器只匹配函数模板

突然发问:重载和重写的区别?--答曰,多态中子类override掉父类的虚函数就是函数重写;如果两个函数名字一样,但是形参不一样,那就是重载。

什么叫做显式转换,什么叫做隐式转换?参考这个博客和这个博客。其实很好记,隐式就是系统给你做了,显式就是你自己做。

接下来开始试验重载函数模板

#include <iostream>
using namespace std;

template <typename T>
T Max(T a,T b)
{
    cout << "T Max(T a,T b)" << endl;
    return a > b ? a : b;
}

template <typename T>
T Max(T* a,T* b) //重载函数模板 
{
    cout << "T Max(T* a,T* b)" << endl;
    return *a > *b ? *a : *b;
}

int Max(int a,int b) //重载普通函数 
{
    cout << "int Max(int a,int b)" << endl;
    return a > b ? a : b;
}

int main()
{
    int a = 0;
    int b = 1;

    cout << "a:b=" << Max(a, b) << endl; //调用普通函数 Max(int,int)
    cout << "a:b=" << Max<>(a, b) << endl; //通过模板参数表 调用 函数模板 Max(int,int)
    cout << "1.5:2.0=" << Max(1.5, 2.0) << endl; 
    //由于两个参数默认都是double,所以无法隐式转换,则调用函数模板 Max(double,double)
    int *p1 = new int(1);
    int *p2 = new int(2);

    cout << "*p1:*p2=" << Max(p1, p2) << endl; // 调用重载函数模板 Max(int* ,int* )    
    cout << "'a',100=" << Max('a', 100) << endl;
    //将char类型进行隐式转换,从而调用普通函数 Max(int,int)

    delete p1;
    delete p2;

    system("pause");
    return 0;
}

C++泛型编程之类模板

好啦~讲完函数模板之后,让我们来康康类模板吧! 毕竟类对于C++来说是灵魂啊灵魂啊灵魂啊~~~

前言

和函数模板一样,将泛型思想应用于类.

编译器对类模板处理方式和函数模板相同,都是进行2次编译

类模板通常应用于数据结构方面,使得类的实现不在关注数据元素的具体类型,而只关注需要实现的功能

比如: 数组类,链表类,Queue类,Stack类等

使用方法

  • 通过template关键字来声明,然后通过typename关键字来定义模板类型,如下图所示:

类模板的使用

  • 定义对象时,必须指定类模板类型,因为编译器无法推导类型
  • 使用具体类型<Type>来定义对象

如下图所示:

初探类模板

写一个类模板,实现不同类型的加减乘除

#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Operator
{
public:
    T add(T a,T b)
    {
        return a + b;
    }
    T minus(T a,T b)
    {
        return a - b;
    }
    T multiply(T a,T b)
    {
        return a * b;
    }
    T divide(T a,T b)
    {
        return a / b;
    }
};
string operator-(string& l,string& r) //由于string类没有重载减号操作符,所以我们自定义一个
{
    return "Minus";
}
int main()
{
    Operator<int> op1; //定义对象时,需要指定类模板类型
    cout << op1.add(1, 2) << endl;

    Operator<string> op2; //定义对象时,需要指定类模板类型
    cout << op2.add("D.T.", "Software") << endl;
    cout << op2.minus("D.T.", "Software") << endl;

    system("pause");
    return 0;
}

类模板的工程应用

  • 类模板必须在.h头文件中定义
  • 类模板的成员函数不能分开在不同的文件中实现
  • 类模板外部定义的成员函数,和模板函数一样,还需要加上模板template <typename T>声明,以及结构体<T>声明

接下来,我们便修改上面代码定义的Operator类模板,只需要写Operator.h文件即可:

多参类模板

类模板可以定义任意多个不同的类型参数,同时还要必须指定每个模板参数

例如:

#include <iostream>
using namespace std;

template<typename T1,typename T2>
class Operator
{
public:
    void add(T1 a, T2 b);
};

template<typename T1,typename T2>
void Operator<T1,T2>::add(T1 a,T2 b)
{
    cout << a + b << endl;
}

int main()
{
    Operator<int, float>op1; //定义op1对象时,必须指定类模板类型
    op1.add(2, 2.1);
    system("pause");
    return 0;
}

从结果来看,上面的类模板好像已经实现了add加法运算.但是却不能支持指针类型.

其实,类模板也可以像函数重载一样, 类模板通过特化的方式可以实现特殊情况.

类模板特化

  • 表示可以存在多个相同的类名,但是模板类型都不一致(和函数重载的参数类似)
  • 特化类型有完全特化部分特化两种类型
  • 完全特化表示显示指定类型参数,模板声明只需写成template<>,并在类名右侧指定参数,比如:
#include <iostream>
using namespace std;

template<typename T1,typename T2> //声明的模板参数个数为2个
class Operator //正常的类模板
{
public:
    void add(T1 a,T2 b)
    {
        cout << a + b << endl;
    }
};

template<> //不需要指定模板类型,因为是完全特化的类模板
class Operator<int,int> //指定类型参数,必须为2个参数,和正常类模板参数个数一致
{
public:
    void add(int a,int b)
    {
        cout << a + b << endl;
    }
};

template<typename T> //有指定模板类型以及指定参数,所以是部分特化的类模板  
class Operator<T*,T*> //指定类型参数,必须为2个参数,和正常类模板参数个数一致
{
public:
    void add(T* a,T* b)
    {
        cout << *a + *b << endl;
    }
};

int main()
{
    Operator<int, int> Op1; //匹配完全特化类模板:class Operator< int,int>
    Operator<int, float> Op2; //匹配正常的类模板
    Operator<int*, int*>Op3; //匹配部分特化: class Operator< T* ,T*>

    system("pause");
    return 0;
}

 

  • 部分特化表示通过特定规则约束类型参数,模板声明和类似,并在类名右侧指定参数
  • 编译时,会根据对象定义的类模板类型,首先去匹配完全特化,再来匹配部分特化,最后匹配正常的类模板.

 

初探类模板特化

#include <iostream>

using namespace std;

template < typename T1, typename T2 >
class Operator                                            //正常的类模板
{
public:
    void add(T1 a, T2 b)
    {
        cout << "add(T1 a, T2 b)" << endl;
        cout << a + b << endl;
    }
};

template < typename T >
class Operator<T, T>                                //部分特化的类模板,当两个参数都一样,调用这个
{
public:
    void add(T a, T b)
    {
        cout << "add(T a, T b)" << endl;
        cout << a + b << endl;
    }
};

template < typename T1, typename T2 >
class Operator<T1*, T2*>                                   //部分特化的类模板,当两个参数都是指针,调用这个
{
public:
    void add(T1* a, T2* b)
    {
        cout << "add(T1* a, T2* b)" << endl;
        cout << *a + *b << endl;
    }
};

template < >
class Operator<void*, void*>                             //完全特化的类模板,当两个参数都是void*,调用这个
{
public:
    void add(void* a, void* b)
    {
        cout << "add(void* a, void* b)" << endl;
        cout << "add void* Error" << endl;                 //void*无法进行加法
    }
};

int main()
{
    int *p1 = new int(1);
    float *p2 = new float(1.25);

    Operator<int, float>  Op1;        //匹配正常的类模板:class Operator      
    Op1.add(1, 1.5);

    Operator<int, int>  Op2;          //匹配部分特化的类模板:class Operator<T,T>
    Op2.add(1, 4);

    Operator<int*, float*>  Op3;      //匹配部分特化的类模板:class Operator<T1*,T2*>      
    Op3.add(p1, p2);

    Operator<void*, void*>  Op4;      //匹配完全特化的类模板:class Operator<void*,void*>
    Op4.add(NULL, NULL);

    delete p1;
    delete p2;

    system("pause");
    return 0;
}

 

数值型模板参数

之前,我们学习的模板参数都是带泛型的(表示不同类型),其实模板参数也可以是数值型参数,如下图所示:

  • 数值型模板参数必须在编译时被唯一确定

比如: 变量在运行期间是可变的,所以不能作为模板参数.以及浮点数(不精确),类对象(可变)等等.

 

接下来,我们便通过数值参数的类模板来求 1+2+3+...+N的值

template < int N >
class Sum
{
public:
    static const int VALUE = Sum<N-1>::VALUE + N;              //定义静态常量并赋值
};
template < >
class Sum < 1 >
{
public:
    static const int VALUE = 1;
};

int main()
{
    cout << "1 + 2 + 3 + ... + 10 = " << Sum<10>::VALUE << endl;
    cout << "1 + 2 + 3 + ... + 100 = " << Sum<100>::VALUE << endl;
    return 0;
}


总体而言,类模板确实很好用,在项目中也很常见。但是要想自己也能盲打出来,需要多写多总结。。。

c++真他娘的复杂。。。

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页