文章目录
1 类模板语法
作用:建立一个通用类,定义类不具体指定类成员的数据类型
,使用虚拟类型表示;创建对象时再确定类成员的具体类型。
语法 :
template<typename T> 或 template<class 类型参数1, class 类型参数2 ..>
类
解释:
template
:关键字,声明创建模板。
typename
:关键字,类型名称,表明其后符号是通用数据类型,可使用class
代替。
T
:通用数据类型/虚拟类型(通常为大写字母),标识符名称可替换。
调用方式:类名<模板参数列表> 对象名(实参列表);
注:模板参数列表向类模板的类型参数传值;实参列表向有参构造函数的形参传值。
示例:
#include <iostream>
#include <string>
using namespace std;
//定义类模板
template<class Type1, class Type2>
class Person {
public:
Type1 name;
Type2 age;
Person(Type1 name, Type2 age) {
this->name = name;
this->age = age;
}
void printInfo() {
cout << "姓名:" << this->name << " 年龄:" << this->age << endl;
}
};
int main() {
//调用类模板:类名<模板参数列表> 对象名(实参列表);
//模板参数列表:向类模板的类型参数传值
//实参列表:向有参构造函数的形参传值
Person<string, int> p("Tom", 18);
p.printInfo();
return 0;
}
2 类模板与函数模板区别
区别:
(1)类模板没有自动类型推导的方式,必须显式指定类型,否则编译器报错:缺少类模板“XXX”的参数列表
。
(2)类模板的模板参数列表中,可包含默认参数,调用类模板时可省略默认参数。
示例:
#include <iostream>
#include <string>
using namespace std;
//定义类模板
//2.类模板的模板参数列表中,可存在默认参数
template<class Type1, class Type2 = int> //Type2的默认参数类型为int
class Person {
public:
Type1 name;
Type2 age;
Person(Type1 name, Type2 age) {
this->name = name;
this->age = age;
}
void printInfo() {
cout << "姓名:" << this->name << " 年龄:" << this->age << endl;
}
};
int main() {
//调用类模板:类名<模板参数列表> 对象名(实参列表);
/* 1.类模板没有自动类型推导的方式 */
Person<string, int> p1("Tom", 18); //正确,必须显式指定类型
//Person p1("Tom", 18); //错误:缺少类模板“Person”的参数列表
p1.printInfo(); //姓名:Tom 年龄:18
/* 2.类模板的模板参数列表中,可存在默认参数,调用时可省略默认参数 */
Person<string> p2("Jerry", 20);
p2.printInfo(); //姓名:Jerry 年龄:20
return 0;
}
3 类模板中成员函数的创建时机
普通类的成员函数,在一开始时创建;
类模板的成员函数,在调用时创建。
示例:类模板中成员函数的创建时机
#include <iostream>
using namespace std;
class Object1 {
public:
void print1() {
cout << "Object1类的成员函数print1()" << endl;
}
};
class Object2 {
public:
void print2() {
cout << "Object1类的成员函数print2()" << endl;
}
};
//类模板
template<class T>
class Test {
public:
T obj; //通用类型T的成员属性
/* 类模板的成员函数,在调用时创建 */
//obj类型未确定,若类模板的成员函数一开始即创建,则调用其它类的成员函数时报错
void func1() {
obj.print1();
}
void func2() {
obj.print2();
}
};
int main() {
//显式指定通用类型T为Object1类型,类模板的成员属性obj只能调用Object1类的成员
Test<Object1> test;
/* 类模板的成员函数,在调用时创建 */
//编译前正常,编译时正常
test.func1(); //等价于obj.print1(); obj为Object1类型
//编译前正常,编译时出错:print2不是Object1类的成员
//test.func2(); //等价于obj.print2(); obj为Object1类型
return 0;
}
4 类模板对象作函数参数
使用实例化的类模板对象,向函数形参传参的3种方式:
(1)函数形参显式指定传入的数据类型:函数形参直接指定对象的具体数据类型。【最常用】
例:void func(Object<string, int> &obj){...}
(2)函数形参的对象参数模板化:将作为函数形参的对象参数的类型进行模板化。【函数模板+类模板】
例:template<typename T1, typename T2>
void func(Object<T1, T2> &obj){...}
(3)函数形参的整个类/对象模板化:将类模板对象所属的整个类进行模板化,作为通用数据类型。【函数模板+类模板】
例:template<typename T>
void func(T &obj){...}
注:查看编译器推导出的通用参数
T
的类型:typeid(T).name()
。
示例:
#include <iostream>
#include <string>
using namespace std;
/* 类模板 */
template<class Type1, class Type2>
class Person {
public:
Type1 name;
Type2 age;
Person(Type1 name, Type2 age) {
this->name = name;
this->age = age;
}
void printInfo() {
cout << "姓名:" << this->name << " 年龄:" << this->age << endl;
}
};
/* 类模板对象作为函数形参 */
//1.显式指定传入的数据类型
void func1(Person<string, int> &obj) {
obj.printInfo();
}
//2.函数形参的对象参数模板化【函数模板+类模板】
template<typename T1, typename T2>
void func2(Person<T1, T2> &obj) {
obj.printInfo();
/* 查看编译器推导出的通用参数`T`的类型 */
cout << "T1:" << typeid(T1).name() << endl;
cout << "T2:" << typeid(T2).name() << endl;
}
//3.函数形参的整个类/对象模板化【函数模板+类模板】
template<typename T>
void func3(T &obj) {
obj.printInfo();
/* 查看编译器推导出的通用参数`T`的类型 */
cout << "T:" << typeid(T).name() << endl;
}
int main() {
//1.显式指定传入的数据类型
Person<string, int> p1("Tom", 18);
func1(p1); //姓名:Tom 年龄:18
//2.函数形参的对象参数模板化【函数模板+类模板】
Person<string, int> p2("Jerry", 20);
func2(p2); //姓名:Jerry 年龄:20
//T1:class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
//T2:int
//3.函数形参的整个类/对象模板化【函数模板+类模板】
Person<string, int> p3("Lucy", 22);
func3(p3); //姓名:Lucy 年龄:22
//T:class Person<class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >, int>
return 0;
}
5 类模板与继承
类模板继承时的注意事项:
(1)当子类继承的父类是类模板时,声明子类时,需明确指定父类中通用数据类型T
的具体类型;若未指定,则编译器无法为子类分配内存(即子类无法继承父类,编译器无法确定子类对象所占内存空间)。
//父类为类模板
template<class T>
class Father{
T field;
};
//子类继承父类:明确指定父类中的泛型类型
class Son : public Father<int>{
...};
(2)若需要灵活指定父类中通用数据类型T
的类型,则子类应使用类模板。
//父类为类模板
template<class T>
class Father{
T field;
};
//子类继承父类:子类使用类模板
template<class T, class E>
class Son : public Father<E>{
T var;
};
示例:继承的父类为类模板
#include <iostream>
using namespace std;
template<class T>
class Father {
public:
T field;
};
//子类继承父类:明确指定父类中的泛型类型
//class Son1 : public Father{} //错误:必须指定父类中的泛型类型,否则无法继承
class Son1 : public Father<int> {
};
//子类继承父类:子类使用类模板
template<class T, class E>
class Son2 : public Father<E> {
public:
T var;
public:
Son2() {
cout << "子类的泛型参数T:" << typeid(T).name() << endl;
cout << "父类的泛型参数E:" << typeid(E).name() << endl;
}
};
int main() {
Son1 s1;
Son2<char, int> s2;
//子类的泛型参数T:char
//父类的泛型参数E:int
return 0;
}
6 类模板成员函数的类外实现
类模板中成员函数在类外实现时:
在类内声明函数;在类外使用类模板声明template
,并在表示作用域的类名后,添加模板参数列表。
//构造函数的类外实现
template<class T1, class T2> //类模板声明
Object<T1, T2>::Object(){
...} //Object<T1, T2> 在类名后添加模板参数列表
//成员函数的类外实现
template<class T1, class T2> //类模板声明
void Object<T1, T2>::func(){
...} //Object<T1, T2> 在类名后添加模板参数列表
示例:类模板成员函数的类外实现
#include<iostream>
using namespace std;
template<class T1, class T2>
class Object {
public:
T1 field;
T2 var;
public:
//构造函数的类内声明
Object(T1 field, T2 var);
//成员函数的类内声明
void func();
};
//构造函数的类外实现
template<class T1, class T2> //类模板声明
Object<T1, T2>::Object(T1 field, T2 var) {
//Object<T1, T2> 在类名后添加模板参数列表
this->field = field;
this->var = var;
}
//成员函数的类外实现
template<class T1, class T2> //类模板声明
void Object<T1, T2>::func() {
//Object<T1, T2> 在类名后添加模板参数列表
cout << "T1类型:" << typeid(T1).name() << "\t field = " << this->field << endl;
cout << "T2类型:" << typeid(T2).name() << "\t var = " << this->var << endl;
}
int main() {
Object<int, char> obj(18, 'a');
obj.func();
//T1类型:int field = 18
//T2类型:char var = a
return 0;
}
7 类模板的分文件编写
问题:类模板中成员函数的创建时机在调用阶段,导致分文件编写时无法链接,编译器报错:无法解析的外部命令
。
解决方案:
(1)将包含头文件修改为直接包含.cpp
源文件,即将#include "xxx.h"
修改为#include "xxx.cpp"
。【不建议】
(2)将.h
头文件和.cpp
源文件的内容合并至同一个文件中,即函数的声明与实现写至同一个文件中,并更改后缀名为.hpp
。【建议:类模板使用.hpp
文件】
注1:
.hpp
是类模板约定俗成的文件后缀名,并非强制。
注2:主流解决方案是第2种:将类模板成员函数的声明与实现合并在同一文件,并将后缀名改为.hpp
。
示例:
解决方案1:将包含头文件修改为直接包含.cpp
源文件
object.h
#pragma once
#include<iostream>
using namespace std;
template<class T1, class T2>
class Object {
public:
T1 field;
T2 var;
public:
//构造函数的类内声明
Object(T1 field, T2 var);
//成员函数的类内声明
void func();
};
object.cpp
#pragma once
#include "object.h"
//构造函数的类外实现
template<class T1, class T2>
Object<T1, T2>::Object(T1 field, T2 var) {
this->field = field;
this->var = var;
}
//成员函数的类外实现
template<class T1, class T2>
void Object<T1, T2>::func() {
cout << "T1类型:" << typeid(T1).name() << "\t field = " << this->field << endl;
cout << "T2类型:" << typeid(T2).name() << "\t var = " << this->var << endl;
}
测试程序main.cpp
#include<iostream>
using namespace std;
#include "object.cpp" //直接包含源文件
int main() {
Object<int, char> obj(18, 'a');
obj.func();
//T1类型:int field = 18
//T2类型:char var = a
return 0;
}
解决方案2:.h
头文件和.cpp
源文件的内容合并至同一个.hpp
文件
object.hpp
#pragma once
#include<iostream>
using namespace std;
template<class T1, class T2>
class Object {
public:
T1 field;
T2 var;
public:
//构造函数的类内声明
Object(T1 field, T2 var);
//成员函数的类内声明
void func();
};
//构造函数的类外实现
template<class T1, class T2>
Object<T1, T2>::Object(T1 field, T2 var) {
this->field = field;
this->var = var;
}
//成员函数的类外实现
template<class T1, class T2>
void Object<T1, T2>::func() {
cout << "T1类型:" << typeid(T1).name() << "\t field = " << this->field << endl;
cout << "T2类型:" << typeid(T2).name() << "\t var = " << this->var << endl;
}
测试程序main.cpp
#include<iostream>
using namespace std;
#include "object.hpp" //包含.hpp头文件
int main() {
Object<int, char> obj(18, 'a');
obj.func();
//T1类型:int field = 18
//T2类型:char var = a
return 0;
}
8 全局函数作为类模板的友元
需求:全局函数作为类模板的友元;全局函数的形参为类模板对象。
注:全局函数作为类模板的友元时,建议全局函数在类模板的内部实现,用法简单,且编译器可直接识别。
实现方式:
(1)全局函数类内实现(在类模板的内部声明并实现,并作为友元)【建议】
步骤:全局函数直接在类模板内声明并实现,并声明友元。
template<class T>
class Object{
//全局函数作友元,在类模板内声明并实现
friend void g_func(Object<T> &obj){
...
}
private:
T field;
};
(2)全局函数的类外实现(在类模板的内部声明、外部实现,并作为友元)【不建议】
步骤:类模板的声明早于
全局函数(函数模板)的定义,早于
类模板的定义。
①全局函数的对象形参包含泛型参数,全局函数实际为函数模板,在类模板内声明友元时,需在全局函数的函数模板名后,添加空模板参数列表<>
(区分函数模板与普通函数)。
例:friend void g_func<>(Object<T> &obj);
注:全局函数的形参包含泛型参数,全局函数的类外定义,实际为函数模板的定义。
②若全局函数在类外实现,使编译器提前知晓全局函数(函数模板)的存在,即全局函数的定义应早于类模板的定义。
/* 全局函数的定义 在 类模板的定义 之前 */
//全局函数定义
template<class T>
void g_func(Object<T> &obj){
...
}
//类模板定义
template<class T>
class Object{
//全局函数(函数模板)作友元,在类模板内声明,并添加空模板参数列表<>
friend void g_func<>(Object<T> &obj);
...
private:
T field;
};
③全局函数的形参包含类模板对象,则类模板的声明应早于全局函数的定义。
/* 类模板的声明 在 全局函数的定义 之前 */
//类模板声明
template<class T>
class Object;
/* 全局函数的定义 在 类模板的定义 之前 */
//全局函数定义
template<class T>
void g_func(Object<T> &obj){
...
}
//类模板定义
template<class T>
class Object{
//全局函数作友元,在类模板内声明,并添加空模板参数列表<>
friend void g_func<>(Object<T> &obj);
...
private:
T field;
};
示例:全局函数作为类模板的友元
#include<iostream>
using namespace std;
#include<string>
/* 类模板的声明 在 全局函数的定义 之前 */
//类模板声明
template<class T>
class Object;
/* 全局函数的定义 在 类模板的定义 之前 */
//全局函数定义
template<class T>
void g_func2(Object<T> &obj) {
cout << "全局函数g_func2()在类模板的外部实现" << endl;
cout << "field = " << obj.field << endl;
cout << "泛型类型T:" << typeid(T).name() << endl;
}
//类模板定义
template<class T>
class Object {
//全局函数g_func1()作友元,在类模板内声明并实现
friend void g_func1(Object<T> &obj) {
cout << "全局函数g_func1()在类模板的内部实现" << endl;
cout << "field = " << obj.field << endl;
cout << "泛型类型T:" << typeid(T).name() << endl;
}
//全局函数g_func2()作友元,在类模板内声明,并添加空模板参数列表<>
friend void g_func2<>(Object<T> &obj); //<> 区分函数模板与普通函数
public:
//带参构造函数
Object(T t) {
this->field = t;
}
private:
//私有成员属性
T field;
};
int main() {
/* 调用类模板【内部】实现的友元全局函数 */
//创建类模板对象
Object<char> obj1('x');
g_func1(obj1);
//全局函数g_func1()在类模板的内部实现
//field = x
//泛型类型T:char
cout << "-----------------------------------" << endl;
/* 调用类模板【外部】实现的友元全局函数 */
//创建类模板对象
Object<int> obj2(10);
g_func2(obj2);
//全局函数g_func2()在类模板的外部实现
//field = 10
//泛型类型T:int
return 0;
}
9 类模板练习:类模板实现通用的数组类
练习:基于类模板,模拟一个通用的数组类:
(1)支持存储内置数据类型及自定义数据类型的数据;
(2)提供私有属性:元素个数和数组容量;
(3)数组中的数据存储至堆区;
(4)带参构造函数中可传入数组容量;
(5)提供自定义拷贝构造函数及重载operator=运算符
,避免堆区数据的浅拷贝问题(析构时重复释放内存);
(6)提供自定义析构函数,释放堆区内存;
(7)对外接口:通过尾插法/尾删法增加/删除数组元素;
(8)可通过索引访问数组元素,即重载[]
。
示例:类模板实现通用的数组类
MyArray.hpp
/* 基于类模板实现通用的数组类 */
#pragma once //防止头文件重复包含
#include <iostream>
using namespace std;
template<class T>
class MyArray {
private:
T* pArr; //指针指向堆区开辟的数组
int capacity; //数组容量
int size; //数组大小
public:
//有参构造函数:参数-容量
MyArray(int cap) {
//cout << "MyArray类的有参构造函数..." << endl;
//初始化堆区数组
this->capacity = cap; //数组容量
this->size = 0; //元素个数0
this->pArr = new T[this->capacity]; //根据数组容量开辟堆区空间
}
//拷贝构造函数
MyArray(const MyArray& arr) {
//cout << "MyArray类的拷贝构造函数..." << endl;
/* 编译器默认的浅拷贝 */
//this->capacity = arr.capacity;
//this->size = arr.size;
//this->pArr = arr.pArr; //堆区内存地址赋值,导致浅拷贝
/* 自定义拷贝构造,深拷贝 */
this->capacity = arr.capacity;
this->size = arr.size;
//深拷贝:重新申请堆区内存
this->pArr = new T[arr.capacity];
//拷贝原数组的所有元素
for (int i = 0; i < this->size; i++) {
this->pArr[i] = arr.pArr[i];
}
}
//重载operator=运算符,防止浅拷贝;支持连续赋值,返回当前对象
MyArray& operator=(const MyArray& arr) {
//cout << "MyArray类的重载运算符operator=..." << endl;
//先判断原先堆区是否存在数据:若有则先释放
if (this->pArr != NULL) {
delete[] this->pArr;
this->pArr = NULL; //防止野指针
this->capacity = 0;
this->size = 0;
}
this->capacity = arr.capacity;
this->size = arr.size;
//深拷贝:重新申请堆区内存
this->pArr = new T[arr.capacity];
//拷贝原数组的所有元素
for (int i = 0; i < this->size; i++) {
this->pArr[i] = arr.pArr[i];
}
return *this; //返回当前对象本身
}
//尾插法:插入数据
void Push_Back(const T& val) {
//先判断是否还有剩余容量
if (this->size == this->capacity) {
cout << "数组容量已满" << endl;
return;
}
//向数组的尾部插入元素
this->pArr[this->size] = val;
//更新数组大小
this->size++;
}
//尾删法:逻辑上的删除,无法访问最后一个数据
void Pop_Back() {
//先判断当前数组大小是否为0
if (this->size == 0) {
cout << "数组大小已为0" << endl;
return;
}
//更新数组大小,无法访问最后一个数据
this->size--;
}
//通过索引访问元素,重载[];且作为左值存在
T& operator[](int index) {
//作为左值->返回引用类型
//判断索引是否越界
if (index < this->size) {
return this->pArr[index];
}
//else {
// cout << "数组越界.." << endl;
// return;
//}
}
//返回数组的容量
int getCapacity(){
return this->capacity;
}
//返回数组的大小
int getSize(){
return this->size;
}
//析构函数
~MyArray() {
//cout << "MyArray类的析构函数..." << endl;
//释放堆区内存
if (this->pArr != NULL) {
delete[] this->pArr;
this->pArr = NULL; //防止野指针
}
}
};
测试代码
#include <iostream>
using namespace std;
#include <string>
#include "MyArray.hpp"
//自定义数据类型
class Person {
public:
string name;
int age;
Person() {
}
Person(string name, int age) {
this->name = name;
this->age = age;
}
};
//打印通用数组的元素(函数模板)
template<typename T>
void printArray(MyArray<T> &arr){
for (int i = 0; i < arr.getSize(); i++) {
//cout << arr.pArr[i] << " ";
//已重载[]
cout << arr[i] << " ";
}
cout << endl;
}
void printPersonArray(MyArray<Person> &arr) {
for (int i = 0; i < arr.getSize(); i++) {
//已重载[]
cout << "姓名:" << arr[i].name << ",年龄:" << arr[i].age << endl;
}
}
//测试有参构造函数、拷贝构造函数、=
void func1() {
//显示指定类型
MyArray<int> arr1(5);
MyArray<int> arr2(arr1);
MyArray<int> arr3(10);
arr3 = arr1;
}
//测试内置数据类型的尾插法、尾删法
void func2() {
MyArray<int> arr(5);
/* 测试尾插法 */
//数组赋值——“尾插法”向空数组尾部插入数据
for (int i = 0; i < 5; i++) {
arr.Push_Back(i);
}
//打印数组
printArray<int>(arr); //0 1 2 3 4
cout << "数组容量:" << arr.getCapacity() << endl; //5
cout << "数组大小:" << arr.getSize() << endl; //5
/* 测试尾删法 */
arr.Pop_Back();
//打印数组
printArray<int>(arr); //0 1 2 3
cout << "数组容量:" << arr.getCapacity() << endl; //5
cout << "数组大小:" << arr.getSize() << endl; //4
}
//测试自定义数据类型
void func3() {
MyArray<Person> arr(5);
Person p1("Tom", 1);
Person p2("Jerry", 2);
Person p3("Lucy", 3);
//尾插法插入至数组
arr.Push_Back(p1);
arr.Push_Back(p2);
arr.Push_Back(p3);
//打印数组
printPersonArray(arr);
//姓名:Tom,年龄:1
//姓名:Jerry,年龄:2
//姓名:Lucy,年龄:3
cout << "数组容量:" << arr.getCapacity() << endl; //5
cout << "数组大小:" << arr.getSize() << endl; //3
}
int main() {
//func1();
//func2();
func3();
return 0;
}