C++模板

本文最后更新于:2021年2月5日 晚上

概览:C++函数模板、类模板,类模板与继承、类模板成员函数实现。

模板就是建立通用的模具,来提高代码的复用性

模板不能够直接使用,它只是框架。此外模板并非万能。

函数模板基本语法

语法template<typename T>或者template<class T>.然后接一个函数。

  1. typename或者class均可,看个人的使用习惯。
  2. T这个也可以随意替换,代表通用模板类型。
  3. 函数模板可以由多个类型,使用都逗号分割即可。template<class T,class P,class M>
  4. 函数模板这句话的声明只对其后紧挨着的第一个函数有效。

函数模板的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

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

void test1() {

int a = 10;
int b = 20;

swap(a, b);
cout << "a = " << a << " , b = " << b << endl;

int c = 10;
int d = 20;

myswap(c,d); //自动类型推导的方式
cout << "c = " << c << " , d = " << d << endl;

char e = 'A';
char f = 'B';

myswap<char>(e,f); //显式指定类型
cout << "e = " << e << " , f = " << f << endl;

}

显然,使用函数模板的优点是:将参数类型化,从而提高代码的复用性

使用函数模板的两种方式:

  • 自动类型推导myswap(c,d);不指定类型,让编译器自行推导。
  • 显式指定类型myswap<char>(e,f);,显式的在模板参数列表中指定类型。
  • 推荐使用显式指定类型的方式。

函数模板使用规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template<typename T>
void myswap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}

template<typename T>
void func() {
cout << "func" << endl;
}

//与上一个函数构成重载
template<typename T>
void func(T a) {
cout << a << " func" << endl;
}

void test2() {

int a = 10;
char b = 'A';

//myswap(a,b); //错误:没有与参数列表匹配的函数模板
//myswap<int>(a, b); //错误:没有与参数列表匹配的函数模板

//func(); //错误:没有与参数列表匹配的函数模板
func<int>(); //正确,模板必须确定T的数据类型,才可以使用

func<int>(a); //正确,重载函数也可以调用

}
  1. 对于模板参数类型T,必须推导出一致的数据类型才能够使用。
  2. 模板必须确定T的数据类型,才可以使用。
  3. 函数模板可以重载

普通函数与函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int add(int a, int b) {
return a + b;
}

template<typename T>
T t_add(T a, T b) {
return a + b;
}

void test3() {
int a = 1;
int b = 2;
char c = 'A';

cout<<add(a, b)<<endl; //3
cout<<add(a, c)<<endl; //66 自动类型转换,char->int

//t_add(a, c);//错误:没有与参数列表匹配的函数模板

cout << t_add<int>(a, c)<<endl;//正确 66
}

区别

  1. 普通函数调用时可以发生自动类型转换,即隐式类型转换。
  2. 函数模板调用时,若是自动类型推导,就不会发生隐式类型转换。
  3. 函数模板使用显式类型调用时,可以发生类型转换。

普通函数与模板函数调用规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void myfunc(int a) {
cout << "普通函数" << endl;
}

template<typename T>
void myfunc(T a) {
cout << "模板函数" << endl;
}

void test4() {

int a = 10;

//1.当普通函数和模板函数均可调用调用时,优先调用普通函数

myfunc(a); //普通函数

//2.可以通过空模板参数列表来强制调用函数模板

myfunc<>(a); //模板函数

char b = 'B';

//3.当函数模板可能产生更好的匹配时,优先调用函数模板

myfunc(b); //模板函数

}
  1. 当普通函数和模板函数均可调用调用时,优先调用普通函数。
  2. 可以通过空模板参数列表来强制调用函数模板。
  3. 当函数模板可能产生更好的匹配时,优先调用函数模板。

一般的建议是:提供了模板函数就不要再提供普通函数了,容易出现二义性。

函数模板的局限性

模板函数并非万能。例如

1
2
3
4
5
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}

当T为自定的类时,通常会无法正常运行。

自定义数据类型的解决

方式一:类重载比较运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
public:
int id;
bool operator>(const Person& p) {
if (this->id > p.id)
return true;
else return false;
}
};

template<typename T>
void cmp(T &a, T& b) {
if (a > b)
cout << ">" << endl;
else cout << "!>" << endl;
}

void test5() {
Person p1 = { 10 };
Person p2 = { 12 };

cmp(p1, p2);
}

缺点就是可能要对每一个比较运算符都需要进行重载。

方式二:具体化模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
public:
int id;
};

