c++ 中的模板通过将类型参数化,可以提高代码的复用性。模板并不能减少代码量,只是从开发者的角度来看,代码量减少了,复用性提高了;从二进制文件的角度看,代码量没有减小。
1 函数模板
当求两个数的和时,数据的类型可能是 int、float 或者 double等,如果不使用模板的话,我们需要写下边的代码,每种数据类型都实现一个函数。这样当我们需要使用其它数据类型时,就需要再写一个函数。
#include <iostream>
#include <string>int sum(int a, int b) {std::cout << "int" << std::endl;return a + b;
}float sum(float a, float b) {std::cout << "float" << std::endl;return a + b;
}double sum(double a, double b) {std::cout << "double" << std::endl;return a + b;
}int main() {std::cout << sum(1, 2) << std::endl;std::cout << sum(1.1f, 2.2f) << std::endl;std::cout << sum(1.1, 2.2) << std::endl;return 0;
}
c++ 提供了模板,模板可以把类型参数化。针对上边的代码,在 c++ 中就可以使用模板函数来实现。代码如下所示,只需要定义一个模板函数就可以。
#include <iostream>
#include <string>template <class T>
T sum(T a, T b) {std::cout << "sum" << std::endl;return a + b;
}int main() {std::cout << sum(1, 2) << std::endl; // 隐式调用, 类型为 intstd::cout << sum(1.1f, 2.2f) << std::endl; // 隐式转换,类型是 floatstd::cout << sum(1.1, 2.2) << std::endl; // 隐式转换,类型是 doublestd::cout << sum<int>(1, 2.2) << std::endl; // 显示转换,类型是 int, 如果不显式指定类型,编译器会出现二义性,到底是 int 还是 doublereturn 0;
}
模板函数的代码量减少了,但是编译之后的二进制文件并没有减少。使用 objdump -tT a.out 可以看到,其中生成了 3 个函数实例。将程序员的工作转化成了编译器的工作,如果是不使用模板,就需要开发者自己写那么多函数;使用模板的方式来定义函数,编译器会生成这些函数。
2 类模板
如下代码, Point_T 是一个模板类,表示一个点,其中包括点的横纵坐标 x 和 y,类型分别是 T1 和 T2。
#include <iostream>template <class T1, class T2>
class Point_T {
public:T1 x_;T2 y_;Point_T() : x_(0), y_(0) {std::cout << "default constructor" << std::endl;}Point_T(T1 x, T2 y) : x_(x), y_(y) {std::cout << "constructor" << std::endl;}Point_T<T1, T2>& operator=(const Point_T<T1, T2> &point) {this->x_ = point.x_;this->y_ = point.y_;return *this;}// +运算符重载,需要声明为 friendfriend Point_T<T1, T2> operator +(Point_T<T1, T2> &point1, Point_T<T1, T2> &point2) {Point_T<T1, T2> tmp;tmp.x_ = point1.x_ + point2.x_;tmp.y_ = point1.y_ + point2.y_;return tmp;}// << 运算符重载,需要声明为 friendfriend std::ostream& operator<< (std::ostream &out, const Point_T<T1, T2>& point) {out << "(" << point.x_ << ", " << point.y_ << ")" << std::endl;return out;};
};int main() {Point_T<int, int> int_point1(1, 2);Point_T<int, int> int_point2(10, 20);// 看到一些书上说了类模板必须指定参数类型// 但是在自己的虚拟机上不指定,编译和运行也是可以的// 在实际使用中后,还是现实指定类型比较规范Point_T int_point3(10, 20);Point_T<float, float> float_point1(1.1f, 2.2f);Point_T<float, float> float_point2(10.10f, 20.20f);Point_T<int, int> int_total_point;Point_T<float, float> float_total_point;int_total_point = int_point1 + int_point3;float_total_point = float_point1 + float_point2;std::cout << int_total_point << std::endl;std::cout << float_total_point << std::endl;return 0;
}
通过 objdump 也能看出来,代码中根据参数类型对类进行了实例化。
3 常见问题
3.1 模板列表中能不能有具体的数据类型
如下代码,模板列表中有一个类型是基本数据类型是 int,这种方式是合法的,编译运行都没问题。不过在开发中基本不这么使用,在实际使用中,模板列表都是抽象的类型,而不是具体的类型。
#include <iostream>
#include <string>template <class T, int data>
class Test {
public:void Do() {std::cout << "data = " << data << std::endl;}
};int main() {Test<int, 1> t1;Test<int, 10> t2;Test<int, 100> t3;t1.Do();t2.Do();t3.Do();return 0;
}
使用 objdump 可以看到,在代码段生成了 3 份 Do() 函数。
3.1 模板特化
3.1.1 函数模板特化
如下 IsEqual() 是一个模板函数,但是在参数类型是 char * 的时候,使用 t1 == t2 来判断是不对的。针对 char * 类型,可以将模板特化,也就是单独写一个判断字符串是否相等的函数。
函数模板的特化就是在函数上面写一个空的模板列表 template<>,然后函数的参数类型写成具体的数据类型就可以。
#include <iostream>
#include <string>
#include <cstring>template <class T>
bool IsEqual(T t1, T t2) {return t1 == t2;
}// 模板特化
template <>
bool IsEqual(char *t1, char *t2) {return strcmp(t1, t2) == 0;
}int main() {char str1[] = "hello";char str2[] = "hello";std::cout << IsEqual(10, 10) << std::endl;std::cout << IsEqual(str1, str2) << std::endl;return 0;
}
3.1.2 类模板的特化
类的特化和函数的特化类似,在累的声明前边声明一个空的模板列表,然后在类名后边声明具体的数据类型。
#include <iostream>
#include <string>
#include <cstring>template <class T>
class Compare {
public:bool IsEqual(T t1, T t2) {return t1 == t2;}
};// 模板特化
template <>
class Compare<char *> {
public:bool IsEqual(char* t1, char* t2) {return strcmp(t1, t2) == 0;}
};int main() {char str1[] = "hello";char str2[] = "hello";Compare<int> c1;Compare<char *> c2;std::cout << c1.IsEqual(10, 10) << std::endl;std::cout << c2.IsEqual(str1, str2) << std::endl;return 0;
}