C++ 类模板 详解

半腔热情 提交于 2020-02-21 18:49:32

什么是类模板?

类模板和函数模板总体上差不多,都是进行虚拟替换!
https://blog.csdn.net/cpp_learner/article/details/104390433


为什么要使用类模板?

类模板能够为类的数据成员、成员函数的参数、返回值提供动态参数化的机制,即可以构造不同数据类型的实例。


类模板的定义

类模板由模板说明和类说明构成
模板说明同函数模板,如下:
template <类型形式参数表>
类声明

例如:

template  <typename T>
class Father {
public:
	Father(T name = "无名");
	//Father 的成员函数

private :
	T name;
};

单个类模板的使用

其实就是说明了模板,然后在对类的成员类型使用模板的参数替换!

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;

// 模板说明
template <typename T>
class test {
public:
	// 构造函数的参数列表使用虚拟类型
	test(T k = 10) { this->k = k; }

	// 成员函数返回值使用虚拟类型
	T getK() const { return k; }

private:
	// 数据类型使用虚拟类型
	T k;
};


// 使用类模板,函数参数必须显示指定类型
void print(test<int>& test) {
	cout << test.getK() << endl;
}

int main(void) {
	// 1.模板类定义类对象,必须显示指定类型
	// 2.模板中如果使用了构造函数, 则遵守以前的类的构造函数的调用规则
	test<int> k(100);
	cout << k.getK() << endl;

	print(k);

	system("pause");
	return 0;
}

注意事项:

  1. 模板类定义类对象,必须显示指定类型
  2. 模板中如果使用了构造函数, 则遵守以前的类的构造函数的调用规则
  3. 使用类模板,函数参数必须显示指定类型

https://blog.csdn.net/cpp_learner/article/details/103335482

运行截图:

在这里插入图片描述


继承和派生中类模板的使用

https://blog.csdn.net/cpp_learner/article/details/104076682

一共有三种情况:

  1. 父类是普通类,子类是模板类;
  2. 父类是模板类,子类是普通类;
  3. 父类和子类都是模板类。

第一种情况:父类是普通类,子类是模板类

和普通继承的用法一模一样,子类的模板类型可以给继承于父类的数据成员使用

例:

  1. 定义一个普通类Father,有私有成员 char *name; int age;
  2. 定义一个模板类Son, 有私有成员 int weight;
  3. 定义对象Father 和 Son ,并输出里面的值。

代码:

#include <iostream>
#include <Windows.h>

using namespace std;

// 第一种情况:父类是普通类,子类是模板类:
// 和普通继承的用法一模一样,子类的模板类型可以给继承于父类的数据成员使用
class Father {
public:
	Father(const char* name=NULL, int age=0) {
		if (!name) {
			name = "无名";
		}

		this->name = new char[strlen(name) + 1];
		strcpy_s(this->name, strlen(name) + 1, name);

		this->age = age;
	}

	~Father() {
		if (name) {
			delete name;
		}
	}

	char* getName() const { return name; }
	int getAge() const { return age; }

private:
	char* name;
	int age;
};

template <typename T>
class Son : public Father {
public:
						// 子类的模板类型可以给继承与父类的数据成员使用
	Son(const char* name = NULL, T age = 0, T weight = 0) : Father(name, age) {
		this->weigth = weight;
	}

	~Son() {

	}

	T getWeigth() const { return weigth; }

private:
	T weigth;	// 体重
};