template<typename T>
void cmp(T &a, T& b) {
if (a > b)
cout << ">" << endl;
else cout << "!>" << endl;
}

//具体化模板
template<> void cmp(Person &a, Person &b) {
if (a.id > b.id)
cout << ">" << endl;
else cout << "!>" << endl;
}

void test5() {
Person p1 = { 10 };
Person p2 = { 12 };

cmp(p1, p2);
}

具体化模板写法:template<> 函数返回值类型 函数名(具体类型 参数)

  • 具体化模板会优先于普通模板

类模板

语法template<typename T>或者template<class T>.然后接一个类。

具体规则和函数模板类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<class T1,class T2>
class Person {
public:
Person(T1 name,T2 age):m_name(name),m_age(age)
{}

void showInfo() {
cout << "name:" << m_name << ", age:" << m_age << endl;

cout << "name type:" << typeid(m_name).name() << endl;
cout << "age type:" << typeid(m_age).name() << endl;
}

T1 m_name;
T2 m_age;
};

void test1() {
Person<string, int> p1 = Person<string, int>("Cc",18);
//另一种对象的写法:
//类模板名<真实类型参数表> 对象名(构造函数实参表);
p1.showInfo();
}

输出结果:

1
2
3
name:Cc, age:18
name type:class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
age type:int

类模板与函数模板

区别

  • 类模板必须使用显式指定类型,没有自动类型的推导方式。
  • 函数模板如上面所示,可以有隐式类型推导。

而可以通过在模板参数列表中设置默认参数来减少指定的类型.

1
2
3
4
5
6
template<class T1,class T2 = int>
class Person {……};

//调用时:
Person<string> p1 = Person<string>("Cc",18);
//或者 Person<string> p1("Cc",18);

函数模板可在类模板中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class T>
class Animal {
public:
Animal(T id) :m_id(id) {}

template<typename T2>
void func(T2 t2) {
cout << t2 << " " << m_id << endl;
}

private:
T m_id;
};

void test2() {

Animal<int> animal(249);

animal.func<string>("Id为");

}

类模板中成员函数的创建时机

  • 普通类中的成员函数在一开始的时候就可以创建。
  • 而类模板中的成员函数在调用时才会生成,所以有些代码编辑器检查不出问题,只有编译生成时才会检查出问题。

不同类型—类模板的不兼容

1
2
3
4
Person<string, int> *p1;
Person<string, double> p2("Bb", 12);

p1 = &p2; //错误

类模板对象做函数参数

当类模板实例化出来的对象作为函数参数时,传参的方式有:

  1. 传入指定的类型,即直接显式的设置为对象的数据类型。使用较为广泛。
  2. 将参数模板化,将对象中的参数变为模板进行传递。
  3. 将整个类模板化,直接将这个对象类型模板化传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
template<class T1,class T2>
class Person {
public:
Person(T1 name,T2 age):m_name(name),m_age(age)
{}

void showInfo() {
cout << "name:" << m_name << ", age:" << m_age << endl;

cout << "name type:" << typeid(m_name).name() << endl;
cout << "age type:" << typeid(m_age).name() << endl;
}

T1 m_name;
T2 m_age;
};

//1.传入指定的类型,即直接显式的设置为对象的数据类型
void myfunc1(Person<string,int> &p) {
p.showInfo();
}

//2.将参数模板化,将对象中的参数变为模板进行传递
template<typename T1,typename T2>
void myfunc2(Person<T1,T2> &p) {
p.showInfo();
}

//3.将整个类模板化,直接将这个对象类型模板化传递。
template<typename T>
void myfunc3(T &t) {
t.showInfo();
}

void test3() {
Person<string, int> p("Cc", 18);

myfunc1(p);
myfunc2<string,int>(p);
myfunc3<Person<string, int>>(p);

}

类模板与继承

如果基类是类模板,派生类继承时要指明基类T的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//1.普通类继承类模板
template<class T>
class Base1 {
T m;
};

//class Son1 :public Base {};//错误,缺少类模板的参数列表
//c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son1 :public Base1<int> {}; //正确

//2.类模板 继承 普通类
class Base2 {
int m;
};

template<class T>
class Son2:public Base2{
T v;
};

//3.类模板 继承 指定了类型的模板类
template<class T>
class Base3 {
T m;
};

template<class T>
class Son3 :public Base3<int> {
T n;
};

//4.类模板 继承 类模板
template<class T>
class Base4 {
T m;
};

template<class BT,class ST>
class Son4 :public Base4<BT> {
ST n;
};

