模板
C++的另一种编程思想是泛型编程,其主要利用的技术就是模板。模板树妖分为两类: 函数模板 和 类模板 。
1.函数模板
所谓函数模板,就是一个返回值和形参类型不用具体指定的通用函数,是一个虚拟的类型。
其目的是提高代码的复用性,将类型参数化。
1.1 语法
template<typename T>
函数声明或者定义
其中,
- template——声明创建模板
- typename——表明其后面的符号是一种数据类型,也可以用class代替
- T——通用数据类型,任意规范的名称均可,一般为大写字母
1.2 范例
这里写一个简单的实例:
#include <iostream>
#include <string>
using namespace std;
template<typename T>
void myswap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
cout << a << "\t" << b << endl;
}
int main()
{
int a = 1, b = 2;
myswap(a, b);
double c = 1.3, d = 3.2;
myswap(c, d);
return 0;
}
1.3 自动类型推导与显式指定类型
在上面的例子中,我们定义完 a 和 b 后直接将其输入myswap() 的形参中去,事实上是编译器自动判断T的类型的,因此这种方式称为自动类型推导。
与自动类型推导相对的,是我们显式的去告诉编译器我们输入的实参的类型,其语法是:
myswap(a,b)
myswap<int>(a,b)
1.4 注意事项
- 自动类型推导,其数据类型必须一致;
- 模板必须确定T的数据类型才可以使用。
对于第一点,其实不完全对,或者说,仅在上面那种写法下是正确的。
template<typename T>
void myswap(T &a, T &b)
{
}
int main()
{
int a = 10;
char b = 'b';
myswap(a,b);
}
但是在下面的情况下是可以不一致的。
template<class T,class V>
void aaa(T &a, V &b)
{
cout << a - b << endl;
}
int main()
{
int a = 10;
char b = 'a';
aaa(a, b);
aaa<int,char>(a,b);
}
对于第二点,它的含义是,在模板函数的实现中必须出现通用类型T。
template<class T>
void aaa()
{
cout << "aaa()实现" << endl;
}
int main()
{
aaa();
}
1.5 与普通函数的区别
- 普通函数可以发生自动类型转换;
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 但是通过显式指定类型的方式,则可以发生隐式类型转换
另外说一句,在引用传递的时候不会发生隐式类型转换。
1.6 函数模板与普通函数的调用规则
- 在都能调用的情况下,优先普通函数
- 通过空模板参数列表强制调用函数模板
- 函数模板可以重载
- 如果函数模板可以产生更好的匹配,使用函数模板
在都能调用的情况下,优先普通函数
void myprint(int a)
{
cout << "普通函数" << endl;
}
template<typename T>
void myprint(T a)
{
cout << "模板函数" << endl;
}
int a =10;
myprint(a);
在这种情况下两个都能调用,编译器选择优先调用普通函数。
通过空模板参数列表强制调用函数模板
int a =10;
myprint<>(a);
这里利用空模板参数列表强制调用了模板函数。
函数模板可以重载
template<typename T>
void myprint(T a)
{
cout << "模板函数" << endl;
}
template<typename T>
void myprint(T a,T b)
{
cout << "重载的模板函数" << endl;
}
int a =10;
myprint(a);
myprint(1,2);
在这里,第一个函数调用的是普通函数;第二个调用的是重载的模板函数。
如果函数模板可以产生更好的匹配,使用函数模板
char a ='a';
myprint(a);
在这里,事实上依然是普通函数与模板函数都能调用,但是由于调用普通函数需要发生隐式类型转换,而模板函数不需要,只要自动推导T的类型即可,因此编译器选择调用函数模板。
1.7 局限性
虽然模板具有很高的泛用性,但是对于自定义数据类型而言可能略有不足。
class PPP
{
public:
PPP(string name,string id)
{
this->m_id = id;
this->m_name = name;
}
string m_name;
string m_id;
};
template<typename T>
bool com(T &a, T &b)
{
return (a == b);
}
int main()
{
PPP p1("tom","4732");
PPP p2("tom", "732");
if (com(p1, p2))
{
cout << "p1 == p2" << endl;
}
else
{
cout << "p1 != p2" << endl;
}
return 0;
}
对于上面的程序而言,com函数并不能判断PPP类的大小关系,因而这段代码无法实现。为了解决这个问题,有两种办法。其一就是之前学习的运算符重载,在这里我们可以重载==运算符。
bool PPP::operator==(const PPP &p)
{
return (this->m_id == p.m_id && this->m_name == p.m_name);
}
除了以上重载的办法,我们还可以利用模板特殊化这个com函数。
template<>bool com(PPP &a, PPP &p)
{
return (a.m_id == p.m_id && a.m_name == p.m_name);
}
这个看上去就像一个全局函数,而且与重载相比好像没什么简单的地方。但是也许以后会有用处,先放在这,如果以后有用到这个方法的话就进行补充。
2. 类模板
2.1 语法
在template关键字后跟上类的定义或声明即可。
template<class T>
2.2 范例
template<class TypeName, class Typeage>
class PPP
{
public:
PPP(TypeName name, Typeage age)
{
this->m_age = age;
this->m_name = name;
}
public:
TypeName m_name;
Typeage m_age;
};
2.3 与函数模板的区别
- 类模板没有自动类型推导,只能显式指定类型;
- 类模板的模板参数列表可以有默认参数的。
类模板没有自动类型推导 在上面类模板的基础下,
PPP<string,int> p1("ton",31);
PPP p1("ton",31);
类模板的模板参数列表可以有默认参数
template<class TypeName, class Typeage = int>
class PPP
{
public:
PPP(TypeName name, Typeage age)
{
this->m_age = age;
this->m_name = name;
}
public:
TypeName m_name;
Typeage m_age;
};
2.4 类模板中成员函数的创建
如果没有用类模板去实例化一个模板对象的话,那么类模板中的方法并不会被创建,换言之,只要对象没有实例化,即使类模板中有看似互相矛盾的两个方法,编译器也不会报错。
class ppp
{
public:
void show1()
{
cout << "ppp show()" << endl;
}
};
class qqq
{
public:
void show2()
{
cout << "qqq show()" << endl;
}
};
template<class T>
class myclass
{
public:
T obj;
void func1()
{
obj.show1();
}
void func2()
{
obj.show2();
}
};
int main()
{
myclass<ppp> myc;
myc.func1();
return 0;
}
func1 与 func2 在普通类中不可能同时出现,因为这个时候不可能同时调用两个类方法。模板参数确定后,编译器才会开始创建方法,在上面的代码中,我们指定模板参数为 ppp ,故而 func2 是无法调用的,会被编译器报错,但是 func1 仍然是可以正常工作的。也就是说,即使是模板参数被指定了, 看上去非法的 func2 也不会报错,只是无法调用罢了。
2.5 类模板对象做函数参数
主要有三种办法:
- 指定模板参数列表的对象
- 模板参数列表的参数化
- 模板对象的参数化
指定模板参数列表的对象
template<class T,class V>
class ppp
{
public:
T m_name;
V m_age;
ppp(T name, V age)
{
m_age = age;
m_name = name;
}
void pppshow()
{
cout << m_age << " " << m_name << endl;
}
};
void show(ppp<string, int> &p)
{
p.pppshow();
}
int main()
{
ppp<string, int> p1("zs",213);
show(p1);
}
模板参数列表的参数化 以及 模板对象的参数化
template<class T, class V>
void show2(ppp<T, V> &p)
{
p.pppshow();
}
template<class T>
void show3(T &p)
{
p.pppshow();
}
上面两个分别把函数形参的模板参数列表和类模板本身参数化了,事实上是一个类模板搭配函数模板的写法。
在实际使用中,指定参数列表的方式,也就是第一种方式是最常用的,后面两种方式都太过麻烦了。
2.6 类模板的继承
类模板做父类,子类必须指定父类的参数列表,无论子类是直接继承,还是通过类模板的方式继承。
template<class T,class V>
class base
{
public:
T m_name;
V m_age;
};
class son : public base<string, int>
{
public:
son();
};
template<class T1, class T2>
class son2 :public base<T1, T2>
{};
2.7 类模板方法的类外实现与分文件编写
首先是方法的类外实现。
template<class T>
class ppp
{
public:
ppp();
void showppp();
T m_member;
};
template<class T>
ppp<T>::ppp()
{}
template<class T>
void ppp<T>::showppp()
{}
与普通类不同的是,在作用域的地方必须申明这是一个类模板,因此需要加上模板参数列表;又由于通用类型参数编译器不认识,所以前面要加上template关键词。
有了类外实现之后就可以尝试去分文件编写类模板了。但是由于之前提到过,类模板的方法并不是在一开始就创建好的,而是在调用的时候才创建。因此用常规的 .h 和 .cpp 这样的形式进行份文件的话,编译器无法将其链接到一起,会报错。
为了解决这个问题,主流的办法是把类模板的申明和方法的实现写在同一个文件内,后缀改为 .hpp,使用的时候就是:
#include "****.hpp"
2.8 类模板的友元
考虑全局函数作为友元,它有两种实现方式。其一是直接在类内实现全局友元函数。
template<class T>
class ppp
{
friend void aaa(ppp<T> p)
{
cout << p.m_member << endl;
}
ppp(T name)
{
m_member = name;
}
private:
T m_member;
};
类内实现是比较简单的,无需考虑太多东西。但是类外实现就完全不一样了。
错误示范1
template<class T>
class ppp
{
friend void aaa(ppp<T> p);
public:
ppp(T name)
{
m_member = name;
}
private:
T m_member;
};
void aaa(ppp<T> p)
{
cout << p.m_member << endl;
}
错误示范2
template<class T>
void aaa(ppp<T> p)
{
cout << p.m_member << endl;
}
之前在函数模板与普通函数的调用规则那提到过,优先调用普通函数,在这里由于aaa是函数模板而不能和类模板处的普通友元函数成功链接。为了解决这个问题,我们可以强制友元函数调用函数模板。
friend void aaa<>(ppp<T> p);
但是这样仍然不可以解决我们的问题。
错误示范3
template<class T>
void aaa(ppp<T> p)
{
cout << p.m_member << endl;
}
template<class T>
class ppp
{
friend void aaa<>(ppp<T> p);
public:
T m_member;
ppp(T name)
{
m_member = name;
}
};
由于aaa变成了函数模板,所以它必须在友元之前就被声明或者实现。但是由于形参为ppp类,编译器自上而下无法检索到ppp类的存在,依然会报错。
正确写法
template<class T>
class ppp;
template<class T>
void aaa(ppp<T> p)
{
cout << p.m_member << endl;
}
template<class T>
class ppp
{
friend void aaa<>(ppp<T> p);
public:
T m_member;
ppp(T name)
{
m_member = name;
}
};
3. 类模板案例
自定义一个类模板,具有以下功能:
- 可以对内置数据类型以及自定义数据类型进行存储;
- 数组中的数据存储到堆区;
- 构造函数中传入数组的数量;
- 修改对应的拷贝构造函数以及重载=运算符;
- 提供尾插法和尾删法对数据增删;
- 通过下标的方式访问数组元素;
- 可以获取当前数组元素个数和容量。
现在分析:
- 对于要求二,自然是要new一个数据在堆区,这个数据应该用一个指针接收。
- 由于存储的是一组数据,自然需要new出一个数组。
- 那么这个类模板的成员包括T类型的指针,int类型的元素个和容量。
以下是类模板的结构:
template<class T>
class myvector
{
public:
myvector(unsigned int capacity);
myvector(const myvector<T> &myvec);
void mypush_back(const T &t);
void mypull_back();
T *begin();
T *end();
T &operator[](unsigned int j);
myvector<T> &operator=(const myvector<T> &myvec);
unsigned int getcapacity();
unsigned int getsize();
~myvector();
private:
unsigned int m_size;
unsigned int m_capacity;
T *m_type;
};
3.1 构造函数
template<class T>
myvector<T>::myvector(unsigned int capacity)
{
this->m_capacity = capacity;
this->m_size = 0;
this->m_type = new T[capacity];
}
3.2 拷贝构造函数
这里要使用 深拷贝 防止浅拷贝带来的析构重复删除指针的情况。
template<class T>
myvector<T>::myvector(const myvector<T> &myvec)
{
this->m_capacity = myvec.m_capacity;
this->m_size = myvec.m_size;
m_type = new T[m_capacity];
for (int i = 0; i != this->m_size; ++i)
{
this->m_type[i] = myvec.m_type[i];
}
}
3.3 operator+函数重载
由于类自动提供一套重载的operator+函数,而且使用的是浅拷贝,因此我们有必要重写一下。
template<class T>
myvector<T> &myvector<T>::operator=(const myvector<T> &myvec)
{
if (!this->m_type == NULL)
{
delete[] this->m_type;
this->m_type = NULL;
}
this->m_capacity = myvec.m_capacity;
this->m_size = myvec.m_size;
this->m_type = new T[this->m_capacity];
for (int i = 0; i != this->m_size; ++i)
{
this->m_type[i] = myvec.m_type[i];
}
return *this;
}
3.4 析构函数
这里清空指针。
template<class T>
myvector<T>::~myvector()
{
if (!this->m_type == NULL)
{
delete[] this->m_type;
this->m_type = NULL;
}
}
3.5 通过下标访问数组元素
为了能够利用[]像数组一样访问数据,有必要重写一下operator[]函数。
template<class T>
T &myvector<T>::operator[](unsigned int j)
{
return this->m_type[j];
}
3.6 尾插和尾删
尾插:当指针非空且元素数量小于当前容量的情况下我们在容器的后面插入一个对象。否则就直接退出程序。
template<class T>
void myvector<T>::mypush_back(const T &t)
{
if (this->m_type != NULL && this->m_size < this->m_capacity)
{
this->m_type[this->m_address] = t;
++this->m_size;
}
else
{
return;
}
}
尾删:做了一个逻辑删除,让用户访问不到这个对象。
template<class T>
void myvector<T>::mypull_back()
{
if (this->m_address == 0) return;
--this->m_address;
}
3.7 begin和end函数
突然想用范围for循环遍历容器的元素,就简单写了这两个函数。
template<class T>
T *myvector<T>::begin()
{
return this->m_type;
}
template<class T>
T *myvector<T>::end()
{
return (this->m_type + this->m_size);
}
4. 出现的一些小问题
- 不能将“this”指针从“const myvector”转换为“myvector &”
这个问题是由于在一开始写拷贝构造函数的时候,调用了getcapacity()函数,而且拷贝构造函数的形参写的是const myvector &myvec,而getcapacity()的类型是个普通的成员函数。这就导致了 常量对象调用了非常量的成员函数 。要想解决这个问题,要么 不在拷贝构造函数中使用getcapacity()函数 ,要么 把拷贝构造函数的形参的const 前缀去除 ,要么 给getcapacity()函数价格后缀const 。
unsigned int getcapacity() const;
- “Person”: 没有合适的默认构造函数可用
我自定义了一个Person类希望用myvector存储。
class Person
{
public:
string m_name;
string m_id;
Person(string name, string id)
{
this->m_name = name;
this->m_id = id;
}
};
myvector<Person> arr(100);
但是这样的语句会报错,这是由于Person类中有有参构造函数,于是编译器不会给他设置一个默认构造函数了。但是myvector语句是要调用Person的无参构造函数的,因此而报错。 解决办法就是写一个空实现的无参构造函数即可。
|