一篇文章带你了解C++模板编程详解

📅 2026/7/13 5:11:28 👁️ 阅读次数 📝 编程学习
一篇文章带你了解C++模板编程详解

泛型编程

在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术。什么是泛型呢?实质上就是不使用具体数据类型(例如 int、double、float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业务逻辑的实现。泛型也是一种数据类型,只不过它是一种用来代替所有类型的“通用类型”

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

voidSwap(int& left,int& right)

{

inttemp = left;

left = right;

right = temp;

}

voidSwap(double& left,double& right)

{

doubletemp = left;

left = right;

right = temp;

}

voidSwap(char& left,char& right)

{

chartemp = left;

left = right;

right = temp;

}

......

Swap函数能实现各种类型的变量交换,但是只要类型不同就需要重新写一个

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  • 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错,那能否告诉编译器一个模版,让编译器根据不同的类型利用该模版来生成代码呢?

可以的,C++语法中有了模板:

函数模板

函数模板概念

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。 这个通用函数就称为 函数模板(Function Template) 。函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}

1

2

3

4

5

6

7

8

template<typenameT>

//或者 template<class T>

voidSwap(T& x1, T& x2)

{

T temp = left;

left = right;

right = temp;

}

T1,T2等等是什么类型现在也不确定,一会用的时候才能确定

注意:

typename是用来定义模板参数关键字,也可以使用class

函数模板的原理

函数模板本身并不是函数,是编译器根据调用的参数类型产生特定具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器,我们看下面的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

template<classT>

voidSwap(T& x, T& y)

{

T temp = x;

x = y;

y = temp;

}

intmain()

{

inta = 1;

intb = 2;

Swap(a, b);

charA ='a';

charB ='b';

Swap(A,B);

return0;

}

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

然而当我们在写了函数时,不会进入模板函数里,没有写具体的函数时,就会进入模板函数里,我们看下面的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

voidSwap(int& x,int& y)

{

inttemp = x;

x = y;

y = temp;

}

template<classT>

voidSwap(T& x, T& y)

{

T temp = x;

x = y;

y = temp;

}

intmain()

{

inta = 1;

intb = 2;

Swap(a, b);

charA ='a';

charB ='b';

Swap(A,B);

return0;

}

我们进行调式:

我们可以看到int类型的交换函数我们写了,调用时调用的是我们写的,而char类型的我们没写,就用了模板。

那么这里调用的是模板函数吗?

不是的,实际上这里会有两个过程

1、模板推演,推演T的具体类型是什么

2、推演出T的具体类型后实例化生成具体的函数

上面的代码实例化生成了下面的函数:

1

2

3

4

5

6

voidSwap(char& x,char& y)

{

chartemp = x;

x = y;

y = temp;

}

真正调用的还是两个函数,但是其中的一个函数不是我们自己写的,而是我们给了编译器一个模板,然后编译器进行推演在编译之前实例化生成三个对应的函数,模板是给编译器用的,编译器充当了写函数的工具:

可以看到这里是调用了Swap<char>函数

在C++当中,其实内置类型也可以像自定义类型那样这样初始化:

1

2

inta(1);

int(2);//匿名

1

2

3

4

5

6

voidSwap(T& x1, T& x2)

{

T temp(x1);

x1 = x2;

x2 = x1;

}

所以模板还可以这样写,可以使内置类型和自定义类型兼容:

1

2

3

4

5

6

voidSwap(T& x1, T& x2)

{

T temp(x1);

x1 = x2;

x2 = x1;

}

我们来具体看一看函数模板的实例化:

函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

隐式实例化:让编译器根据实参推演模板参数的实际类型

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

template<classT>

T Add(constT& left,constT& right)

{

returnleft + right;

}

intmain()

{

inta1 = 10, a2 = 20;

doubled1 = 10.0, d2 = 20.0;

Add(a1, a2);

Add(d1, d2);

// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

Add(a1, d2);

return0;

}

该语句是不能够通过编译的,因为在编译期间,当编译器看到该实例化时,用a1去推T是int,而用d2去推是double,但是模板参数列表里只有一个T,编译器不能明确该T是int还是double,T是不明确的,所以编译器会报错

那么怎么处理呢?

解决方式:

1、调用者自己强制转换

1

2

3

//实参去推演形参的类型

Add(a1, (int)d2);

Add((double)a1,d2);

这里可以将d2先强制类型转换,然后再进行推演;或者将a1先强制类型转换再进行推演

2、使用显式实例化

1

2

3

//实参不需要去推演形参的类型,显式实例化指定T的类型

Add<int>(a1, d2);

Add<double>(a1,d2);

这种方式是显式实例化指定T的类型

显式实例化在哪种场景可用呢?看下面的这种场景:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

classA

{

A(inta=0):_a(a)

{}

private:

int_a;

};

template<classT>

T func(intx)

{

T a(x);

returna;

}

intmain()

{

func<A>(1);

func<int>(2);

return0;

}

有些函数模板里面参数中没用模板参数,函数体内才有用到模板参数,此时就无法通过参数去推演T的类型,这时只能显示实例化

上面我们提了一点模板参数的匹配原则,下面我们具体看看模板参数的匹配原则:

模板参数的匹配原则

一个非模板函数可以和一个同名的函数模板同时存在,此时如果调用地方参数与非模板函数完全匹配,则会调用非模板函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

intAdd(intleft,intright)

{

returnleft + right;

}

// 通用加法函数

template<classT>

T Add(T left, T right)

{

returnleft + right;

}

intmain()

{

Add(1,2);//调用自己的函数

return0;

}

Add(1,2)参数是int类型,而我们有现成的int参数的Add函数,所以有现成的就用现成的,编译器也会偷懒