C++泛型编程(二):类模板


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;
}

猜你喜欢

转载自blog.csdn.net/newson92/article/details/113856080