C++函数模板(模板函数)详解

定义

函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。(好吧,咱也听不懂,直接上用法吧?)

用法:

面向对象的继承和多态机制有效提高了程序的可重用性和可扩充性。在程序的可重用性方面,程序员还希望得到更多支持。举一个最简单的例子,为了交换两个整型变量的值,需要写下面的 Swap 函数:

void Swap(int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

为了交换两个 double 型变量的值,还需要编写下面的 Swap 函数:

void Swap (double & xr double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

如果还要交换两个 char 型变量的值,交换两个 CStudent 类对象的值……都需要再编写 Swap 函数。而这些 Swap 函数除了处理的数据类型不同外,形式上都是一样的。能否只写一遍 Swap 函数,就能用来交换各种类型的变量的值呢?继承和多态显然无法解决这个问题。因此,“模板”的概念就应运而生了。

众所周知,有了“模子”后,用“模子”来批量制造陶瓷、塑料、金属制品等就变得容易了。程序设计语言中的模板就是用来批量生成功能和形式都几乎相同的代码的。有了模板,编译器就能在需要的时候,根据模板自动生成程序的代码。从同一个模板自动生成的代码,形式几乎是一样的。

函数模板的原理

C++ 语言支持模板。有了模板,可以只写一个 Swap 模板,编译器会根据 Swap 模板自动生成多个 Sawp 函数,用以交换不同类型变量的值。

在 C++ 中,模板分为函数模板和类模板两种。

  • 函数模板是用于生成函数;
  • 类模板则是用于生成类的。

函数模板的写法如下:

template <class 类型参数1, class类型参数2, ...>
返回值类型  模板名(形参表)
{
    函数体
}

其中的 class 关键字也可以用 typename 关键字替换,例如:

template <typename 类型参数1, typename 类型参数2, ...>

函数模板看上去就像一个函数。前面提到的 Swap 模板的写法如下:

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

T 是类型参数,代表类型。编译器由模板自动生成函数时,会用具体的类型名对模板中所有的类型参数进行替换,其他部分则原封不动地保留。同一个类型参数只能替换为同一种类型。编译器在编译到调用函数模板的语句时,会根据实参的类型判断该如何替换模板中的类型参数。

例如下面的程序:

#include <iostream>
using namespace std;
template<class T>
void Swap(T & x, T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}
int main()
{
    int n = 1, m = 2;
    Swap(n, m);  //编译器自动生成 void Swap (int &, int &)函数
    double f = 1.2, g = 2.3;
    Swap(f, g);  //编译器自动生成 void Swap (double &, double &)函数
    return 0;
}

编译器在编译到Swap(n, m);时找不到函数 Swap 的定义,但是发现实参 n、m 都是 int 类型的,用 int 类型替换 Swap 模板中的 T 能得到下面的函数:

void Swap (int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

该函数可以匹配Swap(n, m);这条语句。于是编译器就自动用 int 替换 Swap 模板中的 T,生成上面的 Swap 函数,将该 Swap 函数的源代码加入程序中一起编译,并且将Swap(n, m);编译成对自动生成的 Swap 函数的调用。

同理,编译器在编译到Swap(f, g);时会用 double 替换 Swap 模板中的 T,自动生成以下 Swap 函数:

void Swap(double & x, double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

然后再将Swap(f, g);编译成对该 Swap 函数的调用。

编译器由模板自动生成函数的过程叫模板的实例化。由模板实例化而得到的函数称为模板函数。在某些编译器中,模板只有在被实例化时,编译器才会检查其语法正确性。如果程序中写了一个模板却没有用到,那么编译器不会报告这个模板中的语法错误。

编译器对模板进行实例化时,并非只能通过模板调用语句的实参来实例化模板中的类型参数,模板调用语句可以明确指明要把类型参数实例化为哪种类型。可以用:

模板名<实际类型参数1, 实际类型参数2, ...>

的方式告诉编译器应该如何实例化模板函数。例如下面的程序:

#include <iostream>
using namespace std;
template <class T>
T Inc(int n)
{
    return 1 + n;
}
int main()
{
    cout << Inc<double>(4) / 2;
    return 0;
}

Inc(4)指明了此处实例化的模板函数原型应为:

double Inc(double);

编译器不会因为实参 4 是 int 类型,就生成原型为 int Inc(int) 的函数。因此,上面程序输出的结果是 2.5 而非 2。

函数模板中可以有不止一个类型参数。例如,下面这个函数模板的写法是合法的:

template <class Tl, class T2>
T2 print(T1 argl, T2 arg2)
{
    cout << arg1 << " " << arg2 << endl;
    return arg2;
}

【实例】一个求数组中最大元素的函数模板
例题:设计一个分数类 CFraction,再设计一个名为 MaxElement 的函数模板,能够求数组中最大的元素,并用该模板求一个 CFmction 数组中的最大元素。

示例程序如下:

#include <iostream>
using namespace std;
template <class T>
T MaxElement(T a[], int size) //size是数组元素个数
{
    T tmpMax = a[0];
    for (int i = 1; i < size; ++i)
        if (tmpMax < a[i])
            tmpMax = a[i];
    return tmpMax;
}
class CFraction //分数类
{
    int numerator;   //分子
    int denominator; //分母
public:
    CFraction(int n, int d) :numerator(n), denominator(d) { };
    bool operator <(const CFraction & f) const
    {//为避免除法产生的浮点误差,用乘法判断两个分数的大小关系
        if (denominator * f.denominator > 0)
            return numerator * f.denominator < denominator * f.numerator;
        else
            return numerator * f.denominator > denominator * f.numerator;
    }
    bool operator == (const CFraction & f) const
    {//为避免除法产生的浮点误差,用乘法判断两个分数是否相等
        return numerator * f.denominator == denominator * f.numerator;
    }
    friend ostream & operator <<(ostream & o, const CFraction & f);
};
ostream & operator <<(ostream & o, const CFraction & f)
{//重载 << 使得分数对象可以通过cout输出
    o << f.numerator << "/" << f.denominator; //输出"分子/分母" 形式
    return o;
}
int main()
{
    int a[5] = { 1,5,2,3,4 };
    CFraction f[4] = { CFraction(8,6),CFraction(-8,4),
        CFraction(3,2), CFraction(5,6) };
    cout << MaxElement(a, 5) << endl;
    cout << MaxElement(f, 4) << endl;
    return 0;
}

编译到第 41 行时,根据实参 a 的类型,编译器通过 MaxElement 模板自动生成了一个 MaxElement 函数,原型为:

int MaxElement(int a[], int size);

编译到第 42 行时,根据 f 的类型,编译器又生成一个 MaxElement 函数,原型为:

CFraction MaxElement(CFraction a[], int size);

在该函数中,用到了<比较两个 CFraction 对象的大小。如果没有对<进行适当的重载,编译时就会出错。

从 MaxElement 模板的写法可以看出,在函数模板中,类型参数不但可以用来定义参数的类型,还能用于定义局部变量和函数模板的返回值。

延申用法

2.1为什么需要类模板

  • 类模板与函数模板的定义和使用类似,我们已经进行了介绍。 有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,如下面语句声明了一个类:
    在这里插入图片描述
  • 类模板用于实现类所需数据的类型参数化
  • 类模板在表示如数组、表、图等数据结构显得特别重要,
  • 这些数据结构的表示和算法不受所包含的元素类型的影响

2.2单个类模板语法

 1 //类的类型参数化 抽象的类
 2 //单个类模板
 3 template<typename T>
 4 class A 
 5 {
 6 public:
 7     A(T t)
 8     {
 9         this->t = t;
10     }
11 
12     T &getT()
13     {
14         return t;
15     }
16 protected:
17 public:
18     T t;
19 };
20 void main()
21 {
22    //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则
23     A<int>  a(100); 
24     a.getT();
25     printAA(a);
26     return ;
27 }

2.3继承中的类模板语法

在这里插入图片描述
案例1:

 1 //结论: 子类从模板类继承的时候,需要让编译器知道 父类的数据类型具体是什么(数据类型的本质:固定大小内存块的别名)A<int> 
 2 //
 3 class B : public A<int>
 4 {
 5 public:
 6     B(int i) : A<int>(i)
 7     {
 8 
 9     }
10     void printB()
11     {
12         cout<<"A:"<<t<<endl;
13     }
14 protected:
15 private:
16 };
17 
18 //模板与上继承
19 //怎么样从基类继承  
20 //若基类只有一个带参数的构造函数,子类是如何启动父类的构造函数
21 void pintBB(B &b)
22 {
23     b.printB();
24 }
25 void printAA(A<int> &a)  //类模板做函数参数 
26 {
27      //
28     a.getT();
29 }
30 
31 void main()
32 {
33     A<int>  a(100); //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则 
34     a.getT();
35     printAA(a);
36 
37     B b(10);
38     b.printB();
39 
40 
41     cout<<"hello..."<<endl;
42     system("pause");
43     return ;
44 }

案例2:

 1 #include<iostream>
 2 using namespace std;
 3 //A编程模板类--类型参数化
 4 /*
 5 类模板的定义 类模板的使用 类模板做函数参数
 6 */
 7 template <typename T>
 8 class A
 9 {
10 public:
11     A(T a = 0)
12     {
13         this->a = a;
14     }
15 public:
16     void printA()
17     {
18         cout << "a:" << a << endl;
19     }
20 protected:
21     T a;
22 private:
23     
24 };
25 //从模板类派生时,需要具体化模板类,C++编译器需要知道父类的数据类型是什么样子的
26 //要知道父类所占的内存多少
27 class B :public A<int>
28 {
29 public:
30     B(int a =10, int b =20):A<int>(a)
31     {
32         this->b = b;
33     }
34     void printB()
35     {
36         cout << "a:" << a << "b:" << b << endl;
37     }
38 protected:
39 private:
40     int b;
41     
42 };
43 //从模板类派生模板类
44 template <typename T>
45 class C :public A<T>
46 {
47 
48 public:
49     C(T c,T a) : A<T>(a)
50     {
51         this->c = c;
52     }
53     void printC()
54     {
55         cout << "c:" << c << endl;
56     }
57 protected:
58     T c;
59 private:
60     
61 };
62 
63 void main()
64 {
65     //B b1(1, 2);
66     //b1.printB();
67     C<int> c1(1,2);
68     c1.printC();
69 }

2.4类模板的基础语法

 1 #include<iostream>
 2 using namespace std;
 3 //A编程模板类--类型参数化
 4 /*
 5     类模板的定义 类模板的使用 类模板做函数参数
 6 */
 7 template <typename T>
 8 class A
 9 {
10 public:
11     A(T a = 0)
12     {
13         this->a = a;
14     }
15 public:
16     void printA()
17     {
18         cout << "a:" << a << endl;
19     }
20 protected:
21 private:
22     T a;
23 };
24 //参数 C++编译器具体的类
25 void UseA(A<int> &a)
26 {
27     a.printA();
28 }
29 void main()
30 {
31     //模板类本身就是抽象的,具体的类,具体的变量
32     A<int> a1(11),a2(22),a3(33);//模板类是抽象的, 需要类型具体化
33     //a1.printA();
34 
35     UseA(a1);
36     UseA(a2);
37     UseA(a3);
38 }

2.5类模板语法知识体系梳理

1.所有的类模板函数写在类的内部
代码:

复数类:

 1 #include<iostream>
 2 using namespace std;
 3 template <typename T>
 4 class Complex
 5 {
 6 public:
 7     friend Complex MySub(Complex &c1, Complex &c2)
 8     {
 9         Complex tmp(c1.a-c2.a, c1.b-c2.b);
10         return tmp;
11     }
12 
13     friend ostream & operator<< (ostream &out, Complex &c3)
14     {
15         out << c3.a << "+" << c3.b <<"i"<< endl;
16         return out;
17     }
18     Complex(T a, T b)
19     {
20         this->a = a;
21         this->b = b;
22     }
23     Complex operator+(Complex &c2)
24     {
25         Complex tmp(a + c2.a, b + c2.b);
26         return tmp;
27     }
28     void printCom()
29     {
30         cout << "a:" << a << " b:" << b << endl;
31     }
32 protected:
33 private:
34     T a;
35     T b;
36 };
37 
38 /*
39     重载运算符的正规写法:
40     重载左移<<  右移>> 只能用友元函数,其他的运算符重载都要用成员函数,不要滥用友元函数
41 */
42 //ostream & operator<< (ostream &out, Complex &c3)
43 //{
44 //    out<< "a:" << c3.a << " b:" << c3.b << endl;
45 //    return out;
46 //}
47 void main()
48 {
49     Complex<int>     c1(1,2);
50     Complex<int>     c2(3, 4);
51 
52     Complex<int> c3 = c1 + c2;//重载加号运算符
53     
54     c3.printCom();
55 
56     //重载左移运算符
57     cout << c3 << endl;
58     
59     {
60         Complex<int> c4 = MySub(c1 , c2);
61         
62         cout << c4 << endl;
63     }
64     system("pause");
65 }

2.所有的类模板函数写在类的外部,在一个cpp中

注意:

复制代码
//构造函数 没有问题

普通函数 没有问题

友元函数:用友元函数重载 << >>

friend ostream& operator<< (ostream &out, Complex &c3) ;

友元函数:友元函数不是实现函数重载(非 << >>)

1 需要在类前增加 类的前置声明 函数的前置声明

template<typename T>

class Complex;  

template<typename T>

Complex<T> mySub(Complex<T> &c1, Complex<T> &c2);

2 类的内部声明 必须写成:

friend Complex<T> mySub <T> (Complex<T> &c1, Complex<T> &c2);

3 友元函数实现 必须写成:

template<typename T>

  Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)

{

Complex<T> tmp(c1.a - c2.a, c1.b-c2.b);

return tmp;

}

4 友元函数调用 必须写成

Complex<int> c4 = mySub<int>(c1, c2);

cout<<c4;

结论:友元函数只用来进行 左移 友移操作符重载。

复数类:

代码:

 1 #include<iostream>
 2 using namespace std;
 3 
 4 template<typename T>
 5 class Complex;
 6 template<typename T>
 7 Complex<T> mySub(Complex<T> &c1, Complex<T> &c2);
 8 
 9 template <typename T>
10 class Complex
11 {
12 public:
13     friend Complex<T> mySub <T>(Complex<T> &c1, Complex<T> &c2);
14 
15     friend ostream & operator<< <T>(ostream &out, Complex &c3);
16     Complex(T a, T b);
17     void printCom();
18     Complex operator+(Complex &c2);
19     Complex operator-(Complex &c2);
20     
21 protected:
22 private:
23     T a;
24     T b;
25 };
26 
27 //构造函数的实现,写在了外部
28 template <typename T>
29 Complex<T>::Complex(T a, T b)
30 {
31     this->a = a;
32     this->b = b;
33 }
34 
35 template <typename T>
36 void Complex<T>::printCom()
37 {
38     cout << "a:" << a << " b:" << b << endl;
39 }
40 //成员函数实现加号运算符重载
41 template <typename T>
42 Complex<T> Complex<T>::operator+(Complex<T> &c2)
43 {
44     Complex tmp(a + c2.a, b + c2.b);
45     return tmp;
46 }
47 template <typename T>
48 Complex<T> Complex<T>::operator-(Complex<T> &c2)
49 {
50     Complex(a-c2.a,a-c2.b);
51     return tmp;
52 }
53 //友元函数实现<<左移运算符重载
54 
55 /*
56 严重性    代码    说明    项目    文件    行    禁止显示状态
57 错误    C2768    “operator <<”: 非法使用显式模板参数    泛型编程课堂操练    
58 
59 错误的本质:两次编译的函数头,第一次编译的函数头,和第二次编译的函数有不一样
60 */
61 template <typename T>
62 ostream & operator<< (ostream &out, Complex<T> &c3)//不加T
63 {
64     out << c3.a << "+" << c3.b << "i" << endl;
65     return out;
66 }
67 
68 
69 //
70 template <typename T>
71 Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)
72 {
73     Complex<T> tmp(c1.a - c2.a, c1.b - c2.b);
74     return tmp;
75 }
76 
77 void main()
78 {
79     Complex<int>     c1(1, 2);
80     Complex<int>     c2(3, 4);
81 
82     Complex<int> c3 = c1 + c2;//重载加号运算符
83 
84     c3.printCom();
85 
86     //重载左移运算符
87     cout << c3 << endl;
88 
89     {
90         Complex<int> c4 = mySub<int>(c1, c2);
91 
92         cout << c4 << endl;
93     }
94     system("pause");
95 }

所有的类模板函数写在类的外部,在不同的.h和.cpp中
也就是类模板函数说明和类模板实现分开

//类模板函数

构造函数

普通成员函数

友元函数

用友元函数重载<<>>;

用友元函数重载非<< >>

demo_complex.cpp

 1 #include"demo_09complex.h"
 2 #include<iostream>
 3 using namespace std;
 4 
 5 template <typename T>
 6 Complex<T>::Complex(T a, T b)
 7 {
 8     this->a = a;
 9     this->b = b;
10 }
11 
12 template <typename T>
13 void Complex<T>::printCom()
14 {
15     cout << "a:" << a << " b:" << b << endl;
16 }
17 //成员函数实现加号运算符重载
18 template <typename T>
19 Complex<T> Complex<T>::operator+(Complex<T> &c2)
20 {
21     Complex tmp(a + c2.a, b + c2.b);
22     return tmp;
23 }
24 //template <typename T>
25 //Complex<T> Complex<T>::operator-(Complex<T> &c2)
26 //{
27 //    Complex(a - c2.a, a - c2.b);
28 //    return tmp;
29 //}
30 template <typename T>
31 ostream & operator<< (ostream &out, Complex<T> &c3)//不加T
32 {
33     out << c3.a << "+" << c3.b << "i" << endl;
34     return out;
35 }
36 
37 
38 //
39 //template <typename T>
40 //Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)
41 //{
42 //    Complex<T> tmp(c1.a - c2.a, c1.b - c2.b);
43 //    return tmp;
44 //}

2.5总结

归纳以上的介绍,可以这样声明和使用类模板:

  1. 先写出一个实际的类。由于其语义明确,含义清楚,一般不会出错。

  2. 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)。

  3. 在类声明前面加入一行,格式为:

      template <class 虚拟类型参数>
    
      如:
    
          template <class numtype> //注意本行末尾无分号
    
          class Compare
    
            {}; //类体
    
    
  4. 用类模板定义对象时用以下形式:

     类模板名<实际类型名> 对象名;
    
          类模板名<实际类型名> 对象名(实参表列);
    
      如:
    
          Compare<int> cmp;
    
          Compare<int> cmp(3,7);
    
    
  5. 如果在类模板外定义成员函数,应写成类模板形式:

     template <class 虚拟类型参数>
    
         函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {}
    
    

关于类模板的几点说明:

  1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如:

    template <class T1,class T2>
    
     class someclass
    
     {};
    
    

    在定义对象时分别代入实际的类型名,如:

    someclass<int,double> obj;

  2. 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。

  3. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

2.6类模板中的static关键字

从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化
每个模板类有自己的类模板的static数据成员副本

 1 #include<iostream>
 2 using namespace std;
 3 template <typename T>
 4 class AA
 5 {
 6 public:
 7     static T m_a;
 8 protected:
 9 private:
10 };
11 template <typename T>
12 T AA<T>::m_a =0;
13 void main()
14 {
15     AA<int> a1, a2, a3;
16     a1.m_a = 10;
17     a2.m_a++;
18     a3.m_a++;
19     cout << AA<int>::m_a << endl;
20 
21     AA<char> b1, b2, b3;
22     b1.m_a = 'a';
23     b2.m_a++;
24     b3.m_a++;
25     cout << AA<char>::m_a << endl;
26 
27     //m_a是每个类型的类,去使用,手工写两个类 int  char
28     system("pause");
29 }

案例2:以下来自:C++类模板遇上static关键字

 1 #include <iostream>
 2 using namespace std;
 3 
 4 template<typename T>
 5 class Obj{
 6 public:
 7     static T m_t;
 8 };
 9 
10 template<typename T>
11 T Obj<T>::m_t = 0;
12 
13 int main04(){
14     Obj<int> i1,i2,i3;
15     i1.m_t = 10;
16     i2.m_t++;
17     i3.m_t++;
18     cout << Obj<int>::m_t<<endl;
19 
20     Obj<float> f1,f2,f3;
21     f1.m_t = 10;
22     f2.m_t++;
23     f3.m_t++;
24     cout << Obj<float>::m_t<<endl;
25 
26     Obj<char> c1,c2,c3;
27     c1.m_t = 'a';
28     c2.m_t++;
29     c3.m_t++;
30     cout << Obj<char>::m_t<<endl;
31 }

当类模板中出现static修饰的静态类成员的时候,我们只要按照正常理解就可以了。static的作用是将类的成员修饰成静态的,所谓的静态类成员就是指类的成员为类级别的,不需要实例化对象就可以使用,而且类的所有对象都共享同一个静态类成员,因为类静态成员是属于类而不是对象。那么,类模板的实现机制是通过二次编译原理实现的。c++编译器并不是在第一个编译类模板的时候就把所有可能出现的类型都分别编译出对应的类(太多组合了),而是在第一个编译的时候编译一部分,遇到泛型不会替换成具体的类型(这个时候编译器还不知道具体的类型),而是在第二次编译的时候再将泛型替换成具体的类型(这个时候编译器知道了具体的类型了)。由于类模板的二次编译原理再加上static关键字修饰的成员,当它们在一起的时候实际上一个类模板会被编译成多个具体类型的类,所以,不同类型的类模板对应的static成员也是不同的(不同的类),但相同类型的类模板的static成员是共享的(同一个类)。

2.7类模板在项目开发中的应用

小结

模板是C++类型参数化的多态工具。C++提供函数模板和类模板。

模板定义以模板说明开始。类属参数必须在模板定义中至少出现一次。

同一个类属参数可以用于多个模板。

类属参数可用于函数的参数类型、返回类型和声明函数中的变量。

模板由编译器根据实际数据类型实例化,生成可执行代码。实例化的函数。

模板称为模板函数;实例化的类模板称为模板类。

函数模板可以用多种方式重载。

类模板可以在类层次中使用 。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值