C++基础——函数模板与类模板

应该就是 C++ 的一个泛型编程了。

泛型编程

泛型编程主要是因为这样的一个应用场景:如果我要实现一个数的加法,那么数可以是整数,浮点数,长整数等等等,返回值也可以是这些类的其中一个,排列组合一下可以有很多种组合,但是它们函数内部的功能几乎一模一样,这样的话就会比较繁琐,泛型编程就很好的解决了这一个问题。

比如算竞常用的函数 sort 可以排序,它能排很多种数据类型,只要数据类型支持小于号,那么就可以使用 sort,就算没有小于号我们也可以重载小于号使用。

函数模板

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<string>
template<typename T>//古老版本中没有 typename 关键字,用的是 class,效果一样
T add(T a, T b) {
return a + b;
}

int main() {
std::cout << add(1, 2) << std::endl;
std::cout << add(1.2, 2.6) << std::endl;
std::cout << add((std::string)"hello",(std::string)" world") << std::endl;
}

我们先用一句 template<typename T> 去声明一个模板类型 T,然后再定义一个函数,这里我们让两个参数和返回值一致,使用模板类型,内部调用 + 返回即可,结果也是可以正常运行的。

其实这个模板只是一个模板,真正调用的时候会根据用到的情况去实例化一个具体的函数,比如这里我用到了 int float 和 string 类的 add 函数我就会在编译的时候根据模板生成这三个函数,分别对应了类型。

调试一下也可以看到

在整型调用的时候,call 的是 add<int> 函数,而浮点调用,call 的是 add<double> 函数,因此模板的作用是基于一个规则自适应地生成函数。

我们也可以显示地去调用指定类型的模板,比如:add<int>(1,2) ,在这里 <> 中的类型会替换里面所有的 T 类型实例化出一个具体的对象,如果对象不支持模板里的一些操作,那么它会在编译阶段报出错误。

以上的函数模板有一个缺点,就是说你的两个参数必须同一类型,也就是说调用 add(1,1.5) 会报错,那么为了解决这个问题,我们有以下的解决方案:

  • 强制类型转换:这个应该都能想到,把它转成相同的就可以调用了。
  • 显示指定类型:这个我们指定函数类型,若参数类型不符则在传参时候会自动强制转换。
  • 模板重载:这个有点意思了,我们可以定义两个不同的 typename,这样的话即使两个类型不一样也可以支持了,但是前提是里面的操作需要支持我们调用时的类,template<typename T1,typename T2>
  • 非类型模板参数:我们可以在模板中定义参数,然后调用的时候在尖括号中传入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include<string>
template<typename T>
T add(T a, T b) {
return a + b;
}
template<typename T, int num>
T add(T val) {
return val + num;
}

int main() {
std::cout << add(1, 2) << std::endl;
std::cout << add(1.2, 2.6) << std::endl;
std::cout << add((std::string)"hello",(std::string)" world") << std::endl;
std::cout << add<float,2>(1.5) << std::endl;
}

运行之后可以发现没什么问题,函数正常调用并返回了正确的结果。

但是在模板列表中传参只能传 int 型的,类对象和浮点型不行。

类模板

这里就跟我们平时用到的 STL 库差不多了,声明方法和函数差不多,在类定义前声明一个模板类型,然后类内用模板类型的地方都会被替换成对应的显示类型,并且声明类的时候不支持隐式调用,因为它并不不能揣测你的内心想法。

demo

1
2
3
4
5
6
7
8
9
10
template<typename T>
class A
{
public:
A(T val) {
std::cout << val << std::endl;
}
~A() {};
};

我们实例化它也比较简单,跟STL差不多。

1
2
A<int> s(1);
A<std::string> g((std::string)"hello world");

模板全特化

有时候,我们希望某些类在被调调用的时候特殊处理,那么我们可以声明一个特化模板。

定义方法如下:

1
2
3
4
5
6
7
8
9
template<>
class A<std::string>
{
public:
A(std::string val) {
std::cout << "special:" << val << std::endl;
}
~A() {};
};

这样的话定义出来单独的类就会走新的调用空间了,我们上面调用 std::string 类的时候也会调用这里我们定义的函数。

模板偏特化

我们对主模板重定义

1
2
3
4
5
6
7
8
9
template<typename T1,typename T2>
class A
{
public:
A(T val) {
std::cout << val << std::endl;
}
~A() {};
};

然后呢为它再特化一个模板,特化条件是两个类型相同。

1
2
3
4
5
6
7
8
9
template<typename T>
class A<T,T>
{
public:
A(T val1) {
std::cout << "偏特化" << val1 << std::endl;
};
~A() {};
};

然后分别调用一下来看看

1
2
3
4
5
A<int,int> s(1);
A<std::string,int> g((std::string)"hello world");
//运行结果:
//偏特化1
//hello world

与函数一样,可以定义缺省,缺省其实就是默认参数,没传,它的值就是你给的默认值,你传了,用的就是你传的值。就像python里面的 int 函数,它有第二个参数表示是几进制的,你不传默认是十进制,你传了多少就是多少进制的,缺省参数必须在形参列表结尾。

demo

1
2
3
4
5
6
7
8
9
template<typename T1,typename T2=int>
class A
{
public:
A(T1 val) {
std::cout << val << std::endl;
}
~A() {};
};

与函数模板一样,里面也可以传 非类型模板参数,用法和函数模板一致,会基于参数生成不同的实例化类。