void test4() {

Son1 son1; //Base1<int>, Son1

Son2<int> son2; //Base2, Son2<int>

Son3<char> son3; //Base3<int>, Son3<char>

Son4<int,char> son4;//Base4<int>, Son4<int,char>
}

类模板成员函数类外实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<class NameType,class IdType>
class Person
{
public:
Person(NameType name, IdType id);

void showInfo();

private:
NameType m_name;
IdType m_id;
};

//类外实现

template<class NameType, class IdType>
Person<NameType, IdType>::Person(NameType name, IdType id)
{
m_name = name;
m_id = id;
}

template<class NameType, class IdType>
void Person<NameType, IdType>::showInfo() {
cout << "name: " << m_name << " , id: " << m_id << endl;
}
  • 类外实现的时候,要把模板的声明加上,而且作用域限定符前的类要加上类名以及模板参数列表

产生问题:类模板分文件编写

上面的代码,进行分文件编写时:

类的定义,Person.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//文件:Person.h
#pragma once

template<class NameType,class IdType>
class Person
{
public:
Person(NameType name, IdType id);

void showInfo();

private:
NameType m_name;
IdType m_id;
};

类的实现,Person.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "Person.h"

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

template<class NameType, class IdType>
Person<NameType, IdType>::Person(NameType name, IdType id)
{
m_name = name;
m_id = id;
}

template<class NameType, class IdType>
void Person<NameType, IdType>::showInfo() {
cout << "name: " << m_name << " , id: " << m_id << endl;
}

main函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

#include "Person.h"

int main() {

Person<string, int> p("VS",2017);
p.showInfo();

return 0;
}

执行时,编译器会报错误,链接时错误:无法解析的外部符号……

产生错误原因:类模板中的成员函数的创建时机是在调用阶段,分文件编写时,链接不到这些成员函数。

解决方式

  1. main函数那里包含.cpp文件。
  2. 将这个模板类的声明与实现写在同一个文件里,并将文件后缀改为.hpp.hpp是约定成俗的习惯,也是主流的解决方式。

一般来说含有模板类的文件都不应该进行分文件编程,它们的声明与实现都应该放在同一个文件之中,并使用.hpp这种文件格式进行存储。

STL中有许多都是采用的这种方式。

类模板与友元

友元函数,全局类内实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class T>
class Person {

//友元,全局函数类内实现
friend void showInfo(Person<T> &p)
{
cout << p.m_name << endl;
}

public:
Person(T name) :m_name(name) {}

private:
T m_name;
};


void test1() {
Person<string> p("Bob");

showInfo(p);
}

如上,在类内的全局函数加上friend即可。

友元函数,全局类外实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//先声明这个模板类,否则全局友元函数找不到Man
template<class T>
class Man;

//全局友元函数的实现,放在类的实现的前面,否则类中找不到这个函数的声明
template<class T>
void showManInfo(Man<T> &m) {
cout << m.m_name << endl;
}

template<class T>
class Man {

template<class T> //VS中必须要有
friend void showManInfo(Man<T> &m);

public:
Man(T name) :m_name(name) {}

private:
T m_name;
};


void test2() {
Man<string> m("Alice");

showManInfo(m);
}

类外实现比较复杂,为了能让编译器识别必须先放类的声明,再放函数,再放类的实现。

关于类外实现时 类内友元声明的写法:

  1. 第一种如上所示,template<class T>友元声明前必须加这个,否则执行时会报错,但是据说这种方式在Linux上不适用。
  2. 第二种方式:friend void showManInfo <T>(Man<T> &m);,这种在全平台均适用。

这两种方式仅类内友元声明时方式不同,类外的实现写法一模一样,没有任何差别。

建议使用全局函数做类内的实现,用法简单,而且编译器可以直接识别。

类模板与静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template<class T>
class Cat {
public:
Cat(T name) :m_name(name) {}

static int sta_a;

static T sta_b;

static void changeSta_a() {
sta_a++;
cout << "sta_a=" << sta_a << endl;
}

T m_name;
};


//类的静态数据成员,类内声明,类外初始化
template<class T>
int Cat<T>::sta_a = 0;

void test3() {
Cat<int> c1(1);
Cat<string> c2("123");

c1.changeSta_a(); //1
c1.changeSta_a(); //2


c2.changeSta_a(); //1

}
  • 静态数据成员初始化的时候,记得加上template<class T>以及作用域Cat<T>

从上述代码可以看到,从类模板实例化的每一个模板类都有自己的类模板数据成员,该模板的所有对象共享一个static的数据成员。