一.模板基础
思想准备
模板,顾名思义就是一个模子,通过这个模板可以建立很多东西,比如生活中的证件照,我们有一寸、两寸、白底、蓝底的模板,通过软件P图就可以合成最后我们需要的证件照。
但是我们需要注意如下两个点:
- 1.模板只是一个框架,它不能直接使用
- 2.模板是通用的,解决部分问题,但不是万能的
在C++中有函数模板和类模板两种模板。
二.函数模板
1.函数模板
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。
语法:
template<typename T>
//函数声明定义
template关键字用于告诉编译器我要开始写一个模板了;而typename其实是相当于定义了一个通用数据类型T,T可以代替int、double、char......中的任何一个,也可以替代你自己写的person、animal类。
常见的模型是写一个交换值的模板:
template<typename T>
void Swap(T *a, T *b)
{
T temp = *a;
*a = *b;
*b = temp;
}
这里用的是指针传递的方式,我们也可以使用值传递的方式。
调用模板有两种方式
//交换 int 变量的值
int n1 = 100, n2 = 200;
//1、自动推导T
Swap(&n1, &n2);
cout<<n1<<", "<<n2<<endl;
//交换 float 变量的值
float f1 = 12.5, f2 = 56.93;
//2、显示传递
Swap<float>(&f1, &f2);
cout<<f1<<", "<<f2<<endl;
一般使用时我们都使用自动推导的调用方式,但是需要注意:调用模板时编译器必须能确定T的数据类型,且推出来的数据类型一致。
2.练习:数组排序
案例描述:
- 利用函数模板封装一个排序的函数,可以对不同类型数组进行排序
- 排序规则为从大到小,排序算法为选择排序
- 分别利用
char数组和int数组进行测试
#include <iostream>
using namespace std;
//排序模板
template<typename T>
void Arrayswap(T arry[],int len)
{
for (int i = 0; i < len; i++)
{
int max = i;
for (int j = i + 1; j < len; j++)
{
if (arry[j] > arry[max])
max = j;
}
if (max != i)
{
T temp =arry[i];
arry[i] = arry[max];
arry[max] = temp;
}
}
for (int i = 0; i < len; i++)
cout << arry[i] << " ";
cout << endl;
}
void test01()
{
//测试数组1
char arry1[] = "bchgsk";
int num1 = sizeof(arry1) / sizeof(char);
Arrayswap(arry1, num1);
//测试数组2
int arry2[] = { 1,3,9,7,20,15 };
int num2 = sizeof(arry2) / sizeof(int);
Arrayswap(arry2, num2);
}
int main()
{
test01();
return 0;
}
3.对比普通函数
普通函数与函数模板区别:
- 普通函数调用时可以发生自动类型转换 (隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
普通函数与函数模板调用规则:
- 1、如果函数模板和普通函数都可以调用,优先调用普通函数
- 2、可以通过空模板参数列表 强制调用 函数模板
- 3、函数模板可以发生函数重载
- 4、如果函数模板可以产生更好的匹配,优先调用函数模板
总结:
- 利用具体化的模板,可以解决自定义类型的通用化
- 学习模板并不是为了写模板,而是在
STL能够运用系统提供的模板
三.类模板
1.类模板
C++除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。
声明类模板的语法为:
template<typename 类型参数1 , typename 类型参数2 , …>
class 类名{
//TODO:
};
类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。
一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
注意:实际开发过程中为了分辨类模板和函数模板,程序员一般在编写函数模板时用的是
typename关键字,而在编写类模板时用的是class关键字。
template<typename T1, typename T2> //这里不能有分号
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //获取x坐标
void setX(T1 x); //设置x坐标
T2 getY() const; //获取y坐标
void setY(T2 y); //设置y坐标
private:
T1 m_x; //x坐标
T2 m_y; //y坐标
};
上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:
template<typename 类型参数1 , typename 类型参数2 , …>
返回值类型 类名<类型参数1 , 类型参数2, ...>::函数名(形参列表){
//TODO:
}
第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。
例如对其中的某一个1函数类外定义如下:
template<typename T1, typename T2> //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
return m_x;
}
2.对比函数模板
类模板与函数模板区别主要有两点:
- 1.类模板没有自动类型推导的使用方式
- 2.类模板在模板参数列表中可以有默认参数
总结:
- 类模板使用只能用显示指定类型方式
- 类模板中的模板参数列表可以有默认参数
3.成员函数
#include <iostream>
using namespace std;
class Person1 {
public:
void show1() {
cout << "show1调用" << endl;
}
};
class Person2 {
public:
void show2() {
cout << "show2调用" << endl;
}
};
template<class T>
class Myclass {
public:
T obj;
void func1() {
obj.show1();
}
void func2() {
obj.show2();
}
};
void test01()
{
Myclass<Person1> a;
a.func1(); //正确
a.func2(); //错误
}
int main()
{
test01();
return 0;
}
总结:其实在Myclass定义时,不确定obj的类型,所以它可能调用show1也可能调用show2,这是因为类模板的成员函数并不是一开始就创建的,而是在调用时即test01中才去创建。
其次,类模板的成员函数如果在类外实现的话,需要加上模板参数列表,例如:
template<class T1,class T2>
class Myclass{
public:
T1 m_name;
T2 m_age;
Myclass(T1 name,T2 age) :m_name(name), m_age(age) {};
void show()
{
cout << "名字:" << this->m_name << "年龄:" < this->m_age << endl;
}
};
我们 把show函数在类外实现:
template<class T1,class T2>
void Myclass<T1,T2>::show()
{
cout << "名字:" << this->m_name << "年龄:" < this->m_age << endl;
}
4.类模板对象作为函数参数
共有三种传入方式:
- 1.指定传入的类型 : 直接显示对象的数据类型
- 2.参数模板化:将对象中的参数变为模板进行传递
- 3.整个类模板化:将这个对象类型 模板化进行传递
假设现在有一类模板:
template<class T1,class T2>
class Myclass{
public:
T1 m_name;
T2 m_age;
Myclass(T1 name,T2 age) :m_name(name), m_age(age) {};
void show()
{
cout << "名字:" << this->m_name << "年龄:" < this->m_age << endl;
}
};
现在我们创建一个对象并将其作为参数调用:
第一种调用方式:直接显示对象的数据类型
void print1(Myclass <string, int> &p) {
p.show();
}
void test01()
{
Myclass <string,int> p("孙悟空", 999);
print1(p);
}
第二种调用方式:参数模板化
template<typename T1, typename T2>
void print2(Myclass <T1, T2> &p) {
p.show();
}
void test01()
{
Myclass <string,int> p("孙悟空", 999);
print2(p);
}
第三种调用方式:整个类模板化
template<typename T>
void print3(T &p) {
p.show();
}
void test01()
{
Myclass <string,int> p("孙悟空", 999);
print3(p);
}
总结:
- 通过类模板创建的对象,可以有三种方式向函数中进行传参
- 使用比较广泛是第一种:指定传入的类型
5.类模板与继承
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
6.类模板分文件编写
当我们存在多个类模板时,不可避免的需要分文件编写,就上面我们定义的Myclass类来说,如果按照往常的分文件来编写,把类成员函数声明放在.h文件里,把实现放在.cpp源文件里,但是由于类模板成员函数的调用时机是运行时而不是编译时,从而会产生错误,导致编译器无法识别外部命令。
解决这类问题的方法有两个:
- 解决方式1:直接包含
.cpp源文件 - 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为
.hpp,hpp是约定的名称,并不是强制
解释:
- 方式一最直接但实际过程中很少使用,也不会把源文件发给别人,所以常使用第二种方式;
- 方式二的
.hpp文件任然是头文件,放在VS/VC的头文件下。
7.类模板与友元
关于友元我们知道,可以把一个全局函数、或一个成员函数或一个类定义为某个类的友元,那么这里如果把一个全局函数作为一个类模版的友元该怎么说呢?全局函数作为类模板的友元函数,可以在类内定义也可以在类外定义,类内声明可以在成员函数类内定义那样直接写,但是类外定义会出现某些小问题。
首先我们知道全局函数是没有作用域的,而成员函数如果在类外定义是需要作用域的,而友元函数的作用就是访问该类的私有属性,所以在友元函数中是不是要传入一个该类的参数,同理类模板中也需要传入一个类模板的参数,但是这样你会发现在类模板友元函数的声明处是一个全局函数,而到了类外变成了一个函数模板,虽然编译器不报错但是运行会出错,所以首先需要在类模板内中友元函数的声明处告诉这是一个全局函数的声明,所以需要使用空参数列表。
其次,友元一直有一个相关问题,就是顺序问题,在类内声明一个友元函数,需要让编译器提前知道存在这个友元全局函数,但全局函数又有类的参数,所以最好在开始处我们就把友元全局函数和类都声明一下。
四.类模板案例
1.案例描述
案例描述:实现一个通用的数组类,要求如下
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组的容量