int main(void) {
	Father father("父亲", 38);
	// 定义儿子对象时,需要指定类型
	Son<int> son("儿子", 18, 55);
	
	cout << "姓名:" << father.getName() << ", 年龄:"
	<< father.getAge() << endl;
	cout << "姓名:" << son.getName() << ", 年龄:" 
	<< son.getAge() << ", 体重:" << son.getWeigth() << endl;

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述

和普通继承的用法一模一样,子类的模板类型可以给继承于父类的数据成员使用

定义模板类对象时,需要指定类型:
例如代码中的Son:

// 定义儿子对象时,需要指定类型
Son<int> son("儿子", 18, 55);

子类的模板类型可以给继承于父类的数据成员使用:

Son(const char* name = NULL, T age = 0, T weight = 0) : Father(name, age) {
	this->weigth = weight;
}

第二种情况:父类是模板类,子类是普通类

继承时必须在子类里实例化父类的类型参数,子类无法使用父类的模板类型

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;

// 第二种情况:父类是模板类,子类是普通类:
// 继承时必须在子类里实例化父类的类型参数,子类无法使用父类的模板类型
template <typename T, typename Y>
class Father {
public:
	Father(const T* name = NULL, Y age = 0) {
		if (!name) {
			name = "无名";
		}

		this->name = new char[strlen(name) + 1];
		strcpy_s(this->name, strlen(name) + 1, name);

		this->age = age;
	}

	~Father() {
		if (name) {
			delete name;
		}
	}

	T* getName() const { return name; }
	Y getAge() const { return age; }

private:
	T* name;
	Y age;
};

// 继承时必须在子类里实例化父类的类型参数
class Son : public Father<char, int> {
public:
	// 子类中, 不能使用T,Y表示类型		// 这里显示类型可写可不写,因为父类的构造函数赋了默认值
	Son(const char* name = NULL, int age = 0, int weight = 0) : Father<char, int>(name, age) {
		this->weigth = weight;
	}

	~Son() {}

	int getWeigth() const { return weigth; }

private:
	int weigth;	// 体重
};



int main(void) {
	// 定义父亲对象时,需要指定类型
	Father<char, int> father("父亲", 40);
	Son son("儿子", 20, 60);

	cout << "姓名:" << father.getName() << ", 年龄:"
		<< father.getAge() << endl;
	cout << "姓名:" << son.getName() << ", 年龄:"
		<< son.getAge() << ", 体重:" << son.getWeigth() << endl;

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述

继承时必须在子类里实例化父类的类型参数,子类无法使用父类的模板类型

定义模板类对象时,需要指定类型!
例如代码中的Father:

// 定义父亲对象时,需要指定类型
Father<char, int> father("父亲", 40);

继承时必须在子类里实例化父类的类型参数:

// 继承时必须在子类里实例化父类的类型参数
class Son : public Father<char, int> {};

第三种情况,父类和子类都是模板类时

继承时必须在子类里实例化父类的类型参数,子类的虚拟类型可以传递到父类中使用

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;


// 第三种情况,父类和子类都是模板类时
// 继承时必须在子类里实例化父类的类型参数,子类的虚拟类型可以传递到父类中使用
template <typename T, typename Y>
class Father {
public:
	Father(const T* name = NULL, Y age = 0) {
		if (!name) {
			name = "无名";
		}

		this->name = new char[strlen(name) + 1];
		strcpy_s(this->name, strlen(name) + 1, name);

		this->age = age;
	}

	~Father() {
		if (name) {
			delete name;
		}
	}

	T* getName() const { return name; }
	Y getAge() const { return age; }

private:
	T* name;
	Y age;
};

// 继承时必须在子类里实例化父类的类型参数
template <typename Z>
class Son : public Father<char, int> {
public:
	// 子类中, 不可以使用父类的T,Y表示类型			// 这里显示类型可写可不写,
	// 子类的模板类型可以给继承与父类的数据成员使用		// 因为父类的构造函数赋了默认值
	Son(const char* name = NULL, Z age = 0, Z weight = 0) : Father<char, int>(name, age) {
		this->weigth = weight;
	}

	~Son() {}

	Z getWeigth() const { return weigth; }

private:
	Z weigth;	// 体重
};



int main(void) {

	// 定义父亲对象时,需要指定类型
	Father<char, int> father("父亲", 42);
	// 定义儿子对象时,需要指定类型
	Son<int> son("儿子", 22, 62);

	cout << "姓名:" << father.getName() << ", 年龄:"
		<< father.getAge() << endl;
	cout << "姓名:" << son.getName() << ", 年龄:"
		<< son.getAge() << ", 体重:" << son.getWeigth() << endl;

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述

继承时必须在子类里实例化父类的类型参数,子类的虚拟类型可以传递到父类中使用

定义模板类对象时,需要指定类型!
例如代码中的Father 和 Son:

// 定义父亲对象时,需要指定类型
Father<char, int> father("父亲", 42);
// 定义儿子对象时,需要指定类型
Son<int> son("儿子", 22, 62);

继承时必须在子类里实例化父类的类型参数:

// 继承时必须在子类里实例化父类的类型参数
template <typename Z>
class Son : public Father<char, int> {};

子类的虚拟类型可以传递到父类中使用:

// 子类中, 不可以使用父类的T,Y表示类型				// 这里显示类型可写可不写,
// 子类的模板类型可以给继承与父类的数据成员使用		// 因为父类的构造函数赋了默认值
Son(const char* name = NULL, Z age = 0, Z weight = 0) : Father<char, int>(name, age) {
	this->weigth = weight;
}

结论: 子类从模板类继承的时候,需要让编译器知道 父类的数据类型具体是什么

  1. 父类一般类,子类是模板类, 和普通继承的玩法类似
  2. 子类是一般类,父类是模板类,继承时必须在子类里实例化父类的类型参数
  3. 父类和子类都时模板类时,子类的虚拟的类型可以传递到父类中

类模板函数的三种表达描述方式

  1. 所有的类模板函数写在类的内部;
    (上面 继承和派生中类模板的使用 中就是这种用法)
  2. 所有的类模板函数写在类的外部,在一个cpp中
  3. 所有的类模板函数写在类的外部,在不同的.h和.cpp中

第二种:所有的类模板函数写在类的外部,在一个cpp中

需求:

  1. 定义一个Father类(使用模板类),有私有成员float weigth; int age;;
  2. 重载加号运算符和赋值运算符;
  3. 定义对象:
	Father<float, int> f1(50, 30);
    Father<float, int> f2(60, 32);
    Father<float, int> f3(0, 0);
  1. 计算:f3 = f1 + f2;;
  2. 输出f3.

根据需求,我们得先说明类模板:template <typename T, typename Y>;
然后实现类,在类的外部再实现函数方法,但是每个函数前都得再说明一次类模板;
返回值类型、类声明也必须加上<T, Y>,且类型说明也必须使用T,Y替换。

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;

// 所有的类模板函数写在类的外部实现,在一个cpp中
template <typename T, typename Y>
class Father {
public:
	Father(T weigth = 0, Y age = 0);

	T getWeigth() const;
	Y getAge() const;

	Father& operator+(const Father& father);
	Father& operator=(const Father& father);

private:
	T weigth;
	Y age;
};

// 必须重新说明类模板
template <typename T, typename Y>
// 类声明也必须加上<T, Y>,且类型说明也必须使用T,Y替换
Father<T, Y>::Father(T weigth, Y age) {
	this->weigth = weigth;
	this->age = age;
}



// 必须重新说明类模板
template <typename T, typename Y>
// 返回值类型也必须使用类模板中的参数替换
T Father<T, Y>::getWeigth() const {
	return weigth;
}

template <typename T, typename Y>
Y Father<T, Y>::getAge() const {
	return age;
}


template<typename T, typename Y>
// 类的返回值也必须使用类模板中的参数替换
Father<T, Y>& Father<T, Y>::operator+(const Father& father)
{
	static Father f;	// 可写可不写类模板中的参数:Father<T, Y> f;

	f.weigth = this->weigth + father.weigth;
	f.age = this->age + father.age;

	return f;
}

template<typename T, typename Y>
// 赋值运算符重载
Father<T, Y>& Father<T, Y>::operator=(const Father& father) {
	this->weigth = father.weigth;
	this->age = father.age;

	return *this;
}![在这里插入图片描述](https://img-blog.csdnimg.cn/2020022113384279.png)

int main(void) {
	Father<float, int> f1(50, 30);
	Father<float, int> f2(60, 32);

	Father<float, int> f3(0, 0);
	
	f3 = f1 + f2;

	cout << "f3的体重为:" << f3.getWeigth() << ", 年龄为:" << f3.getAge() << endl;

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述

总结:
在同一个cpp 文件中把模板类的成员函数放到类的外部,需要注意以下几点:

  1. 函数前声明 template <类型形式参数表>
  2. 类的成员函数前的类限定域说明必须要带上虚拟参数列表
  3. 返回的变量是模板类的对象时必须带上虚拟参数列表
  4. 成员函数参数中出现模板类的对象时必须带上虚拟参数列表
  5. 成员函数内部没有限定

第三种:所有的类模板函数写在类的外部,在不同的.h和.cpp中

用法和第二种类似,只是要记得,模板类实现的文件后缀一般用 .hpp ;
且mian函数里面需包含的头文件不是类声明的.h文件,而且类实现的.hpp文件。

代码实例:

Fatehr.h

#pragma once

// 所有的类模板函数分开写,写在不同的文件中
template <typename T, typename Y>
class Father {
public:
	Father(T weigth = 0, Y age = 0);

	T getWeigth() const;
	Y getAge() const;

	Father& operator+(const Father& father);
	Father& operator=(const Father& father);

private:
	T weigth;
	Y age;
};

Father.hpp

#include "Father.h"

// 必须重新说明类模板
template <typename T, typename Y>
// 类声明也必须加上<T, Y>,且类型说明也必须使用T,Y替换
Father<T, Y>::Father(T weigth, Y age) {
	this->weigth = weigth;
	this->age = age;
}



// 必须重新说明类模板
template <typename T, typename Y>
// 返回值类型也必须使用类模板中的参数替换
T Father<T, Y>::getWeigth() const {
	return weigth;
}

template <typename T, typename Y>
Y Father<T, Y>::getAge() const {
	return age;
}


template<typename T, typename Y>
// 类的返回值也必须使用类模板中的参数替换
Father<T, Y>& Father<T, Y>::operator+(const Father& father)
{
	static Father f;	// 可写可不写类模板中的参数:Father<T, Y> f;

	f.weigth = this->weigth + father.weigth;
	f.age = this->age + father.age;

	return f;
}

template<typename T, typename Y>
// 赋值运算符重载
Father<T, Y>& Father<T, Y>::operator=(const Father& father) {
	this->weigth = father.weigth;
	this->age = father.age;

	return *this;
}

mian函数

#include <iostream>
#include <Windows.h>
#include "Father.hpp"

using namespace std;

int main(void) {
	Father<float, int> f1(50, 30);
	Father<float, int> f2(60, 32);

	Father<float, int> f3(0, 0);

	f3 = f1 + f2;

	cout << "f3的体重为:" << f3.getWeigth() << ", 年龄为:" << f3.getAge() << endl;

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述

注意:当类模板的声明(.h文件)和实现(.cpp 或.hpp文件)完全分离,因为类模板的特殊实现,我们应在使用类模板时使用#include 包含 实现部分的.cpp 或.hpp文件。


类模板与友元函数

https://blog.csdn.net/cpp_learner/article/details/104193181

这两者合在一起使用属于特殊情况

需求:
使用友元函数实现两类相加

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;

// 所有的类模板函数写在类的外部实现,在一个cpp中
template <typename T, typename Y>
class Father {
public:
	Father(T weigth = 0, Y age = 0);

	void print() const;

	//Father& operator+(const Father& father);
	Father& operator=(const Father& father);

private:
	T weigth;
	Y age;
	
	// 必须说明类模板
	template <typename T, typename Y>
	friend Father<T, Y> add(const Father<T, Y> &f1, const Father<T, Y> &f2);
};

template <typename T, typename Y>
Father<T, Y>::Father(T weigth, Y age) {
	this->weigth = weigth;
	this->age = age;
}


//template<typename T, typename Y>
//// 类的返回值也必须使用类模板中的参数替换
//Father<T, Y>& Father<T, Y>::operator+(const Father& father)
//{
//	static Father f;	// 可写可不写类模板中的参数:Father<T, Y> f;
//
//	f.weigth = this->weigth + father.weigth;
//	f.age = this->age + father.age;
//
//	return f;
//}


template<typename T, typename Y>
void Father<T, Y>::print() const {
	cout << "体重为:" << this->weigth << ", 年龄为:" << this->age << endl;
}


template<typename T, typename Y>
// 赋值运算符重载
Father<T, Y>& Father<T, Y>::operator=(const Father& father) {
	this->weigth = father.weigth;
	this->age = father.age;

	return *this;
}

// 友元函数实现:两数相加
template<typename T, typename Y>
//返回值类型,类类型,函数体里面定义对象都需要添加对应的类型
Father<T, Y> add(const Father<T, Y>& f1, const Father<T, Y>& f2) {
	Father<T, Y> f;
	f.weigth = f1.weigth + f2.weigth;
	f.age= f1.age + f2.age;
	return f;
}

int main(void) {

	Father<float, int> f1(500, 30);
	Father<float, int> f2(60, 32);

	Father<float, int> f3(0, 0);

	// 调用友元函数时也必须添加类型
	f3 = add<float, int>(f1, f2);

	f3.print();

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述


上面那种情况是 “所有的类模板函数写在类的外部实现,在一个cpp中”,
现在我们把它分成三个文件来实现,
其实都是一样的。

Father.h

#pragma once

// 所有的类模板函数分开写,写在不同的文件中
template <typename T, typename Y>
class Father {
public:
	Father(T weigth = 0, Y age = 0);

	void print() const;

	//Father& operator+(const Father& father);
	Father& operator=(const Father& father);

private:
	T weigth;
	Y age;

	template <typename T, typename Y>
	friend Father<T, Y> add(const Father<T, Y>& f1, const Father<T, Y>& f2);
};

Father.hpp

#include <iostream>
#include "Father.h"

using namespace std;

// 必须重新说明类模板
template <typename T, typename Y>
// 类声明也必须加上<T, Y>,且类型说明也必须使用T,Y替换
Father<T, Y>::Father(T weigth, Y age) {
	this->weigth = weigth;
	this->age = age;
}




template <typename T, typename Y>
void Father<T, Y>::print() const {
	cout << "体重为:" << this->weigth << ", 年龄为:" << this->age << endl;
}


//template<typename T, typename Y>
//// 类的返回值也必须使用类模板中的参数替换
//Father<T, Y>& Father<T, Y>::operator+(const Father& father)
//{
//	static Father f;	// 可写可不写类模板中的参数:Father<T, Y> f;
//
//	f.weigth = this->weigth + father.weigth;
//	f.age = this->age + father.age;
//
//	return f;
//}

template<typename T, typename Y>
// 赋值运算符重载
Father<T, Y>& Father<T, Y>::operator=(const Father& father) {
	this->weigth = father.weigth;
	this->age = father.age;

	return *this;
}


// 友元函数实现:两数相加
template<typename T, typename Y>
Father<T, Y> add(const Father<T, Y>& f1, const Father<T, Y>& f2) {
	Father<T, Y> f;
	f.weigth = f1.weigth + f2.weigth;
	f.age = f1.age + f2.age;
	return f;
}

mian函数

#include <Windows.h>
#include "Father.hpp"

int main(void) {
	Father<float, int> f1(50, 30);
	Father<float, int> f2(60, 32);

	Father<float, int> f3(0, 0);

	//f3 = f1 + f2;
	f3 = add(f1, f2);

	f3.print();

	system("pause");
	return 0;
}

运行截图:

在这里插入图片描述


结论:
(1) 类内部声明友元函数,必须写成一下形式

template<typename T>
friend Father<T> add (Father<T> &a, Father<T> &b);

(2) 友元函数实现 必须写成

template<typename T>
Father<T> add(Father<T> &a, Father<T> &b) {
	// 如果有定义新的对象那么也必须写成:
	Father<T> c;
}

(3) 友元函数调用 必须写成

Father<int> c1, c2;
Father<int> c = add<int>(c1, c2);

类模板与静态变量

代码注释中有详细的用法和知识点。

代码示例:

#include <iostream>
#include <Windows.h>

using namespace std;

template <typename T>
class Father {
public:
	Father(T socre = 0);

	void getScore() const;

public:
	static T value;
private:
	T score;
};

// 静态成员也可以使用类模板参数实例化
template <typename T> 
T Father<T>::value = 666;


template <typename T>
Father<T>::Father(T cosre) {
	this->score = score;
}

template <typename T>
void Father<T>::getScore() const {
	return score;
}


int main(void) {
	Father<int> f1, f2, f3;

	cout << "f1:" << f1.value << endl;
	// 使用f2去修改静态变量value的值
	f2.value = 888;
	cout << "f3:" << f3.value << endl;
	// 由结果可以看出,f1, f2, f3 共享static静态变量value


	cout << "*****************************" << endl;
	Father<float> s1, s2, s3;
	
	// 使用不同类型的对象去修改value
	s1.value = 999;
	cout << "f3:" << f3.value << endl;
	// 由结果可以知道,不同类型的对象他们并没有共性static静态变量value

	s2.value = 111;
	cout << "s3:" << s3.value << endl;

	// 最后得出总结:
	// 类模板中,同一类型的对象共享同一个静态变量
	// 不同类型的对象并没有共享同一个静态变量,而是有各自的静态变量
	return 0;
}

运行截图:

在这里插入图片描述

分开三个文件来实现用法都是一样的,这里就不介绍了。

总结:

  1. 从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员;
  2. 和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化;
  3. static 数据成员也可以使用虚拟类型参数T。

类模板使用总结

归纳以上的介绍,可以这样声明和使用类模板:

  1. 先写出一个实际的类。

  2. 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的T)。

  3. 在类声明前面加入一行,格式为:
    template <typename 虚拟类型参数>
    如:
    template < typename numtype >
    class A
    {…}; //类体

  4. 用类模板定义对象时用以下形式:
    类模板名<实际类型名> 对象名;
    或 类模板名<实际类型名> 对象名(实参表列);
    如:
    A cmp;
    A cmp(3,7);

  5. 如果在类模板外定义成员函数,应写成类模板形式:
    template <typename 虚拟类型参数>
    函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {…}

关于类模板的几点补充:

  1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加typename 或class,如:
    template <typename T1,typename T2>
    class someclass
    {…};
    在定义对象时分别代入实际的类型名,如:
    someclass<int, char> object;

  2. 和使用类一样,使用类模板时要注意其作用域,只有在它的有效作用域内用使用它定义对象。

  3. 模板类也可以有支持继承,有层次关系,一个类模板可以作为基类,派生出派生模板类。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!