2.实验代码
.hpp头文件代码:
#pragma once
#include <iostream>
#include<string>
using namespace std;
template<class T>
class Myarray {
public:
//尾插法增加数据
void push(const T& val)
{
if (this->m_capsity == this->m_size)
return;
this->addr[this->m_size] = val;
this->m_size++;
}
//尾删法删除数据
void pop()
{
if (this->m_size == 0)
return;
this->m_size--;
}
//重载中括号来访问某元素
T& operator[](int dex)
{
return this->addr[dex];
}
//返回数组容量
int get_cap()
{
return this->m_capsity;
}
int get_size()
{
return this->m_size;
}
//有参构造
Myarray(int num)
{
this->m_capsity = num;
this->m_size = 0;
this->addr = new T[this->m_capsity];
}
//拷贝构造
Myarray(const Myarray& arr)
{
this->m_capsity = arr.m_capsity;
this->m_size = arr.m_size;
//深拷贝
this->addr = new T[arr.m_capsity];
for (int i = 0; i < arr.m_size; i++)
{
this->addr[i] = arr.addr[i];
}
}
//operatrt=,防止链式套娃
Myarray& operator= (const Myarray& arr)
{
//先判断原来堆区是否有数据,如果有先释放
if (this->addr != NULL)
{
delete[] this->addr;
this->addr = NULL;
this->m_capsity = 0;
this->m_size = 0;
}
//深拷贝
this->m_capsity = arr.m_capsity;
this->m_size = arr.m_size;
this->addr = new T[arr.m_capsity];
for (int i = 0; i < arr.m_size; i++)
{
this->addr[i] = arr.addr[i];
}
return *this;
}
//析构函数
~Myarray()
{
if (this->addr != NULL)
{
delete[] this->addr;
this->addr = NULL;
}
}
private:
T* addr; //指向堆区开辟的真实数组
int m_capsity; //数组的容量
int m_size; //数组的大小
};
3.测试代码
test.cpp源文件代码:
#include <iostream>
#include <string>
#include "myarray.hpp"
using namespace std;
void print(Myarray<int>& p)
{
for (int i = 0; i < p.get_size(); i++)
{
cout << p[i] << endl;
}
}
class Person {
public:
Person() {};
Person(string name, int age)
{
this->name = name;
this->age = age;
}
string name;
int age;
};
void print123(Myarray<Person>& p)
{
for (int i = 0; i < p.get_size(); i++)
{
cout << "名字:" << p[i].name << " 年龄:" << p[i].age << endl;
}
}
void test01()
{
Myarray<int> p(5);
for (int i = 0; i < 5; i++)
{
p.push(i);
}
print(p);
cout << "容量:" << p.get_cap() << endl;
cout << "大小:" << p.get_size() << endl;
Myarray<int> q(p);
print(q);
q.pop();
print(q);
cout << "容量:" << q.get_cap() << endl;
cout << "大小:" << q.get_size() << endl;
}
void test02() {
Myarray<Person> arr(10);
Person p1("小明",999);
Person p2("小红", 999);
Person p3("小蓝", 999);
arr.push(p1);
arr.push(p2);
arr.push(p3);
print123(arr);
}
int main()
{
test01();
test02();
return 0;
}
注意:测试代码仅为了测试,不符合程序员平时习惯。
}
print§;
cout << “容量:” << p.get_cap() << endl;
cout << “大小:” << p.get_size() << endl;
Myarray q§;
print(q);
q.pop();
print(q);
cout << “容量:” << q.get_cap() << endl;
cout << “大小:” << q.get_size() << endl;
}
void test02() {
Myarray arr(10);
Person p1(“小明”,999);
Person p2(“小红”, 999);
Person p3(“小蓝”, 999);
arr.push(p1);
arr.push(p2);
arr.push(p3);
print123(arr);
}
int main()
{
test01();
test02();
return 0;
}
> 注意:测试代码仅为了测试,不符合程序员平时习惯。
本文详细介绍了C++中的函数模板和类模板,包括它们的原理、语法、使用方法、模板参数化、类型推导、模板与普通函数的区别、类模板的继承和友元等内容,以及一个通用数组类的实现案例。
1502

被折叠的 条评论
为什么被折叠?



